feat: 完成发货撤回优化、财务单据清理与多模块功能完善
本次提交完成了一系列核心功能优化与修复: 1. **发货流程优化**:实现发货撤回校验、发货状态展示与控制,限制一次性发满剩余应发数量,新增通过出库单号反查库存明细的方法 2. **财务单据优化**:新增多级财务单据级联删除方法,修复应收/应付单搜索模糊匹配问题,实现红冲单负数金额高亮展示 3. **前端页面优化**:新增发货状态标签、应收/应付单商品明细展示,修复产品表单预装系统类型显示逻辑,优化文件上传非必填校验 4. **性能优化**:为高频查询表添加二级索引优化撤回操作性能 5. **工具类新增**:新增商品明细组装工具类,支持冲红单明细金额缩放适配 6. **业务逻辑修复**:修复订单撤单时的应收应付处理逻辑,移除冗余的权限默认过滤,新增重复撤回拦截校验dev_1.0.3
parent
054c0a1789
commit
9cc5c64fee
|
|
@ -61,6 +61,14 @@ export function recallApply(id, reason, amountChanged) {
|
|||
})
|
||||
}
|
||||
|
||||
// 按发货记录 id 检查关联财务单据的审批状态(撤回/作废按钮前置校验)
|
||||
export function checkRecallByDelivery(deliveryId) {
|
||||
return request({
|
||||
url: '/inventory/delivery/vue/recall/check-by-delivery/' + deliveryId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 导出采购合同
|
||||
export function exportDelivery(query) {
|
||||
return request({
|
||||
|
|
|
|||
|
|
@ -41,14 +41,14 @@
|
|||
<!-- @keyup.enter.native="handleQuery"-->
|
||||
<!-- />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="出入库单号" prop="inventoryCode">-->
|
||||
<!-- <el-input-->
|
||||
<!-- v-model="queryParams.inventoryCode"-->
|
||||
<!-- placeholder="请输入出入库单号"-->
|
||||
<!-- clearable-->
|
||||
<!-- @keyup.enter.native="handleQuery"-->
|
||||
<!-- />-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="出入库单号" prop="inventoryCode">
|
||||
<el-input
|
||||
v-model="queryParams.inventoryCode"
|
||||
placeholder="请输入出入库单号"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品类型" prop="productType">
|
||||
<el-select v-model="queryParams.productType" placeholder="请选择产品类型" clearable>
|
||||
<el-option
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="payableList" show-summary :summary-method="getSummaries" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="payableList" show-summary :summary-method="getSummaries" :cell-class-name="amountCellClassName" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column label="项目编号" align="center" prop="projectCode" width="120" />
|
||||
<el-table-column label="项目名称" align="center" prop="projectName" width="260" />
|
||||
|
|
@ -467,6 +467,15 @@ export default {
|
|||
}
|
||||
return {};
|
||||
},
|
||||
/** 金额列负数(红冲单)时加红色样式类 */
|
||||
amountCellClassName({ row, column }) {
|
||||
const amountProps = ['planAmount', 'totalPriceWithTax', 'unpaidPaymentAmount', 'unreceivedTicketAmount'];
|
||||
const cellValue = row[column.property];
|
||||
if (amountProps.includes(column.property) && Number(cellValue) < 0) {
|
||||
return 'amount-negative';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
/** 时间处理 */
|
||||
timeProcessing(value) {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
|
|
@ -476,8 +485,13 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.operation-column .el-button--text:hover {
|
||||
color: red !important;
|
||||
}
|
||||
/* 红冲单负数金额显示红色 */
|
||||
::v-deep .amount-negative {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
<template>
|
||||
<el-dialog :title="titleText" :visible.sync="dialogVisible" width="900px" @close="handleClose">
|
||||
<el-dialog
|
||||
:title="dialogTitle"
|
||||
:visible.sync="dialogVisible"
|
||||
:width="dialogWidth"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
custom-class="upload-receipt-dialog"
|
||||
>
|
||||
<div v-if="loading" class="loading-spinner">
|
||||
<i class="el-icon-loading"></i>
|
||||
</div>
|
||||
<div v-else class="receipt-dialog-body">
|
||||
<div v-if="canUpload" class="upload-btn-container">
|
||||
<el-button type="primary" icon="el-icon-upload" v-hasPermi="['finance:payment:uploadReceipt']" @click="openUploadDialog">上传{{ titleText }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-timeline v-if="attachments.length > 0">
|
||||
<el-timeline-item
|
||||
v-for="attachment in attachments"
|
||||
:key="attachment.id"
|
||||
:timestamp="parseTime(attachment.createTime, '{y}-{m}-{d} {h}:{i}:{s}')"
|
||||
placement="top"
|
||||
>
|
||||
<el-card>
|
||||
<div class="receipt-card-content">
|
||||
<!-- 已上传:展示回执单图片及单据信息 -->
|
||||
<div v-else-if="hasUploaded" class="receipt-view">
|
||||
<el-card v-for="attachment in activeAttachments" :key="attachment.id" class="receipt-view-card">
|
||||
<div class="receipt-details">
|
||||
<div class="detail-item">
|
||||
<span class="item-label">支付方式</span>
|
||||
|
|
@ -32,40 +29,20 @@
|
|||
v-if="!isPdf(attachment.filePath)"
|
||||
:src="getImageUrl(attachment.filePath)"
|
||||
:preview-src-list="previewList"
|
||||
style="width: 200px; height: 150px;"
|
||||
style="width: 100%; height: 100%;"
|
||||
fit="contain"
|
||||
></el-image>
|
||||
<div v-else-if="pdfUrls[attachment.filePath]" class="pdf-thumbnail-container" @click="openPdfPreview(pdfUrls[attachment.filePath])">
|
||||
<iframe :src="pdfUrls[attachment.filePath]" width="100%" height="150px" frameborder="0"></iframe>
|
||||
<div
|
||||
v-else-if="pdfUrls[attachment.filePath]"
|
||||
class="pdf-thumbnail-container"
|
||||
@click="openPdfPreview(pdfUrls[attachment.filePath])"
|
||||
>
|
||||
<iframe :src="pdfUrls[attachment.filePath]" width="100%" height="100%" frameborder="0"></iframe>
|
||||
<div class="pdf-hover-overlay">
|
||||
<i class="el-icon-zoom-in"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="attachment.delFlag === '2'" class="void-overlay">作废</div>
|
||||
</div>
|
||||
<el-row>
|
||||
<el-col span="8">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
class="download-btn"
|
||||
icon="el-icon-download"
|
||||
@click="downloadFile(attachment)"
|
||||
>下载{{ titleText }}</el-button>
|
||||
</el-col>
|
||||
<el-col span="8">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
class="download-btn"
|
||||
icon="el-icon-remove"
|
||||
v-if="paymentData.paymentBillType==='PRE_PAYMENT' && attachment.delFlag !== '2'"
|
||||
v-hasPermi="['finance:attachment:delete']"
|
||||
@click="deleteFile(paymentData)"
|
||||
>作废{{ titleText }}
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
|
|
@ -76,37 +53,19 @@
|
|||
<span class="item-label">备注</span>
|
||||
<span class="item-value">{{ attachment.remark }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="item-label">上传时间</span>
|
||||
<span class="item-value">{{ parseTime(attachment.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="receipt-view-footer">
|
||||
<el-button size="mini" type="primary" icon="el-icon-download" @click="downloadFile(attachment)">下载{{ titleText }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else :description="'暂无' + titleText"></el-empty>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</span>
|
||||
|
||||
<!-- PDF Preview Dialog -->
|
||||
<el-dialog
|
||||
:visible.sync="pdfPreviewVisible"
|
||||
width="80%"
|
||||
top="5vh"
|
||||
append-to-body
|
||||
custom-class="pdf-preview-dialog"
|
||||
>
|
||||
<iframe :src="currentPdfUrl" width="100%" height="600px" frameborder="0"></iframe>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Upload Dialog -->
|
||||
<el-dialog
|
||||
:title="'上传' + titleText"
|
||||
:visible.sync="uploadDialogVisible"
|
||||
width="70vw"
|
||||
append-to-body
|
||||
@close="closeUploadDialog"
|
||||
custom-class="upload-receipt-dialog"
|
||||
>
|
||||
<!-- 未上传:上传表单(整个弹窗区域均可拖拽上传,拖拽事件在 document 上统一处理) -->
|
||||
<div v-else class="upload-area">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form :model="uploadForm" ref="uploadForm" label-width="120px" size="medium" >
|
||||
|
|
@ -121,20 +80,35 @@
|
|||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="paymentData.paymentBillType==='FROM_PAYABLE'?'回执单': '退款图'" required>
|
||||
<div style="display: flex; flex-direction: column; align-items: flex-start;">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
:show-file-list="false"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
<div
|
||||
ref="pasteZone"
|
||||
class="paste-zone"
|
||||
:class="{ 'is-focused': pasteZoneFocused, 'is-dragover': dragOver, 'is-selected': !!uploadForm.file }"
|
||||
tabindex="0"
|
||||
@click="chooseFile"
|
||||
@focus="pasteZoneFocused = true"
|
||||
@blur="pasteZoneFocused = false"
|
||||
>
|
||||
<el-button size="small" type="primary" icon="el-icon-upload2">{{ uploadForm.file ? '重新上传' : '点击上传' }}</el-button>
|
||||
</el-upload>
|
||||
<div class="el-upload__tip" style="line-height: 1.5; margin-top: 5px;">支持上传PNG、JPG、PDF文件格式</div>
|
||||
<template v-if="uploadForm.file">
|
||||
<img v-if="previewUrl && !isPreviewPdf" :src="previewUrl" class="paste-zone-thumb" />
|
||||
<i v-else class="el-icon-document paste-zone-icon"></i>
|
||||
<div class="paste-zone-filename" :title="uploadForm.file.name">{{ uploadForm.file.name }}</div>
|
||||
<div class="paste-zone-subtext">点击可重新选择,或将新文件拖拽 / 粘贴到此处</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="el-icon-picture-outline paste-zone-icon"></i>
|
||||
<div class="paste-zone-text">点击选择文件,或将文件拖拽到此处</div>
|
||||
<div class="paste-zone-text">也可按 Ctrl+V / Command+V 粘贴图片</div>
|
||||
<div class="paste-zone-subtext">支持 JPG / PNG / PDF,大小不超过 2MB</div>
|
||||
</template>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
class="hidden-file-input"
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.pdf,image/jpeg,image/png,application/pdf"
|
||||
@change="handleFileInputChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="含税总价">
|
||||
<span>{{ paymentData.totalPriceWithTax }}</span>
|
||||
|
|
@ -153,32 +127,63 @@
|
|||
</el-form>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div v-if="uploadForm.file" class="preview-file-bar">
|
||||
<span class="preview-file-name" :title="uploadForm.file.name">
|
||||
<i class="el-icon-document"></i> {{ uploadForm.file.name }}
|
||||
</span>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" @click="removeFile">移除</el-button>
|
||||
</div>
|
||||
<div class="upload-preview-container" style="height: 70vh;">
|
||||
<div v-if="previewUrl" class="preview-content">
|
||||
<div v-if="fetchingUrl" class="preview-fetching">
|
||||
<i class="el-icon-loading"></i>
|
||||
<span>正在获取拖拽的图片...</span>
|
||||
</div>
|
||||
<div v-else-if="previewUrl" class="preview-content">
|
||||
<img v-if="!isPreviewPdf" :src="previewUrl" class="preview-image" />
|
||||
<iframe v-else :src="previewUrl" width="100%" height="100%" frameborder="0"></iframe>
|
||||
<el-button
|
||||
class="preview-remove-btn"
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
circle
|
||||
title="移除文件"
|
||||
@click="removeFile"
|
||||
></el-button>
|
||||
</div>
|
||||
<div v-else class="preview-placeholder">
|
||||
<div class="placeholder-icon">
|
||||
<i class="el-icon-picture"></i>
|
||||
</div>
|
||||
<div class="placeholder-text">点击图片进入预览</div>
|
||||
<div class="placeholder-text">尚未选择文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitNewUpload">保存</el-button>
|
||||
<el-button @click="closeUploadDialog">取消</el-button>
|
||||
<template v-if="!loading && !hasUploaded">
|
||||
<el-button type="primary" :loading="uploading" :disabled="fetchingUrl" @click="submitNewUpload">保存</el-button>
|
||||
<el-button :disabled="uploading" @click="dialogVisible = false">取消</el-button>
|
||||
</template>
|
||||
<el-button v-else @click="dialogVisible = false">关闭</el-button>
|
||||
</span>
|
||||
|
||||
<!-- PDF Preview Dialog -->
|
||||
<el-dialog
|
||||
:visible.sync="pdfPreviewVisible"
|
||||
width="80%"
|
||||
top="5vh"
|
||||
append-to-body
|
||||
custom-class="pdf-preview-dialog"
|
||||
>
|
||||
<iframe :src="currentPdfUrl" width="100%" height="600px" frameborder="0"></iframe>
|
||||
</el-dialog>
|
||||
|
||||
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {deleteFile, getPaymentAttachments, uploadPaymentAttachment} from "@/api/finance/payment";
|
||||
import { getPaymentAttachments, uploadPaymentAttachment } from "@/api/finance/payment";
|
||||
import request from '@/utils/request';
|
||||
|
||||
export default {
|
||||
|
|
@ -190,7 +195,7 @@ export default {
|
|||
},
|
||||
paymentData: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
default: () => ({}),
|
||||
},
|
||||
dicts: {
|
||||
type: Object,
|
||||
|
|
@ -200,9 +205,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
// 已上传的回执单附件
|
||||
attachments: [],
|
||||
// Upload Dialog Data
|
||||
uploadDialogVisible: false,
|
||||
uploadForm: {
|
||||
paymentMethod: '',
|
||||
confirmPrice: '',
|
||||
|
|
@ -211,7 +215,12 @@ export default {
|
|||
},
|
||||
previewUrl: '',
|
||||
isPreviewPdf: false,
|
||||
// PDF Preview Data
|
||||
pasteZoneFocused: false,
|
||||
dragOver: false,
|
||||
// 上传中 / 通过图片地址获取中
|
||||
uploading: false,
|
||||
fetchingUrl: false,
|
||||
// PDF 预览
|
||||
pdfUrls: {},
|
||||
pdfPreviewVisible: false,
|
||||
currentPdfUrl: '',
|
||||
|
|
@ -226,31 +235,68 @@ export default {
|
|||
this.$emit("update:visible", val);
|
||||
},
|
||||
},
|
||||
previewList() {
|
||||
return this.attachments
|
||||
.filter(att => !this.isPdf(att.filePath))
|
||||
.map(att => this.getImageUrl(att.filePath));
|
||||
},
|
||||
canUpload() {
|
||||
if (!this.attachments || this.attachments.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return this.attachments.every(att => att.delFlag === '2');
|
||||
},
|
||||
titleText() {
|
||||
return this.paymentData && this.paymentData.paymentBillType === 'REFUND' ? '退款图' : '回执单';
|
||||
},
|
||||
dialogTitle() {
|
||||
return this.loading || this.hasUploaded ? this.titleText : '上传' + this.titleText;
|
||||
},
|
||||
// 查看时收窄弹窗,避免右侧留白过多
|
||||
dialogWidth() {
|
||||
return this.hasUploaded ? '620px' : '70vw';
|
||||
},
|
||||
// 有效(未作废)的附件
|
||||
activeAttachments() {
|
||||
return this.attachments.filter(att => att.delFlag !== '2');
|
||||
},
|
||||
hasUploaded() {
|
||||
return this.activeAttachments.length > 0;
|
||||
},
|
||||
// 当前是否可接收文件(加载中 / 已上传 / 正在获取图片时不可)
|
||||
canAcceptFile() {
|
||||
return !this.loading && !this.hasUploaded && !this.fetchingUrl;
|
||||
},
|
||||
previewList() {
|
||||
return this.activeAttachments
|
||||
.filter(att => !this.isPdf(att.filePath))
|
||||
.map(att => this.getImageUrl(att.filePath));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val && this.paymentData) {
|
||||
if (val) {
|
||||
this.initUploadForm();
|
||||
this.fetchAttachments();
|
||||
document.addEventListener('paste', this.handlePaste);
|
||||
document.addEventListener('dragenter', this.handleGlobalDragOver);
|
||||
document.addEventListener('dragover', this.handleGlobalDragOver);
|
||||
document.addEventListener('drop', this.handleGlobalDrop);
|
||||
document.addEventListener('dragleave', this.handleGlobalDragLeave);
|
||||
document.addEventListener('dragend', this.handleGlobalDragEnd);
|
||||
} else {
|
||||
this.removeGlobalListeners();
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.removeGlobalListeners();
|
||||
},
|
||||
methods: {
|
||||
removeGlobalListeners() {
|
||||
document.removeEventListener('paste', this.handlePaste);
|
||||
document.removeEventListener('dragenter', this.handleGlobalDragOver);
|
||||
document.removeEventListener('dragover', this.handleGlobalDragOver);
|
||||
document.removeEventListener('drop', this.handleGlobalDrop);
|
||||
document.removeEventListener('dragleave', this.handleGlobalDragLeave);
|
||||
document.removeEventListener('dragend', this.handleGlobalDragEnd);
|
||||
this.dragOver = false;
|
||||
},
|
||||
/** 查询已上传的回执单附件 */
|
||||
fetchAttachments() {
|
||||
if (!this.paymentData.id) return;
|
||||
if (!this.paymentData.id) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
getPaymentAttachments(this.paymentData.id, { type: 'payment' })
|
||||
.then(response => {
|
||||
|
|
@ -265,6 +311,40 @@ export default {
|
|||
this.loading = false;
|
||||
});
|
||||
},
|
||||
initUploadForm() {
|
||||
// 确认含税总价默认取含税总价,可手动修改
|
||||
this.uploadForm = {
|
||||
paymentMethod: this.paymentData.paymentMethod,
|
||||
confirmPrice: this.paymentData.totalPriceWithTax,
|
||||
remark: '',
|
||||
file: null
|
||||
};
|
||||
this.previewUrl = '';
|
||||
this.isPreviewPdf = false;
|
||||
this.dragOver = false;
|
||||
this.fetchingUrl = false;
|
||||
this.uploading = false;
|
||||
},
|
||||
handleClose() {
|
||||
if (this.previewUrl) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
}
|
||||
Object.values(this.pdfUrls).forEach(url => URL.revokeObjectURL(url));
|
||||
this.pdfUrls = {};
|
||||
this.attachments = [];
|
||||
this.uploadForm.file = null;
|
||||
this.previewUrl = '';
|
||||
this.pasteZoneFocused = false;
|
||||
this.dragOver = false;
|
||||
this.fetchingUrl = false;
|
||||
this.uploading = false;
|
||||
},
|
||||
getImageUrl(resource) {
|
||||
return process.env.VUE_APP_BASE_API + "/common/download/resource?resource=" + resource;
|
||||
},
|
||||
isPdf(filePath) {
|
||||
return filePath && filePath.toLowerCase().endsWith('.pdf');
|
||||
},
|
||||
loadPdfPreviews() {
|
||||
this.attachments.forEach(att => {
|
||||
if (this.isPdf(att.filePath) && !this.pdfUrls[att.filePath]) {
|
||||
|
|
@ -286,17 +366,6 @@ export default {
|
|||
this.currentPdfUrl = url;
|
||||
this.pdfPreviewVisible = true;
|
||||
},
|
||||
getImageUrl(resource) {
|
||||
return process.env.VUE_APP_BASE_API + "/common/download/resource?resource=" + resource;
|
||||
},
|
||||
isPdf(filePath) {
|
||||
return filePath && filePath.toLowerCase().endsWith('.pdf');
|
||||
},
|
||||
deleteFile(paymentData) {
|
||||
deleteFile(paymentData.id).then(() => {
|
||||
this.fetchAttachments()
|
||||
})
|
||||
},
|
||||
downloadFile(attachment) {
|
||||
const link = document.createElement('a');
|
||||
link.href = this.getImageUrl(attachment.filePath);
|
||||
|
|
@ -306,34 +375,171 @@ export default {
|
|||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
handleClose() {
|
||||
this.attachments = [];
|
||||
// Clean up object URLs
|
||||
Object.values(this.pdfUrls).forEach(url => URL.revokeObjectURL(url));
|
||||
this.pdfUrls = {};
|
||||
/** 点击区域选择文件 */
|
||||
chooseFile() {
|
||||
if (this.loading || this.hasUploaded || this.fetchingUrl) return;
|
||||
const input = this.$refs.fileInput;
|
||||
if (input) {
|
||||
input.click();
|
||||
}
|
||||
},
|
||||
// New Upload Dialog Methods
|
||||
openUploadDialog() {
|
||||
this.uploadForm = {
|
||||
paymentMethod: this.paymentData.paymentMethod,
|
||||
confirmPrice: '',
|
||||
remark: '',
|
||||
file: null
|
||||
};
|
||||
this.previewUrl = '';
|
||||
this.isPreviewPdf = false;
|
||||
this.uploadDialogVisible = true;
|
||||
/** 选择文件回调 */
|
||||
handleFileInputChange(event) {
|
||||
const files = event.target.files;
|
||||
if (files && files.length > 0) {
|
||||
this.processFile(files[0]);
|
||||
}
|
||||
// 清空 value,保证再次选择同一文件也能触发 change
|
||||
event.target.value = '';
|
||||
},
|
||||
closeUploadDialog() {
|
||||
this.uploadDialogVisible = false;
|
||||
this.uploadForm.file = null;
|
||||
this.previewUrl = '';
|
||||
/** 全局 dragover:阻止浏览器默认打开文件,并给出可放置提示(Edge/Chrome/Firefox 通用) */
|
||||
handleGlobalDragOver(event) {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
try {
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
} catch (e) {
|
||||
// 忽略:部分浏览器不允许设置 dropEffect
|
||||
}
|
||||
}
|
||||
if (this.canAcceptFile) {
|
||||
this.dragOver = true;
|
||||
}
|
||||
},
|
||||
handleFileChange(file) {
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.raw.type);
|
||||
/** 全局 dragleave:仅当拖出窗口时清除高亮 */
|
||||
handleGlobalDragLeave(event) {
|
||||
if (!event.relatedTarget) {
|
||||
this.dragOver = false;
|
||||
}
|
||||
},
|
||||
/** 全局 dragend:拖拽结束清除高亮 */
|
||||
handleGlobalDragEnd() {
|
||||
this.dragOver = false;
|
||||
},
|
||||
/** 全局 drop:统一处理拖拽上传 */
|
||||
handleGlobalDrop(event) {
|
||||
event.preventDefault();
|
||||
this.dragOver = false;
|
||||
if (!this.canAcceptFile) return;
|
||||
const dataTransfer = event.dataTransfer;
|
||||
if (!dataTransfer) return;
|
||||
const files = this.extractFilesFromDataTransfer(dataTransfer);
|
||||
if (files.length > 0) {
|
||||
if (files.length > 1) {
|
||||
this.$message.warning('一次仅支持上传一个文件,已取第一个');
|
||||
}
|
||||
this.processFile(files[0]);
|
||||
return;
|
||||
}
|
||||
// 从网页/其他应用拖拽图片时,部分浏览器只提供图片地址而不提供文件
|
||||
const url = this.extractUrlFromDataTransfer(dataTransfer);
|
||||
if (url) {
|
||||
this.loadFileFromUrl(url);
|
||||
}
|
||||
},
|
||||
/** 从拖拽数据中提取文件列表,兼容仅存在于 items 中的情况 */
|
||||
extractFilesFromDataTransfer(dataTransfer) {
|
||||
if (dataTransfer.files && dataTransfer.files.length > 0) {
|
||||
return Array.from(dataTransfer.files);
|
||||
}
|
||||
if (dataTransfer.items && dataTransfer.items.length > 0) {
|
||||
return Array.from(dataTransfer.items)
|
||||
.filter(item => item.kind === 'file')
|
||||
.map(item => item.getAsFile())
|
||||
.filter(file => !!file);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
/** 从拖拽数据中提取图片地址 */
|
||||
extractUrlFromDataTransfer(dataTransfer) {
|
||||
if (!dataTransfer.getData) return '';
|
||||
let url = '';
|
||||
try {
|
||||
url = dataTransfer.getData('text/uri-list') || dataTransfer.getData('text/plain') || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
url = (url || '').split('\n')[0].trim();
|
||||
return /^(https?:|data:image)/i.test(url) ? url : '';
|
||||
},
|
||||
/** 通过图片地址获取文件 */
|
||||
loadFileFromUrl(url) {
|
||||
this.fetchingUrl = true;
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('request failed');
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
this.processFile(this.createFile(blob, this.buildFileName('drag', blob.type)));
|
||||
this.fetchingUrl = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.fetchingUrl = false;
|
||||
this.$message.error('无法获取拖拽的图片,请先保存到本地后再拖拽或选择文件');
|
||||
});
|
||||
},
|
||||
/** 处理粘贴(截图、图片或复制的文件) */
|
||||
handlePaste(event) {
|
||||
if (this.loading || this.hasUploaded || this.fetchingUrl) return;
|
||||
const clipboardData = event.clipboardData || (event.originalEvent && event.originalEvent.clipboardData);
|
||||
if (!clipboardData) return;
|
||||
const file = this.extractFileFromClipboard(clipboardData);
|
||||
if (!file) return;
|
||||
event.preventDefault();
|
||||
this.processFile(file);
|
||||
},
|
||||
/** 从剪贴板中提取文件,兼容 items / files 两种来源 */
|
||||
extractFileFromClipboard(clipboardData) {
|
||||
let file = null;
|
||||
if (clipboardData.items && clipboardData.items.length > 0) {
|
||||
const fileItem = Array.from(clipboardData.items).find(item => item.kind === 'file');
|
||||
if (fileItem) {
|
||||
file = fileItem.getAsFile();
|
||||
}
|
||||
}
|
||||
if (!file && clipboardData.files && clipboardData.files.length > 0) {
|
||||
file = clipboardData.files[0];
|
||||
}
|
||||
if (!file) return null;
|
||||
// 部分浏览器粘贴时文件名为空,补一个带扩展名的名称
|
||||
if (file.name) return file;
|
||||
return this.createFile(file, this.buildFileName('paste', file.type));
|
||||
},
|
||||
/** 根据类型生成带扩展名的文件名 */
|
||||
buildFileName(prefix, type) {
|
||||
const t = (type || '').toLowerCase();
|
||||
const ext = t === 'application/pdf' ? 'pdf' : (t.split('/')[1] || 'png').replace('jpeg', 'jpg');
|
||||
return `${prefix}-${Date.now()}.${ext}`;
|
||||
},
|
||||
/** 创建文件对象,兼容不支持 File 构造函数的浏览器 */
|
||||
createFile(blob, name) {
|
||||
try {
|
||||
return new File([blob], name, { type: blob.type });
|
||||
} catch (e) {
|
||||
try {
|
||||
Object.defineProperty(blob, 'name', { value: name, configurable: true });
|
||||
} catch (err) {
|
||||
// 忽略:部分环境不允许扩展 Blob 属性
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
},
|
||||
/** 判断是否为允许的文件类型(兼容部分环境 MIME 为空/不准确的情况) */
|
||||
isAcceptedFile(rawFile) {
|
||||
const type = (rawFile.type || '').toLowerCase();
|
||||
const name = (rawFile.name || '').toLowerCase();
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/pjpeg', 'image/png', 'application/pdf'];
|
||||
const extAccepted = /\.(jpe?g|png|pdf)$/.test(name);
|
||||
return allowedTypes.includes(type) || extAccepted;
|
||||
},
|
||||
/** 校验并设置待上传文件 */
|
||||
processFile(rawFile) {
|
||||
if (!rawFile) return;
|
||||
const size = rawFile.size;
|
||||
const isLt2M = typeof size !== 'number' || size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isAcceptedType) {
|
||||
if (!this.isAcceptedFile(rawFile)) {
|
||||
this.$message.error('上传文件只能是 JPG/PNG/PDF 格式!');
|
||||
return;
|
||||
}
|
||||
|
|
@ -342,16 +548,25 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.uploadForm.file = file.raw;
|
||||
this.isPreviewPdf = file.raw.type === 'application/pdf';
|
||||
if (this.previewUrl) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
}
|
||||
this.uploadForm.file = rawFile;
|
||||
this.isPreviewPdf = rawFile.type === 'application/pdf' || /\.pdf$/i.test(rawFile.name || '');
|
||||
|
||||
this.previewUrl = URL.createObjectURL(file.raw);
|
||||
this.previewUrl = URL.createObjectURL(rawFile);
|
||||
},
|
||||
handleFileRemove() {
|
||||
/** 移除已选文件 */
|
||||
removeFile() {
|
||||
if (this.previewUrl) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
}
|
||||
this.uploadForm.file = null;
|
||||
this.previewUrl = '';
|
||||
this.isPreviewPdf = false;
|
||||
},
|
||||
submitNewUpload() {
|
||||
if (this.uploading || this.fetchingUrl) return;
|
||||
if (!this.uploadForm.file) {
|
||||
this.$message.warning("请选择要上传的文件");
|
||||
return;
|
||||
|
|
@ -366,18 +581,22 @@ export default {
|
|||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", this.uploadForm.file);
|
||||
// 显式带文件名,兼容部分浏览器/经地址获取的文件没有 name 的情况
|
||||
formData.append("file", this.uploadForm.file, this.uploadForm.file.name || 'receipt');
|
||||
formData.append("relatedBillId", this.paymentData.id);
|
||||
formData.append("remark", this.uploadForm.remark);
|
||||
|
||||
this.uploading = true;
|
||||
uploadPaymentAttachment(formData)
|
||||
.then(response => {
|
||||
.then(() => {
|
||||
this.$message.success("上传成功");
|
||||
this.closeUploadDialog();
|
||||
this.fetchAttachments();
|
||||
})
|
||||
.catch(error => {
|
||||
.catch(() => {
|
||||
this.$message.error("上传失败");
|
||||
})
|
||||
.then(() => {
|
||||
this.uploading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -385,17 +604,19 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.receipt-dialog-body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
padding: 20px;
|
||||
padding: 40px;
|
||||
}
|
||||
.receipt-card-content {
|
||||
/* 回执单查看 */
|
||||
.receipt-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
.receipt-view-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.receipt-details {
|
||||
flex-grow: 1;
|
||||
|
|
@ -417,104 +638,22 @@ export default {
|
|||
}
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
min-height: 150px;
|
||||
width: 320px;
|
||||
height: 260px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #DCDFE6;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.void-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-30deg);
|
||||
color: red;
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
.download-btn {
|
||||
display: block;
|
||||
}
|
||||
.upload-btn-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.pdf-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
color: #606266;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pdf-placeholder:hover {
|
||||
border-color: #409EFF;
|
||||
color: #409EFF;
|
||||
}
|
||||
.pdf-placeholder .el-icon-document {
|
||||
font-size: 48px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* New Dialog Styles */
|
||||
.upload-preview-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.preview-pdf {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
.preview-pdf .el-icon-document {
|
||||
font-size: 64px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.preview-placeholder {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
}
|
||||
.placeholder-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 10px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
.receipt-view-footer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.pdf-thumbnail-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pdf-hover-overlay {
|
||||
|
|
@ -531,14 +670,144 @@ export default {
|
|||
transition: opacity 0.3s;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
pointer-events: none; /* Let clicks pass through to container, but this is overlay on top of iframe so actually we want it to capture clicks if iframe swallows them? */
|
||||
/* Actually, if pointer-events is none, the click goes to the iframe and iframe swallows it. */
|
||||
/* So we want pointer-events: auto on the overlay, or just rely on the container. */
|
||||
/* If container has the click listener, and overlay covers everything, overlay needs to propagate click or handle it. */
|
||||
/* A simple way: make overlay clickable. */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pdf-thumbnail-container:hover .pdf-hover-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 粘贴区域 */
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
.paste-zone {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 35px 20px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
outline: none;
|
||||
transition: border-color 0.3s, color 0.3s, background-color 0.3s;
|
||||
}
|
||||
.paste-zone.is-selected {
|
||||
border-color: #67C23A;
|
||||
background-color: #f0f9eb;
|
||||
color: #67C23A;
|
||||
}
|
||||
.paste-zone-thumb {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 120px;
|
||||
margin: 0 auto 10px;
|
||||
border-radius: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.paste-zone-filename {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.paste-zone:hover,
|
||||
.paste-zone.is-focused,
|
||||
.paste-zone.is-dragover {
|
||||
border-color: #409EFF;
|
||||
color: #409EFF;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
.paste-zone.is-dragover {
|
||||
border-style: solid;
|
||||
}
|
||||
.paste-zone-icon {
|
||||
display: block;
|
||||
font-size: 38px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.paste-zone-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
.paste-zone-subtext {
|
||||
font-size: 12px;
|
||||
line-height: 1.9;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
/* New Dialog Styles */
|
||||
.upload-preview-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.preview-file-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 5px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.preview-file-name {
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.preview-remove-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.preview-fetching {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
.preview-fetching i {
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.preview-placeholder {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
}
|
||||
.placeholder-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 10px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -92,6 +92,27 @@
|
|||
<div style="padding: 20px">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="明细" name="details">
|
||||
<el-divider content-position="left">商品明细</el-divider>
|
||||
<el-table :data="formData.goodsDetailList" style="width: 100%">
|
||||
<el-table-column type="index" label="序号" width="50"></el-table-column>
|
||||
<el-table-column prop="productType" label="产品类型" width="120">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.product_type" :value="scope.row.productType"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="productCode" label="产品编码" width="120"></el-table-column>
|
||||
<el-table-column prop="productModel" label="产品型号" width="120"></el-table-column>
|
||||
<el-table-column prop="productDescription" label="产品描述" min-width="180" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="quantity" label="数量" width="100" align="center"></el-table-column>
|
||||
<el-table-column prop="unit" label="单位" width="80" align="center"></el-table-column>
|
||||
<el-table-column prop="price" label="单价" width="120" align="center" :formatter="(row, column, cellValue)=>formatCurrency(cellValue)"></el-table-column>
|
||||
<el-table-column prop="taxRate" label="税率" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.taxRate * 100 }}%
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amountTotal" label="含税小计" width="140" align="center" :formatter="(row, column, cellValue)=>formatCurrency(cellValue)"></el-table-column>
|
||||
</el-table>
|
||||
<el-divider content-position="left">销售-收款单</el-divider>
|
||||
<el-table :data="formData.detailList" style="width: 100%" show-summary :summary-method="getSummaries">
|
||||
<el-table-column type="index" label="序号" width="50"></el-table-column>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@
|
|||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="出入库单号" prop="inventoryCode">
|
||||
<el-input
|
||||
v-model="queryParams.inventoryCode"
|
||||
placeholder="请输入出入库单号"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品类型" prop="productType">
|
||||
<el-select v-model="queryParams.productType" placeholder="请选择产品类型" clearable>
|
||||
<el-option
|
||||
|
|
@ -95,7 +103,7 @@
|
|||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="receivableList" @selection-change="handleSelectionChange">
|
||||
<el-table v-loading="loading" :data="receivableList" :cell-class-name="amountCellClassName" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column label="项目编号" align="center" prop="projectCode" width="120" />
|
||||
<el-table-column label="项目名称" align="center" prop="projectName" width="200" />
|
||||
|
|
@ -204,6 +212,7 @@ export default {
|
|||
projectName: null,
|
||||
receivableBillCode: null,
|
||||
partnerName: null,
|
||||
inventoryCode: null,
|
||||
productType: null,
|
||||
collectionStatus: null,
|
||||
createTimeStart: null,
|
||||
|
|
@ -329,6 +338,15 @@ export default {
|
|||
this.getList(); // Refresh the list
|
||||
});
|
||||
},
|
||||
/** 金额列负数(红冲单)时加红色样式类 */
|
||||
amountCellClassName({ row, column }) {
|
||||
const amountProps = ['planAmount', 'totalPriceWithTax', 'unreceivedAmount', 'uninvoicedAmount'];
|
||||
const cellValue = row[column.property];
|
||||
if (amountProps.includes(column.property) && Number(cellValue) < 0) {
|
||||
return 'amount-negative';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
/** 时间处理 */
|
||||
timeProcessing(value) {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
|
|
@ -338,8 +356,13 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.operation-column .el-button--text:hover {
|
||||
color: red !important;
|
||||
}
|
||||
/* 红冲单负数金额显示红色 */
|
||||
::v-deep .amount-negative {
|
||||
color: #f56c6c;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<el-dialog title="申请付款" :visible.sync="visible" width="1000px" append-to-body :before-close="handleClose"
|
||||
<el-dialog title="申请收款" :visible.sync="visible" width="700px" append-to-body :before-close="handleClose"
|
||||
custom-class="apply-payment-dialog">
|
||||
<el-row :gutter="20">
|
||||
<!-- Left Side: Form Data -->
|
||||
<el-col :span="12">
|
||||
<el-col :span="24">
|
||||
<div class="form-tip">请选择客户的支付方式并确认客户打款的账户信息,提交至财务审批</div>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small">
|
||||
<el-form-item label="支付方式" prop="receiptMethod">
|
||||
|
|
@ -19,36 +19,22 @@
|
|||
<el-form-item label="账户名称" prop="receiptAccountName">
|
||||
<el-input v-model="form.receiptAccountName" placeholder="请输入账户名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="银行账号" prop="receiptBankNumber">
|
||||
<el-input v-model="form.receiptBankNumber" placeholder="请输入银行账号"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="银行开户行" prop="receiptBankOpenAddress">
|
||||
<el-input v-model="form.receiptBankOpenAddress" placeholder="请输入银行开户行"/>
|
||||
<el-select v-model="form.receiptBankOpenAddress" placeholder="请选择银行开户行" style="width: 100%"
|
||||
@change="handleBankInfoChange">
|
||||
<el-option
|
||||
v-for="dict in dicts.bank_info"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.label"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="银行行号" prop="bankNumber">
|
||||
<el-input v-model="form.bankNumber" placeholder="请输入银行行号"/>
|
||||
<el-form-item label="银行行号" prop="bankNumber" class="readonly-item">
|
||||
<el-input v-model="form.bankNumber" :disabled="true" placeholder="选择银行开户行后自动带出"/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- New Field: Client Payment Image Upload -->
|
||||
<el-form-item label="客户付款图" prop="file">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
:show-file-list="false"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
>
|
||||
<el-button size="mini" type="primary" icon="el-icon-upload2">{{
|
||||
form.file ? '重新上传' : '点击上传'
|
||||
}}
|
||||
</el-button>
|
||||
<div slot="tip" class="el-upload__tip">支持JPG/PNG/PDF格式</div>
|
||||
</el-upload>
|
||||
<div v-if="form.file" class="file-name-tip">
|
||||
<i class="el-icon-document"></i> {{ form.fileName }}
|
||||
</div>
|
||||
<el-form-item label="银行账号" prop="receiptBankNumber" class="readonly-item">
|
||||
<el-input v-model="form.receiptBankNumber" :disabled="true" placeholder="选择银行开户行后自动带出"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收款金额" prop="totalPriceWithTax">
|
||||
|
|
@ -56,7 +42,7 @@
|
|||
</el-form-item>
|
||||
|
||||
<!-- New Field: Confirm Receipt Amount -->
|
||||
<el-form-item label="确认收款金额" prop="confirmAmount">
|
||||
<el-form-item label="确认收款金额" prop="confirmAmount" :class="{ 'over-amount-item': isConfirmAmountOver }">
|
||||
<el-input v-model="form.confirmAmount" placeholder="请输入确认收款金额"/>
|
||||
</el-form-item>
|
||||
|
||||
|
|
@ -66,26 +52,6 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
|
||||
<!-- Right Side: Preview -->
|
||||
<el-col :span="12">
|
||||
<div class="preview-container">
|
||||
<div v-if="previewUrl" class="preview-content">
|
||||
<div v-if="isPreviewPdf" class="pdf-preview">
|
||||
<iframe :src="previewUrl" width="100%" height="100%" frameborder="0"></iframe>
|
||||
</div>
|
||||
<div v-else class="image-preview">
|
||||
<img :src="previewUrl" alt="预览图片"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="preview-placeholder">
|
||||
<div class="placeholder-icon">
|
||||
<i class="el-icon-picture-outline"></i>
|
||||
</div>
|
||||
<div class="placeholder-text">上传文件后在此处预览</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
|
|
@ -133,9 +99,6 @@ export default {
|
|||
bankNumber: [
|
||||
{ required: true, message: "请输入银行行号", trigger: "blur" }
|
||||
],
|
||||
file: [
|
||||
{ required: true, message: "请上传客户付款图", trigger: "change" }
|
||||
],
|
||||
confirmAmount: [
|
||||
{ required: true, message: "请输入确认收款金额", trigger: "blur" }
|
||||
]
|
||||
|
|
@ -149,12 +112,8 @@ export default {
|
|||
totalPriceWithTax: null,
|
||||
confirmAmount: null,
|
||||
remark: null,
|
||||
file: null,
|
||||
fileName: '',
|
||||
id: this.receiptData.id
|
||||
},
|
||||
previewUrl: '',
|
||||
isPreviewPdf: false
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -165,19 +124,23 @@ export default {
|
|||
this.form = {
|
||||
id: this.receiptData.id,
|
||||
receiptMethod: this.receiptData.receiptMethod,
|
||||
receiptAccountName: this.receiptData.receiptAccountName,
|
||||
receiptAccountName: this.receiptData.receiptAccountName || '紫光汇智信息技术有限公司',
|
||||
receiptBankNumber: this.receiptData.receiptBankNumber,
|
||||
receiptBankOpenAddress: this.receiptData.receiptBankOpenAddress,
|
||||
bankNumber: this.receiptData.bankNumber,
|
||||
totalPriceWithTax: this.receiptData.totalPriceWithTax,
|
||||
confirmAmount: null,
|
||||
remark: null,
|
||||
file: null,
|
||||
fileName: ''
|
||||
confirmAmount: this.receiptData.totalPriceWithTax,
|
||||
remark: null
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 确认收款金额大于收款金额时高亮提示
|
||||
isConfirmAmountOver() {
|
||||
return this.$calc.sub(this.form.confirmAmount, this.form.totalPriceWithTax) > 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.form = {
|
||||
|
|
@ -188,12 +151,8 @@ export default {
|
|||
bankNumber: null,
|
||||
totalPriceWithTax: null,
|
||||
confirmAmount: null,
|
||||
remark: null,
|
||||
file: null,
|
||||
fileName: ''
|
||||
remark: null
|
||||
};
|
||||
this.previewUrl = '';
|
||||
this.isPreviewPdf = false;
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.resetFields();
|
||||
}
|
||||
|
|
@ -201,31 +160,22 @@ export default {
|
|||
handleClose() {
|
||||
this.$emit("update:visible", false);
|
||||
},
|
||||
handleFileChange(file) {
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.raw.type);
|
||||
|
||||
if (!isAcceptedType) {
|
||||
this.$modal.msgError('上传文件只能是 JPG/PNG/PDF 格式!');
|
||||
// Remove file from upload list if needed, though we use show-file-list="false"
|
||||
// 选择银行开户行后,解析字典键值(json)自动带出银行账号与银行行号
|
||||
handleBankInfoChange(label) {
|
||||
const bank = (this.dicts.bank_info || []).find(item => item.label === label);
|
||||
if (!bank || !bank.value) {
|
||||
return;
|
||||
}
|
||||
if (!isLt10M) {
|
||||
this.$modal.msgError('上传文件大小不能超过 10MB!');
|
||||
return;
|
||||
try {
|
||||
const bankInfo = JSON.parse(bank.value);
|
||||
this.form.receiptBankNumber = bankInfo.receipt_bank_number || null;
|
||||
this.form.bankNumber = bankInfo.bank_number || null;
|
||||
} catch (e) {
|
||||
console.error("bank_info 字典键值解析失败", e);
|
||||
this.form.receiptBankNumber = null;
|
||||
this.form.bankNumber = null;
|
||||
}
|
||||
|
||||
this.form.file = file.raw;
|
||||
this.form.fileName = file.name;
|
||||
this.isPreviewPdf = file.raw.type === 'application/pdf';
|
||||
this.previewUrl = URL.createObjectURL(file.raw);
|
||||
this.$refs.form.validateField('file');
|
||||
},
|
||||
handleFileRemove() {
|
||||
this.form.file = null;
|
||||
this.form.fileName = '';
|
||||
this.previewUrl = '';
|
||||
this.$refs.form.validateField('file');
|
||||
this.$refs.form.validateField(['receiptBankNumber', 'bankNumber']);
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.$calc.sub(this.form.totalPriceWithTax,this.form.confirmAmount)!=0){
|
||||
|
|
@ -238,24 +188,17 @@ export default {
|
|||
const formData = new FormData();
|
||||
// Append regular fields
|
||||
Object.keys(this.form).forEach(key => {
|
||||
if (key !== 'file' && key !== 'fileName' && this.form[key] !== null && this.form[key] !== undefined) {
|
||||
if (this.form[key] !== null && this.form[key] !== undefined) {
|
||||
formData.append(key, this.form[key]);
|
||||
}
|
||||
});
|
||||
// Append file if exists
|
||||
if (this.form.file) {
|
||||
formData.append("file", this.form.file);
|
||||
}
|
||||
|
||||
|
||||
// Since applyPaymentApi usually takes JSON, we might need to verify if backend supports FormData
|
||||
// Assuming we are sending FormData now.
|
||||
applyReceipt(formData).then(response => {
|
||||
this.$modal.msgSuccess("申请付款提交成功");
|
||||
this.$modal.msgSuccess("申请收款提交成功");
|
||||
this.$emit("submit");
|
||||
this.handleClose();
|
||||
}).catch(error => {
|
||||
console.error("申请付款提交失败", error);
|
||||
console.error("申请收款提交失败", error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -275,54 +218,21 @@ export default {
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
/* 只读展示字段:值使用深色文字,便于查看 */
|
||||
.readonly-item ::v-deep .el-input.is-disabled .el-input__inner {
|
||||
color: #303133;
|
||||
background-color: #f5f7fa;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
-webkit-text-fill-color: #303133;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.readonly-item ::v-deep .el-input.is-disabled .el-input__inner::placeholder {
|
||||
color: #c0c4cc;
|
||||
-webkit-text-fill-color: #c0c4cc;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.pdf-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-name-tip {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
/* 确认收款金额大于收款金额时,值显示为红色 */
|
||||
.over-amount-item ::v-deep .el-input__inner {
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ export default {
|
|||
ApplyPaymentDialog,
|
||||
ApplyRefundDialog
|
||||
},
|
||||
dicts:['receipt_bill_type','approve_status','receipt_bill_status', 'payment_method'],
|
||||
dicts:['receipt_bill_type','approve_status','receipt_bill_status', 'payment_method', 'bank_info'],
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@
|
|||
<el-table-column label="产品编码" align="center" width="180" prop="productCode" show-overflow-tooltip/>
|
||||
<el-table-column label="产品型号" align="center" width="180" prop="model" show-overflow-tooltip/>
|
||||
<el-table-column label="发货数量" align="center" width="100" prop="quantity"/>
|
||||
<el-table-column label="发货状态" align="center" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="deliveryStatusTag(scope.row.deliveryStatus)">{{ deliveryStatusText(scope.row.deliveryStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发货方式" align="center" width="100" prop="deliveryType">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.delivery_type" :value="scope.row.deliveryType"/>
|
||||
|
|
@ -76,7 +81,8 @@
|
|||
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-document" @click="handleView(scope.row)">发货记录</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-refresh-left" @click="handleRecall(scope.row)"
|
||||
<!-- 仅「已发货」可撤回;已撤回记录保留展示但不提供撤回入口 -->
|
||||
<el-button v-if="scope.row.deliveryStatus === '1'" size="mini" type="text" icon="el-icon-refresh-left" @click="handleRecall(scope.row)"
|
||||
v-hasPermi="['inventory:delivery:recall']">撤回
|
||||
</el-button>
|
||||
</template>
|
||||
|
|
@ -222,6 +228,16 @@ export default {
|
|||
this.deliveryId = row.id;
|
||||
this.viewOpen = true;
|
||||
},
|
||||
/** 发货状态文案(撤回/作废后的记录保留展示) */
|
||||
deliveryStatusText(status) {
|
||||
const map = { '0': '待发货', '1': '已发货', '2': '已撤回' };
|
||||
return map[status] || '-';
|
||||
},
|
||||
/** 发货状态标签样式 */
|
||||
deliveryStatusTag(status) {
|
||||
const map = { '0': 'info', '1': 'success', '2': 'danger' };
|
||||
return map[status] || 'info';
|
||||
},
|
||||
/** 撤回按钮操作 */
|
||||
handleRecall(row) {
|
||||
const id = row.id;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" min-width="90">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="!readOnly && (scope.row.outerStatus === '1' || scope.row.outerStatus === '4')" class="op-btns">
|
||||
<!-- 已撤回(deliveryStatus='3')的出库单保留展示,但不再提供撤销/确认出库操作 -->
|
||||
<div v-if="!readOnly && scope.row.deliveryStatus !== '3' && (scope.row.outerStatus === '1' || scope.row.outerStatus === '4')" class="op-btns">
|
||||
<el-button size="mini" type="danger" @click="handleDeleteOuter(scope.row)">撤销</el-button>
|
||||
<el-button size="mini" type="default" @click="handleConfirmOuter(scope.row)">确认出库</el-button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -437,6 +437,16 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
// 发货必须一次发满剩余应发数量:所有发货方式(手工选择/导入/指定发货)通用,不允许分多次发货
|
||||
const remainQuantity = Number(this.productData.quantity)
|
||||
- Number(this.productData.deliveryGenerateQuantity || 0)
|
||||
- Number(this.productData.deliveryConfirmQuantity || 0);
|
||||
// 数量数据缺失(NaN)时不在此拦截,交由后端兜底校验,避免异常数据把发货彻底拦住
|
||||
if (!Number.isNaN(remainQuantity) && this.selectedSnList.length !== remainQuantity) {
|
||||
this.$message.error(`发货必须一次发满剩余应发数量(剩余 ${remainQuantity} 台,当前已选 ${this.selectedSnList.length} 台)`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isImported) {
|
||||
this.snList.forEach(item => item.taxRate = this.taxRate)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@
|
|||
<el-table-column label="仓库" prop="warehouseName" />
|
||||
<el-table-column v-if="!viewOnly" label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
<!-- 出库单已撤回/作废:记录保留展示,但不再允许发货,需回到订单重新出库 -->
|
||||
<el-tag v-if="form.deliveryStatus === '3'" type="danger">出库单已撤回</el-tag>
|
||||
<el-button
|
||||
v-else
|
||||
size="mini"
|
||||
type="success"
|
||||
@click="handleDeliver(scope.row)"
|
||||
|
|
@ -61,10 +64,16 @@
|
|||
<el-table-column label="仓库" prop="warehouseName" />
|
||||
<el-table-column label="发货时间" prop="deliveryTime" />
|
||||
<el-table-column label="发货数量" prop="quantity" />
|
||||
<el-table-column label="发货状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="deliveryStatusTag(scope.row.deliveryStatus)">{{ deliveryStatusText(scope.row.deliveryStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发货人" prop="createByName" />
|
||||
<el-table-column label="操作" align="center" width="200">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.deliveryStatus === '0'" class="op-btns">
|
||||
<!-- 出库单已撤回/作废时仅保留查看 -->
|
||||
<div v-if="form.deliveryStatus !== '3' && scope.row.deliveryStatus === '0'" class="op-btns">
|
||||
<el-button size="mini" type="danger" @click="handleDeleteDelivery(scope.row.id)">取消发货</el-button>
|
||||
<el-button size="mini" type="primary" @click="handleConfirmDelivery(scope.row)">确认发货</el-button>
|
||||
</div>
|
||||
|
|
@ -72,7 +81,6 @@
|
|||
<el-button size="mini" type="info" @click="handleViewDelivery(scope.row.id)">查看详情</el-button>
|
||||
<!-- 已提交撤回审批:可查看申请,审批中不可重复提交 -->
|
||||
<el-button v-if="scope.row.approveStatus" size="mini" type="warning" @click="handleViewRecallApply(scope.row)">查看申请</el-button>
|
||||
<el-button v-if="scope.row.approveStatus !== '1'" size="mini" type="danger" @click="handleRecall(scope.row)" v-hasPermi="['inventory:delivery:recall']">撤回 / 作废</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -109,15 +117,18 @@
|
|||
</el-dialog>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button v-if="showReturn && !viewOnly" type="danger" @click="handleStatusChange(form, '4')">退 回</el-button>
|
||||
<!-- 已撤回/作废的出库单不再提供「退回」 -->
|
||||
<el-button v-if="showReturn && !viewOnly && form.deliveryStatus !== '3'" type="danger" @click="handleStatusChange(form, '4')">退 回</el-button>
|
||||
<!-- 撤回/作废按出库单整理:整张出库单一次性处理 -->
|
||||
<el-button v-if="hasRecallableDelivery" type="danger" @click="handleRecallBatch" v-hasPermi="['inventory:delivery:recall']">撤回 / 作废</el-button>
|
||||
<el-button @click="handleCancel">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {changeOuterStatus, getOuter, queryInfo} from '@/api/inventory/outer';
|
||||
import { removeDelivery, updateDeliveryStatus, recallDelivery, recallApply } from '@/api/inventory/delivery';
|
||||
import { changeOuterStatus, getOuter, queryInfo } from '@/api/inventory/outer';
|
||||
import { removeDelivery, updateDeliveryStatus, recallDelivery, recallApply, checkRecallByDelivery } from '@/api/inventory/delivery';
|
||||
import GenerateDeliveryForm from './GenerateDeliveryForm.vue';
|
||||
import DeliveryDetail from '@/views/inventory/delivery/Detail.vue';
|
||||
import OuterRebackDetail from '@/views/approve/all/components/OuterRebackDetail.vue';
|
||||
|
|
@ -147,7 +158,8 @@ export default {
|
|||
// 提交撤回复核弹窗
|
||||
recallSubmitOpen: false,
|
||||
recallSubmitting: false,
|
||||
recallSubmitDeliveryId: null,
|
||||
// 本次提交撤回审批的发货记录集合(按出库单整理,可含多条)
|
||||
recallSubmitDeliveryIds: [],
|
||||
recallSubmitForm: {
|
||||
amountChanged: '',
|
||||
reason: ''
|
||||
|
|
@ -158,6 +170,20 @@ export default {
|
|||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 可撤回/作废的发货记录:已发货(deliveryStatus='1')且未提交撤回审批(approveStatus='1')
|
||||
recallableDeliveries() {
|
||||
return this.deliveryList.filter(row => row.deliveryStatus === '1' && row.approveStatus !== '1');
|
||||
},
|
||||
// 未撤回的发货记录(已撤回记录保留展示,但不参与"出库单是否可退回"的判断)
|
||||
activeDeliveries() {
|
||||
return this.deliveryList.filter(row => row.deliveryStatus !== '2');
|
||||
},
|
||||
// 撤回/作废按钮:放在关闭左侧,按出库单整单处理
|
||||
hasRecallableDelivery() {
|
||||
return !this.viewOnly && this.recallableDeliveries.length > 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(id) {
|
||||
this.reset();
|
||||
|
|
@ -168,7 +194,7 @@ export default {
|
|||
this.form = response.data.inventoryOuter;
|
||||
this.productList = response.data.productVoList || [];
|
||||
this.deliveryList = response.data.deliveryList || [];
|
||||
this.showReturn=this.deliveryList.length<=0;
|
||||
this.showReturn = this.activeDeliveries.length <= 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -187,7 +213,7 @@ export default {
|
|||
this.recallApplyOuterCode = '';
|
||||
this.recallSubmitOpen = false;
|
||||
this.recallSubmitting = false;
|
||||
this.recallSubmitDeliveryId = null;
|
||||
this.recallSubmitDeliveryIds = [];
|
||||
this.recallSubmitForm = { amountChanged: '', reason: '' };
|
||||
if (this.$refs.recallSubmitForm) {
|
||||
this.$refs.recallSubmitForm.clearValidate();
|
||||
|
|
@ -196,9 +222,11 @@ export default {
|
|||
// 刷新所有表格数据
|
||||
refreshTables() {
|
||||
queryInfo(this.form.id).then(res => {
|
||||
// 出库单状态也需要同步刷新(撤回/作废后出库单标记为已撤回,控制发货入口)
|
||||
this.form = res.data.inventoryOuter || this.form;
|
||||
this.productList = res.data.productVoList || [];
|
||||
this.deliveryList = res.data.deliveryList || [];
|
||||
this.showReturn = this.deliveryList.length <= 0;
|
||||
this.showReturn = this.activeDeliveries.length <= 0;
|
||||
});
|
||||
},
|
||||
// 发货按钮
|
||||
|
|
@ -245,6 +273,16 @@ export default {
|
|||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
/** 发货状态文案(撤回/作废后的记录保留展示) */
|
||||
deliveryStatusText(status) {
|
||||
const map = { '0': '待发货', '1': '已发货', '2': '已撤回' };
|
||||
return map[status] || '-';
|
||||
},
|
||||
/** 发货状态标签样式 */
|
||||
deliveryStatusTag(status) {
|
||||
const map = { '0': 'info', '1': 'success', '2': 'danger' };
|
||||
return map[status] || 'info';
|
||||
},
|
||||
// 查看发货详情
|
||||
handleViewDelivery(deliveryId) {
|
||||
this.deliveryId = deliveryId;
|
||||
|
|
@ -256,15 +294,69 @@ export default {
|
|||
this.recallApplyOuterCode = row.outerCode;
|
||||
this.recallApplyOpen = true;
|
||||
},
|
||||
// 撤回/作废发货记录入口:按创建时间分流处理
|
||||
handleRecall(row) {
|
||||
if (this.isToday(row.createTime)) {
|
||||
// 今日创建的记录:直接处理
|
||||
this.handleRecallDelivery(row);
|
||||
} else {
|
||||
// 非今日创建的记录:走审批流程
|
||||
this.handleRecallNotToday(row);
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 撤回 / 作废:按出库单整理,整张出库单一次性处理
|
||||
// 今日发货记录 → 直接撤回;非今日发货记录 → 提交出库撤回审批
|
||||
// 应收/应付单据以出库单为颗粒度,由后端在整单撤回完成后统一处理一次
|
||||
// ══════════════════════════════════════════════════════
|
||||
async handleRecallBatch() {
|
||||
const rows = this.recallableDeliveries;
|
||||
if (!rows.length) {
|
||||
this.$message.warning('当前没有可撤回 / 作废的发货记录');
|
||||
return;
|
||||
}
|
||||
const todayRows = rows.filter(row => this.isToday(row.createTime));
|
||||
const otherRows = rows.filter(row => !this.isToday(row.createTime));
|
||||
if (todayRows.length) {
|
||||
const confirmed = await this.confirmRecall(todayRows[0].id);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.$modal.loading('撤回中,请稍候...');
|
||||
try {
|
||||
for (const row of todayRows) {
|
||||
await recallDelivery(row.id);
|
||||
}
|
||||
} catch (e) {
|
||||
this.$modal.closeLoading();
|
||||
this.refreshTables();
|
||||
return;
|
||||
}
|
||||
this.$modal.closeLoading();
|
||||
this.$message.success('撤回 / 作废 成功');
|
||||
this.refreshTables();
|
||||
this.$emit('success');
|
||||
}
|
||||
if (otherRows.length) {
|
||||
// 非今日记录:填写撤回原因后统一提交出库撤回审批
|
||||
this.recallSubmitDeliveryIds = otherRows.map(row => row.id);
|
||||
this.recallSubmitForm = { amountChanged: '', reason: '' };
|
||||
this.recallSubmitOpen = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.recallSubmitForm && this.$refs.recallSubmitForm.clearValidate();
|
||||
});
|
||||
}
|
||||
},
|
||||
// 撤回前置校验与二次确认,返回是否继续
|
||||
confirmRecall(deliveryId) {
|
||||
return checkRecallByDelivery(deliveryId)
|
||||
.then(res => res.data || {})
|
||||
.catch(() => ({}))
|
||||
.then(data => {
|
||||
if (data.blocked) {
|
||||
// 仅存在审批中的财务单据 → 拦截,提示先驳回审批后再次操作
|
||||
this.$message.error(data.tip || '存在审批中的财务单据(收款单/开票单/付款单/收票单),请驳回审批后再次操作');
|
||||
return false;
|
||||
}
|
||||
const tip = data.approved
|
||||
? '存在审核通过的财务单据,整张出库单撤回后将自动生成应收/应付冲红单。确认继续发起撤回?'
|
||||
: (data.tip ? data.tip + '。' : '') + '撤回 / 作废 该出库单下的发货记录后无法恢复,操作不可逆转,确认无误后再执行!';
|
||||
return this.$confirm(tip, '撤回/作废确认', {
|
||||
confirmButtonText: '确认撤回',
|
||||
cancelButtonText: '再想想',
|
||||
type: 'warning'
|
||||
}).then(() => true).catch(() => false);
|
||||
});
|
||||
},
|
||||
// 判断日期是否为今天
|
||||
isToday(dateStr) {
|
||||
|
|
@ -276,56 +368,36 @@ export default {
|
|||
date.getMonth() === today.getMonth() &&
|
||||
date.getDate() === today.getDate();
|
||||
},
|
||||
// 非今日创建记录:提交出库撤回审批(需填写金额是否变化与撤回原因)
|
||||
handleRecallNotToday(row) {
|
||||
this.recallSubmitDeliveryId = row.id;
|
||||
this.recallSubmitForm = { amountChanged: '', reason: '' };
|
||||
this.recallSubmitOpen = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.recallSubmitForm && this.$refs.recallSubmitForm.clearValidate();
|
||||
});
|
||||
},
|
||||
// 关闭提交撤回复核弹窗
|
||||
handleRecallSubmitCancel() {
|
||||
this.recallSubmitOpen = false;
|
||||
this.recallSubmitting = false;
|
||||
},
|
||||
// 确认提交出库撤回审批
|
||||
// 确认提交出库撤回审批(出库单下多条非今日记录共用一次填报)
|
||||
submitRecallApply() {
|
||||
this.$refs.recallSubmitForm.validate(valid => {
|
||||
this.$refs.recallSubmitForm.validate(async valid => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const { amountChanged, reason } = this.recallSubmitForm;
|
||||
this.recallSubmitting = true;
|
||||
this.$modal.loading();
|
||||
recallApply(this.recallSubmitDeliveryId, reason.trim(), amountChanged)
|
||||
.then(() => {
|
||||
this.$modal.closeLoading();
|
||||
try {
|
||||
for (const id of this.recallSubmitDeliveryIds) {
|
||||
// 撤回提示由后端放在响应 msg 中(无提示时 msg 为空,不弹窗)
|
||||
const res = await recallApply(id, reason.trim(), amountChanged);
|
||||
if (res && res.msg) {
|
||||
this.$message.warning(res.msg);
|
||||
}
|
||||
}
|
||||
this.$message.success('已提交出库撤回审批');
|
||||
this.recallSubmitOpen = false;
|
||||
this.recallSubmitting = false;
|
||||
this.refreshTables();
|
||||
})
|
||||
.catch(() => {
|
||||
} finally {
|
||||
this.$modal.closeLoading();
|
||||
this.recallSubmitting = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
// 撤回/作废发货记录
|
||||
handleRecallDelivery(row) {
|
||||
const id = row.id;
|
||||
this.$confirm('撤回 / 作废 该发货记录后无法恢复,操作不可逆转,确认无误后再执行!', "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
return recallDelivery(id);
|
||||
}).then(() => {
|
||||
this.$message.success("撤回 / 作废 成功");
|
||||
this.refreshTables();
|
||||
}).catch(() => {});
|
||||
},
|
||||
handleStatusChange(row, status) {
|
||||
const actionText = status === '3' ? '确认接收' : '退回';
|
||||
|
|
|
|||
|
|
@ -63,7 +63,11 @@
|
|||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="220">
|
||||
<template slot-scope="scope">
|
||||
<!-- <el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>-->
|
||||
<template v-if="scope.row.outerStatus === '3'">
|
||||
<!-- 已撤回/作废的出库单保留展示,仅可查看历史发货记录 -->
|
||||
<template v-if="scope.row.deliveryStatus === '3'">
|
||||
<el-button size="mini" type="text" icon="el-icon-document" @click="handleDeliveryRecord(scope.row)">发货记录</el-button>
|
||||
</template>
|
||||
<template v-else-if="scope.row.outerStatus === '3'">
|
||||
<el-button size="mini" type="text" icon="el-icon-truck" @click="handleUpdate(scope.row)" v-hasPermi="['inventory:outer:edit']">发货管理</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-document" @click="handleDeliveryRecord(scope.row)" v-if="scope.row.deliveryStatus === '1' || scope.row.deliveryStatus === '2'">发货记录</el-button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item v-if="form.type == '2'" label="预装系统类型" prop="preSystemType">
|
||||
<el-form-item v-if="form.type == '2' && form.level2Type != 6" label="预装系统类型" prop="preSystemType">
|
||||
<el-select v-model="form.preSystemType" placeholder="请选择预装系统类型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in dict.type.pre_system_type"
|
||||
|
|
@ -365,7 +365,8 @@ export default {
|
|||
required: true,
|
||||
trigger: "change",
|
||||
validator: (rule, value, callback) => {
|
||||
if (this.form.type == '2' && (value === null || value === undefined || value === '')) {
|
||||
// 产品类型为硬件且二级类型不是"硬件-配件"时,预装系统类型必填
|
||||
if (this.form.type == '2' && this.form.level2Type != 6 && (value === null || value === undefined || value === '')) {
|
||||
callback(new Error("预装系统类型不能为空"));
|
||||
} else {
|
||||
callback();
|
||||
|
|
@ -638,6 +639,10 @@ export default {
|
|||
this.form.localization = null;
|
||||
this.form.cpuBrand = null;
|
||||
this.form.cpuArchitecture = null;
|
||||
// 硬件-配件时隐藏预装系统类型,同时清空已填值
|
||||
if (isAccessory) {
|
||||
this.form.preSystemType = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,4 +71,4 @@ unis:
|
|||
enabled: false
|
||||
opportunity:
|
||||
integration:
|
||||
base-url: http://192.168.2.158:8080
|
||||
base-url: http://localhost:8080
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ public class OmsReceiptBillController extends BaseController {
|
|||
@Log(title = "申请收款", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/applyReceipt")
|
||||
@ResponseBody
|
||||
public AjaxResult applyReceipt(OmsReceiptBill omsReceiptBill, @RequestParam("file") MultipartFile file) {
|
||||
public AjaxResult applyReceipt(OmsReceiptBill omsReceiptBill, @RequestParam(value = "file", required = false) MultipartFile file) {
|
||||
try {
|
||||
omsReceiptBillService.applyReceipt(omsReceiptBill, file);
|
||||
} catch (IOException e) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import com.ruoyi.common.core.controller.BaseController;
|
|||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.sip.domain.InventoryDelivery;
|
||||
import com.ruoyi.sip.domain.OmsInventoryDeliveryDetail;
|
||||
|
|
@ -48,10 +47,7 @@ public class VueDeliveryController extends BaseController {
|
|||
@RequiresPermissions("inventory:delivery:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(InventoryDelivery inventoryDelivery) {
|
||||
// 默认只查已发货,前端显式指定状态时按指定状态查询
|
||||
if (StringUtils.isEmpty(inventoryDelivery.getDeliveryStatus())) {
|
||||
inventoryDelivery.setDeliveryStatus(InventoryDelivery.DeliveryStatusEnum.CONFIRM_DELIVERY.getCode());
|
||||
}
|
||||
// 撤回/作废后的记录需要保留可见:不再默认只查已发货,前端指定发货状态时按指定状态过滤
|
||||
if (!inventoryAuthService.authAll()) {
|
||||
List<String> productCodeList = inventoryAuthService.authProductCode();
|
||||
if (CollUtil.isEmpty(productCodeList)) {
|
||||
|
|
@ -122,15 +118,47 @@ public class VueDeliveryController extends BaseController {
|
|||
|
||||
/**
|
||||
* 提交出库撤回审批申请(跨天发货记录)
|
||||
* 存在审批通过的财务单据时,返回流程审批完成后将生成冲红单的提示给前端
|
||||
*/
|
||||
@RequiresPermissions("inventory:delivery:recall")
|
||||
@Log(title = "发货记录", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/recall/apply")
|
||||
public AjaxResult recallApply(@RequestBody Map<String, Object> params) {
|
||||
inventoryDeliveryService.applyRecall(Long.valueOf(params.get("id").toString()),
|
||||
String tip = inventoryDeliveryService.applyRecall(Long.valueOf(params.get("id").toString()),
|
||||
String.valueOf(params.get("reason")),
|
||||
String.valueOf(params.get("amountChanged")));
|
||||
return AjaxResult.success();
|
||||
return AjaxResult.success(tip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按产品 SN 判断关联财务单据的审批状态,并返回撤回/作废时的提示
|
||||
* 规则:
|
||||
* - 存在审批通过的财务单据 → 提示:存在审核通过的财务单据,将生成应收/应付冲红单(放行)
|
||||
* - 仅存在审批中的财务单据 → 提示:存在审批中的财务单据,请驳回审批后再次操作(拦截)
|
||||
* - 均无 → 情况C,提示将直接处理原应收单
|
||||
*
|
||||
* @param params 需包含 sns(产品 SN 列表)
|
||||
*/
|
||||
@RequiresPermissions("inventory:delivery:recall")
|
||||
@Log(title = "发货记录", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/recall/check-by-sns")
|
||||
public AjaxResult checkRecallBySns(@RequestBody Map<String, Object> params) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> sns = (List<String>) params.get("sns");
|
||||
String productCode = params.get("productCode") != null ? String.valueOf(params.get("productCode")) : null;
|
||||
String amountChanged = params.get("amountChanged") != null ? String.valueOf(params.get("amountChanged")) : null;
|
||||
return AjaxResult.success(inventoryDeliveryService.checkFinanceBillBySns(sns, productCode, amountChanged));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按发货记录 id 判断关联财务单据审批状态(便捷接口,内部自动取 SN + 产品编码 + 金额变化)
|
||||
* 前端「撤回/作废」按钮推荐直接调用此接口。
|
||||
*/
|
||||
@RequiresPermissions("inventory:delivery:recall")
|
||||
@Log(title = "发货记录", businessType = BusinessType.OTHER)
|
||||
@GetMapping("/recall/check-by-delivery/{deliveryId}")
|
||||
public AjaxResult checkRecallByDelivery(@PathVariable Long deliveryId) {
|
||||
return AjaxResult.success(inventoryDeliveryService.checkFinanceBillByDelivery(deliveryId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ public class VueInventoryOuterController extends BaseController
|
|||
{
|
||||
|
||||
inventoryOuter.setOuterStatusList(Arrays.asList(InventoryOuter.OuterStatusEnum.WAIT_RECEIVE.getCode(), InventoryOuter.OuterStatusEnum.RECEIVED.getCode()));
|
||||
inventoryOuter.setExcludeRecalled(true);
|
||||
// 撤回/作废后的出库单(delivery_status='3')保留可见,前端仅提供「发货记录」查看入口
|
||||
if (!inventoryAuthService.authAll()){
|
||||
List<String> productCodeList = inventoryAuthService.authProductCode();
|
||||
if (CollUtil.isEmpty(productCodeList)){
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import java.util.List;
|
|||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import com.ruoyi.sip.domain.dto.PayableGoodsDetailDto;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
|
@ -96,6 +97,8 @@ public class OmsReceivableBill extends BaseEntity
|
|||
|
||||
private List<OmsReceivableReceiptDetail> detailList;
|
||||
private List<OmsReceivableInvoiceDetail> invoiceDetailList;
|
||||
/** 商品明细(来源:出库单) */
|
||||
private List<PayableGoodsDetailDto> goodsDetailList;
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import lombok.Data;
|
|||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 应付单商品明细DTO(来源:入库单)
|
||||
* 单据商品明细DTO(应付单、应收单详情共用)
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.InventoryDelivery;
|
||||
import com.ruoyi.sip.dto.ApiDataQueryDto;
|
||||
|
|
@ -71,4 +72,42 @@ public interface InventoryDeliveryMapper
|
|||
|
||||
List<DeliveryApproveVo> selectDeliveryApproveList(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 按产品 SN 列表查询关联财务单据的审批状态(去重后的 approve_status:1=审批中,2=审批通过)
|
||||
*
|
||||
* @param sns 产品 SN 列表
|
||||
* @return 审批状态集合(无数据则返回空列表)
|
||||
*/
|
||||
List<Integer> selectApproveStatusBySn(@Param("sns") List<String> sns);
|
||||
|
||||
/**
|
||||
* 按入库单号(inner_code)查询关联应付单的付款/收票审批状态
|
||||
* 用于 INNER_PAY 厂商的 inner_code 应付单审批检查
|
||||
*
|
||||
* @param innerCodes 入库单号列表
|
||||
* @return 审批状态集合(1=审批中,2=审批通过;无数据则返回空列表)
|
||||
*/
|
||||
List<Integer> selectPayableApproveStatusByInnerCodes(@Param("innerCodes") List<String> innerCodes);
|
||||
|
||||
/**
|
||||
* 统计某出库单(outer_code)下、除指定发货记录外,仍处于有效(非撤回 delivery_status!='2')状态的发货记录数
|
||||
* 用于判断部分撤回(同一出库单还有其他发货在途)
|
||||
*/
|
||||
Long countOtherActiveDeliveryByOuterCode(@Param("outerCode") String outerCode, @Param("excludeId") Long excludeId);
|
||||
|
||||
/**
|
||||
* 统计某个入库单号(inner_code)对应的库存明细总数
|
||||
* 用于 INNER_PAY 厂商撤回时计算红冲比例(delivery_qty / inner_total_qty)
|
||||
*/
|
||||
Long countInventoryByInnerCode(@Param("innerCode") String innerCode);
|
||||
|
||||
/**
|
||||
* 查询某出库单(outer_code)下最近一次撤回的时间
|
||||
* 用于判断撤回后重新确认发货时是否需要重新生成应收单(撤回前生成的单据已被红冲/删除)
|
||||
*
|
||||
* @param outerCode 出库单号
|
||||
* @return 最近一次撤回时间;该出库单从未撤回过时返回 null
|
||||
*/
|
||||
Date selectLastRecallTimeByOuterCode(@Param("outerCode") String outerCode);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,15 @@ public interface InventoryInfoMapper
|
|||
|
||||
List<InventoryInfo> listByDeliveryId(Long id);
|
||||
|
||||
/**
|
||||
* 按出库单号查库存明细(通过发货记录+SN明细反查)
|
||||
* 撤回后 inventory_info.outer_code 会被清空,此方式仍可查到该出库单发过货的库存明细
|
||||
*
|
||||
* @param outerCode 出库单号
|
||||
* @return 库存明细列表
|
||||
*/
|
||||
List<InventoryInfo> listByOuterCodeViaDelivery(@Param("outerCode") String outerCode);
|
||||
|
||||
List<InventoryInfo> selectInventoryInfoByOrderCode(List<String> strings);
|
||||
|
||||
List<InventoryInfo> selectInventoryInfoByOuterCodeList(List<String> outerCodeList);
|
||||
|
|
|
|||
|
|
@ -31,4 +31,12 @@ public interface OmsPayablePaymentDetailMapper {
|
|||
void deleteByPaymentCode(String payableBillCode);
|
||||
|
||||
List<OmsPayablePaymentDetail> listByPaymentUpdateTime(Date startTime, Date endTime);
|
||||
|
||||
/**
|
||||
* 按应付单主键删除其付款明细(删除应付单时级联清理,避免孤儿数据)
|
||||
*
|
||||
* @param payableBillId 应付单主键
|
||||
* @return 结果
|
||||
*/
|
||||
int deleteByPayableBillId(Long payableBillId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,4 +78,12 @@ public interface OmsPayableTicketDetailMapper
|
|||
void updateBatch(List<OmsPayableTicketDetail> updateList);
|
||||
|
||||
void deleteByTicketBillCode(String ticketBillCode);
|
||||
|
||||
/**
|
||||
* 按应付单主键删除其收票明细(删除应付单时级联清理,避免孤儿数据)
|
||||
*
|
||||
* @param payableBillId 应付单主键
|
||||
* @return 结果
|
||||
*/
|
||||
int deleteByPayableBillId(Long payableBillId);
|
||||
}
|
||||
|
|
@ -60,4 +60,12 @@ public interface OmsPayableTicketPlanMapper
|
|||
public int deleteOmsPayableTicketPlanByIds(Long[] ids);
|
||||
|
||||
OmsPayableTicketPlan firstUnPayPlan(Long payableBillId);
|
||||
|
||||
/**
|
||||
* 按应付单主键删除其收票计划(删除应付单时级联清理,避免孤儿数据)
|
||||
*
|
||||
* @param payableBillId 应付单主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteByPayableBillId(Long payableBillId);
|
||||
}
|
||||
|
|
@ -82,4 +82,12 @@ public interface OmsReceivableInvoiceDetailMapper
|
|||
|
||||
void updateWriteOffIdBatch(List<OmsPayableTicketDetail> updateList);
|
||||
|
||||
/**
|
||||
* 按应收单主键删除其开票明细(删除应收单时级联清理,避免孤儿数据)
|
||||
*
|
||||
* @param receivableBillId 应收单主键
|
||||
* @return 结果
|
||||
*/
|
||||
int deleteByReceivableBillId(Long receivableBillId);
|
||||
|
||||
}
|
||||
|
|
@ -61,4 +61,12 @@ public interface OmsReceivableInvoicePlanMapper
|
|||
|
||||
OmsReceivableInvoicePlan firstUnPayPlan(Long receivableBillId);
|
||||
|
||||
/**
|
||||
* 按应收单主键删除其开票计划(删除应收单时级联清理,避免孤儿数据)
|
||||
*
|
||||
* @param receivableBillId 应收单主键
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteByReceivableBillId(Long receivableBillId);
|
||||
|
||||
}
|
||||
|
|
@ -75,4 +75,12 @@ public interface OmsReceivableReceiptDetailMapper
|
|||
void clearWriteOffByWriteOffId(List<Long> ids);
|
||||
|
||||
void deleteByBillCode(String receiptBillCode);
|
||||
|
||||
/**
|
||||
* 按应收单主键删除其收款明细(删除应收单时级联清理,避免孤儿数据)
|
||||
*
|
||||
* @param receivableBillId 应收单主键
|
||||
* @return 结果
|
||||
*/
|
||||
int deleteByReceivableBillId(Long receivableBillId);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.InventoryDelivery;
|
||||
import com.ruoyi.sip.dto.ApiDataQueryDto;
|
||||
import com.ruoyi.sip.dto.inventory.InventoryDeliveryDetailExcelDto;
|
||||
|
|
@ -76,17 +77,34 @@ public interface IInventoryDeliveryService
|
|||
* @param id 发货记录主键
|
||||
* @param reason 撤回原因
|
||||
* @param amountChanged 金额是否变化
|
||||
* @return 存在审批通过的财务单据时返回前端提示文案(流程审批完成后将生成冲红单),否则返回 null
|
||||
*/
|
||||
void applyRecall(Long id, String reason, String amountChanged);
|
||||
String applyRecall(Long id, String reason, String amountChanged);
|
||||
|
||||
/**
|
||||
* 按产品 SN 列表判断关联财务单据的审批状态,并返回撤回时所需的前端提示
|
||||
*
|
||||
* @param sns 产品 SN 列表
|
||||
* @param productCode 产品编码(用于判断是否为 P001 厂商;可为 null)
|
||||
* @param amountChanged 申请金额是否变化("是"/"否"/null)
|
||||
* @return map 字段:hasAny / approving / approved / blocked / tip
|
||||
*/
|
||||
Map<String, Object> checkFinanceBillBySns(List<String> sns, String productCode, String amountChanged);
|
||||
|
||||
/**
|
||||
* 按发货记录 id 判断关联财务单据审批状态(便捷方法:内部自动取 SN + 产品编码 + 金额变化)
|
||||
*/
|
||||
Map<String, Object> checkFinanceBillByDelivery(Long deliveryId);
|
||||
|
||||
/**
|
||||
* 撤回发货记录时的应收应付处理(发货撤回与订单撤单共用)
|
||||
* 应收/应付单据挂在出库单号上,故以出库单为颗粒度:调用方需保证该出库单下的发货记录已全部撤回,整单只调用一次
|
||||
* 统一按财务单据审批状态判断处理:情况A(审批中)拦截、情况B(审批通过)红冲、情况C(无单据)清理应收
|
||||
*
|
||||
* @param deliveryId 发货记录主键
|
||||
* @param deliveryId 发货记录主键(该出库单下任一条记录均可,用于 outer_code 路径取数)
|
||||
* @param outerCode 出库单号(用于关联应收/应付单)
|
||||
* @param skipReceivablePayable 是否跳过应收应付处理
|
||||
*/
|
||||
void handleRecallReceivablePayable(Long deliveryId, String outerCode, boolean skipReceivablePayable);
|
||||
void handleRecallReceivablePayable(Long deliveryId, String outerCode);
|
||||
|
||||
List<InventoryDeliveryDetailExcelDto> detailExport(InventoryDelivery inventoryDelivery);
|
||||
|
||||
|
|
|
|||
|
|
@ -359,7 +359,10 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
|
|||
InventoryOuter.DeliveryStatusEnum.REBACK.getCode());
|
||||
}
|
||||
}
|
||||
Map<String, Long> outerSumMap = inventoryOuters.stream().collect(Collectors.toMap(InventoryOuter::getProductCode, InventoryOuter::getQuantity, Long::sum));
|
||||
// 排除订单撤单前已是「已撤回」的出库单:它们在之前撤回时已回补过实时库存,此处再次累加会造成库存虚增
|
||||
Map<String, Long> outerSumMap = inventoryOuters.stream()
|
||||
.filter(outer -> !InventoryOuter.DeliveryStatusEnum.REBACK.getCode().equals(outer.getDeliveryStatus()))
|
||||
.collect(Collectors.toMap(InventoryOuter::getProductCode, InventoryOuter::getQuantity, Long::sum));
|
||||
|
||||
Map<String, Long> deliveryMap = inventoryDeliveries.stream().collect(Collectors.toMap(InventoryDelivery::getProductCode, InventoryDelivery::getQuantity, Long::sum));
|
||||
Map<String, ProductInfo> updateMap = new HashMap<>();
|
||||
|
|
@ -394,54 +397,19 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
|
|||
if (CollUtil.isNotEmpty(updateMap.values())) {
|
||||
productInfoService.updateCount(new ArrayList<>(updateMap.values()));
|
||||
}
|
||||
// 撤回时处理应收应付:与发货撤回一致,按发货记录逐个判断并处理(金额是否变化为“是”则整单跳过;制造商为P001的该发货记录跳过)
|
||||
boolean orderAmountChanged = isOrderRecallAmountChanged(projectOrderInfo.getOrderCode());
|
||||
// 撤回时处理应收应付:与发货撤回一致,按「出库单」为颗粒度处理
|
||||
// 应收/应付单据挂在出库单号上,故按出库单去重,每个出库单只处理一次,避免同一出库单多次发货时重复红冲
|
||||
// 统一按财务单据审批状态判断(情况A/B/C),不再考虑 P001 厂商与金额是否变化
|
||||
Map<String, InventoryDelivery> outerDeliveryMap = new LinkedHashMap<>();
|
||||
for (InventoryDelivery inventoryDelivery : inventoryDeliveryList) {
|
||||
boolean skipReceivablePayable = isManufacturerP001(inventoryDelivery.getProductCode()) || orderAmountChanged;
|
||||
outerDeliveryMap.putIfAbsent(inventoryDelivery.getOuterCode(), inventoryDelivery);
|
||||
}
|
||||
for (InventoryDelivery inventoryDelivery : outerDeliveryMap.values()) {
|
||||
inventoryDeliveryService.handleRecallReceivablePayable(
|
||||
inventoryDelivery.getId(), inventoryDelivery.getOuterCode(), skipReceivablePayable);
|
||||
inventoryDelivery.getId(), inventoryDelivery.getOuterCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断订单撤单审批中记录的“金额是否变化”是否为“是”
|
||||
* 金额变化存储于待办/已办记录的 extendField3
|
||||
*
|
||||
* @param orderCode 订单号(撤单流程 businessKey)
|
||||
* @return 金额是否变化为“是”则返回 true
|
||||
*/
|
||||
private boolean isOrderRecallAmountChanged(String orderCode) {
|
||||
Todo query = new Todo()
|
||||
.setProcessKey(processConfig.getDefinition().getOrderReback())
|
||||
.setBusinessKey(orderCode);
|
||||
// 先查已办,再查待办
|
||||
List<Todo> todos = todoMapper.selectTodoCompletedList(query);
|
||||
if (CollUtil.isEmpty(todos)) {
|
||||
Todo todo = todoMapper.selectTodo(query);
|
||||
if (todo != null) {
|
||||
todos = Collections.singletonList(todo);
|
||||
}
|
||||
}
|
||||
return CollUtil.isNotEmpty(todos) && "是".equals(todos.get(0).getExtendField3());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断产品制造商编码是否为 P001
|
||||
*
|
||||
* @param productCode 产品编码
|
||||
* @return 制造商编码为 P001 则返回 true
|
||||
*/
|
||||
private boolean isManufacturerP001(String productCode) {
|
||||
if (StringUtils.isEmpty(productCode)) {
|
||||
return false;
|
||||
}
|
||||
List<ProductInfo> products = productInfoService.selectProductInfoByCodeList(Collections.singletonList(productCode));
|
||||
if (CollUtil.isEmpty(products)) {
|
||||
return false;
|
||||
}
|
||||
return "P001".equals(products.get(0).getVendorCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void applyRecall(Long id, String reason, String amountChanged) {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ import javax.annotation.Resource;
|
|||
@Service
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, TodoCommonTemplate {
|
||||
/** 情况 A(存在审批中的财务单据,且无审批通过单据)的统一拦截提示 */
|
||||
private static final String APPROVING_BILL_TIP = "存在审批中的财务单据(收款单/开票单/付款单/收票单),请驳回审批后再次操作";
|
||||
|
||||
@Autowired
|
||||
private InventoryDeliveryMapper inventoryDeliveryMapper;
|
||||
@Autowired
|
||||
|
|
@ -80,6 +83,14 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
@Autowired
|
||||
private IOmsReceivableBillService billService;
|
||||
@Autowired
|
||||
private IOmsReceivableReceiptDetailService receivableReceiptDetailService;
|
||||
@Autowired
|
||||
private IOmsReceivableInvoiceDetailService receivableInvoiceDetailService;
|
||||
@Autowired
|
||||
private IOmsPayablePaymentDetailService payablePaymentDetailService;
|
||||
@Autowired
|
||||
private IOmsPayableTicketDetailService payableTicketDetailService;
|
||||
@Autowired
|
||||
private IOmsPurchaseOrderService omsPurchaseOrderService;
|
||||
@Resource
|
||||
private OmsPurchaseOrderMapMapper omsPurchaseOrderMapMapper;
|
||||
|
|
@ -213,6 +224,38 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
inventoryDelivery.setUpdateBy(currentUserId);
|
||||
inventoryDelivery.setUpdateTime(nowDate);
|
||||
inventoryDelivery.setDeliveryStatus(InventoryDelivery.DeliveryStatusEnum.WAIT_DELIVERY.getCode());
|
||||
//出库单已撤回/作废时不允许再发货,需回到订单重新出库(生成新的出库单)
|
||||
InventoryOuter recalledOuter = inventoryOuterMapper.selectInventoryOuterByCode(inventoryDelivery.getOuterCode());
|
||||
if (recalledOuter != null && InventoryOuter.DeliveryStatusEnum.REBACK.getCode().equals(recalledOuter.getDeliveryStatus())) {
|
||||
throw new ServiceException("该出库单已撤回/作废,请回到订单重新出库后再发货");
|
||||
}
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 兜底校验:发货必须一次发满该出库单明细的剩余应发数量,不允许分多次发货
|
||||
// 前端已限制,此处防止绕过前端直接调用接口;
|
||||
// 取不到出库单明细时不做拦截,避免影响历史/异常调用
|
||||
// ══════════════════════════════════════════════════════
|
||||
if (inventoryDelivery.getQuantity() != null && StringUtils.isNotEmpty(inventoryDelivery.getOuterCode())) {
|
||||
InventoryOuterDetail detailQuery = new InventoryOuterDetail();
|
||||
detailQuery.setOuterCode(inventoryDelivery.getOuterCode());
|
||||
detailQuery.setWarehouseId(inventoryDelivery.getWarehouseId());
|
||||
List<InventoryOuterDetail> outerDetails = inventoryOuterDetailMapper.selectInventoryOuterDetailList(detailQuery);
|
||||
if (CollUtil.isNotEmpty(outerDetails)) {
|
||||
long shouldQuantity = outerDetails.stream()
|
||||
.mapToLong(item -> item.getQuantity() == null ? 0L : item.getQuantity()).sum();
|
||||
// 已占用数量:同一出库单、同一仓库下未撤回(待发货/已发货)的发货记录数量合计
|
||||
InventoryDelivery occupiedQuery = new InventoryDelivery();
|
||||
occupiedQuery.setOuterCode(inventoryDelivery.getOuterCode());
|
||||
occupiedQuery.setWarehouseId(inventoryDelivery.getWarehouseId());
|
||||
long occupiedQuantity = inventoryDeliveryMapper.selectInventoryDeliveryList(occupiedQuery).stream()
|
||||
.filter(item -> !InventoryDelivery.DeliveryStatusEnum.RECALL_DELIVERY.getCode().equals(item.getDeliveryStatus()))
|
||||
.mapToLong(item -> item.getQuantity() == null ? 0L : item.getQuantity()).sum();
|
||||
long remainQuantity = shouldQuantity - occupiedQuantity;
|
||||
if (inventoryDelivery.getQuantity() != remainQuantity) {
|
||||
throw new ServiceException(String.format("发货必须一次发满剩余应发数量(应发 %d 台,已发 %d 台,剩余 %d 台,本次 %d 台)",
|
||||
shouldQuantity, occupiedQuantity, remainQuantity, inventoryDelivery.getQuantity()));
|
||||
}
|
||||
}
|
||||
}
|
||||
//修改数据的时候同步修改出库价
|
||||
BigDecimal bigDecimal = inventoryOuterMapper.selectOutPriceByCode(inventoryDelivery.getOuterCode());
|
||||
|
||||
|
|
@ -405,12 +448,22 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
InventoryDelivery inventoryDelivery1 = selectInventoryDeliveryById(inventoryDelivery.getId());
|
||||
BigDecimal allPrice = price.multiply(new BigDecimal(inventoryDelivery1.getQuantity().toString()));
|
||||
//防重:同一出库单+产品且金额一致的应收单已生成过则不重复生成(避免重复确认/并发导致重复)
|
||||
//撤回后重新发货:撤回时原应收单已通过红冲(情况B)或删除(情况C)处理完毕,
|
||||
// 故仅当同金额的应收单是在最近一次撤回之后生成时才算重复,否则需要重新生成
|
||||
OmsReceivableBill receivableQuery = new OmsReceivableBill();
|
||||
receivableQuery.setInventoryCode(inventoryDelivery.getOuterCode());
|
||||
receivableQuery.setProductCode(inventoryOuter.getProductCode());
|
||||
List<OmsReceivableBill> existReceivableBills = billService.selectOmsReceivableBillList(receivableQuery);
|
||||
Date lastRecall = inventoryDeliveryMapper.selectLastRecallTimeByOuterCode(inventoryDelivery.getOuterCode());
|
||||
// 仅当该出库单下已无其他在途发货记录(整单撤回完成、财务已按整单处理过)时,撤回时间才生效;
|
||||
// 仍有在途记录说明之前的撤回未处理财务单据,此时不得重新生成,避免重复计应收
|
||||
Long otherActive = inventoryDeliveryMapper.countOtherActiveDeliveryByOuterCode(
|
||||
inventoryDelivery.getOuterCode(), inventoryDelivery.getId());
|
||||
final Date lastRecallTime = (otherActive != null && otherActive > 0) ? null : lastRecall;
|
||||
boolean receivableDuplicated = existReceivableBills.stream()
|
||||
.anyMatch(item -> item.getTotalPriceWithTax() != null && item.getTotalPriceWithTax().compareTo(allPrice) == 0);
|
||||
.filter(item -> item.getTotalPriceWithTax() != null && item.getTotalPriceWithTax().compareTo(allPrice) == 0)
|
||||
.anyMatch(item -> lastRecallTime == null
|
||||
|| (item.getCreateTime() != null && !item.getCreateTime().before(lastRecallTime)));
|
||||
if (!receivableDuplicated) {
|
||||
receivableBill.setTotalPriceWithTax(allPrice);
|
||||
BigDecimal defaultTaxRate = projectProductInfo.getTaxRate() == null ? new BigDecimal(defaultTax) : projectProductInfo.getTaxRate().divide(new BigDecimal("100"));
|
||||
|
|
@ -578,7 +631,7 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
* @param amountChanged 金额是否变化
|
||||
*/
|
||||
@Override
|
||||
public void applyRecall(Long id, String reason, String amountChanged) {
|
||||
public String applyRecall(Long id, String reason, String amountChanged) {
|
||||
InventoryDelivery inventoryDelivery = inventoryDeliveryMapper.selectInventoryDeliveryById(id);
|
||||
if (inventoryDelivery == null) {
|
||||
throw new ServiceException("发货记录不存在");
|
||||
|
|
@ -593,6 +646,23 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
if (processInstance != null) {
|
||||
throw new ServiceException("该发货记录已提交撤回审批,请勿重复提交");
|
||||
}
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 情况 A 预检查:阻止有审批中财务单据的撤回申请
|
||||
// 仅当【不存在任何审批通过(approveStatus=2)的财务单据】且【存在审批中(approveStatus=1)
|
||||
// 的 收款单/开票单/付款单/收票单】时,才禁止发起撤回;用户需先驳回相关审批流程,
|
||||
// 之后财务单据不再关联 → 进入情况 C
|
||||
// 只要存在审批通过(approveStatus=2)的财务单据 → 按情况 B 处理:允许发起撤回,
|
||||
// 审批通过后由 handleRecallReceivablePayable 生成负金额红冲单
|
||||
// ══════════════════════════════════════════════════════
|
||||
Map<String, Boolean> billStatus = checkFinanceBillApproveStatus(id);
|
||||
if (billStatus.get("approving") && !billStatus.get("approved")) {
|
||||
throw new ServiceException(APPROVING_BILL_TIP);
|
||||
}
|
||||
// 存在审批通过的财务单据(情况 B):放行发起撤回,并提示前端审批完成后将生成冲红单
|
||||
String recallApplyTip = null;
|
||||
if (Boolean.TRUE.equals(billStatus.get("approved"))) {
|
||||
recallApplyTip = "该发货记录存在审批通过的财务单据,流程审批完成后将自动生成冲红单";
|
||||
}
|
||||
// 标记撤回审批中
|
||||
inventoryDelivery.setApproveStatus(ApproveStatusEnum.WAIT_APPROVE.getCode());
|
||||
inventoryDeliveryMapper.updateInventoryDelivery(inventoryDelivery);
|
||||
|
|
@ -607,6 +677,84 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
put("extendField3", amountChanged);
|
||||
put("deliveryId", id);
|
||||
}}, processConfig.getDefinition().getOuterReback());
|
||||
return recallApplyTip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按产品 SN 列表判断关联财务单据的审批状态并返回前端撤回提示
|
||||
* 规则:
|
||||
* - 存在审批通过(approveStatus=2)的财务单据 → 提示"存在审核通过的财务单据,将生成应收/应付冲红单"(放行)
|
||||
* - 仅存在审批中(approveStatus=1)的财务单据 → 提示"请驳回审批后再次操作"(拦截)
|
||||
* - 都没有 → 情况C,提示将直接处理原应收单
|
||||
*
|
||||
* @param sns 产品 SN 列表
|
||||
* @param productCode 产品编码(保留参数,不再参与判断)
|
||||
* @param amountChanged 申请金额是否变化(保留参数,不再参与判断)
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> checkFinanceBillBySns(List<String> sns, String productCode, String amountChanged) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("hasAny", false);
|
||||
result.put("approving", false);
|
||||
result.put("approved", false);
|
||||
result.put("blocked", false);
|
||||
result.put("tip", null);
|
||||
|
||||
if (CollUtil.isEmpty(sns)) {
|
||||
return result;
|
||||
}
|
||||
List<Integer> statuses = inventoryDeliveryMapper.selectApproveStatusBySn(sns);
|
||||
boolean approving = statuses.contains(1);
|
||||
boolean approved = statuses.contains(2);
|
||||
result.put("hasAny", approving || approved);
|
||||
result.put("approving", approving);
|
||||
result.put("approved", approved);
|
||||
|
||||
if (approved) {
|
||||
// 情况 B:存在审批通过的财务单据 → 生成红冲单
|
||||
result.put("tip", "存在审核通过的财务单据,将生成应收/应付冲红单");
|
||||
} else if (approving) {
|
||||
result.put("blocked", true);
|
||||
result.put("tip", APPROVING_BILL_TIP);
|
||||
} else {
|
||||
result.put("tip", "未关联审批中的财务单据,撤回后将直接处理原应收/应付单");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按发货记录 id 判断关联财务单据审批状态(便捷入口,供前端撤回/作废按钮直接调用)
|
||||
* 核心判断委托给 checkFinanceBillApproveStatus(三路并集),并组装前端提示 tip
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> checkFinanceBillByDelivery(Long deliveryId) {
|
||||
InventoryDelivery delivery = inventoryDeliveryMapper.selectInventoryDeliveryById(deliveryId);
|
||||
if (delivery == null) {
|
||||
throw new ServiceException("发货记录不存在");
|
||||
}
|
||||
// 复用统一入口(三路并集)
|
||||
Map<String, Boolean> billStatus = checkFinanceBillApproveStatus(deliveryId);
|
||||
boolean hasAny = Boolean.TRUE.equals(billStatus.get("hasAny"));
|
||||
boolean approving = Boolean.TRUE.equals(billStatus.get("approving"));
|
||||
boolean approved = Boolean.TRUE.equals(billStatus.get("approved"));
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("hasAny", hasAny);
|
||||
result.put("approving", approving);
|
||||
result.put("approved", approved);
|
||||
|
||||
// 组装前端提示 tip
|
||||
if (approved) {
|
||||
// 情况 B:存在审批通过的财务单据 → 生成红冲单
|
||||
result.put("tip", "存在审核通过的财务单据,将生成应收/应付冲红单");
|
||||
} else if (approving) {
|
||||
result.put("blocked", true);
|
||||
result.put("tip", APPROVING_BILL_TIP);
|
||||
} else {
|
||||
// 情况 C:无关联财务单据 → 撤回时将清理原应收单
|
||||
result.put("tip", "未关联审批中的财务单据,撤回后将直接处理原应收单");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -669,6 +817,7 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean multiInstanceApproveCallback(String activityName, ProcessInstance processInstance) {
|
||||
if (processConfig.getDefinition().getOuterReback().equals(processInstance.getProcessDefinitionKey())
|
||||
&& "领导".equals(activityName)) {
|
||||
|
|
@ -692,26 +841,27 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
Todo firstCompleted = todoMapper.selectFirstCompletedByProcessInstanceId(processInstance.getId());
|
||||
String updateBy = firstCompleted != null && StringUtils.isNotBlank(firstCompleted.getApproveUser())
|
||||
? firstCompleted.getApproveUser() : ShiroUtils.getUserId().toString();
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 情况 A 兜底二次检查:防竞态条件
|
||||
// 仅当【不存在任何审批通过(approveStatus=2)的财务单据】且【存在审批中
|
||||
// (approveStatus=1)的财务单据】时判定撤回失败返回;存在审批通过的财务单据
|
||||
// 则放行,由 handleRecallReceivablePayable 按情况 B 生成负金额红冲单
|
||||
// ══════════════════════════════════════════════════════
|
||||
Map<String, Boolean> billStatus = checkFinanceBillApproveStatus(delivery.getId());
|
||||
if (billStatus.get("approving") && !billStatus.get("approved")) {
|
||||
log.error("撤回执行失败,存在审批中的财务单据。deliveryId={}, outerCode={}, 请驳回审批后再次操作",
|
||||
delivery.getId(), delivery.getOuterCode());
|
||||
// 标记撤回审批为驳回状态,供用户查看
|
||||
InventoryDelivery updateFail = new InventoryDelivery();
|
||||
updateFail.setId(delivery.getId());
|
||||
updateFail.setApproveStatus(ApproveStatusEnum.APPROVE_REJECT.getCode());
|
||||
updateFail.setRemark("撤回执行失败:" + APPROVING_BILL_TIP);
|
||||
inventoryDeliveryMapper.updateInventoryDelivery(updateFail);
|
||||
return TodoCommonTemplate.super.multiInstanceApproveCallback(activityName, processInstance);
|
||||
}
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 出库单作废(标记已撤回、回补实时库存、回退订单出库状态)由 recall() 在整单撤回完成时统一处理
|
||||
recall(delivery.getId(), updateBy);
|
||||
// 审批通过:出库单标记为已撤回(按 outerCode 匹配)
|
||||
if (StringUtils.isNotBlank(delivery.getOuterCode())) {
|
||||
inventoryOuterMapper.updateDeliveryStatusByOuterCode(delivery.getOuterCode(),
|
||||
InventoryOuter.DeliveryStatusEnum.REBACK.getCode());
|
||||
// 参照撤销出库单业务(不物理删除出库单及其明细):
|
||||
// 1. 回补实时库存
|
||||
InventoryOuter outer = inventoryOuterMapper.selectInventoryOuterByCode(delivery.getOuterCode());
|
||||
if (outer != null) {
|
||||
productInfoService.updateAvailableCount(outer.getQuantity(), outer.getProductCode());
|
||||
ProjectOrderInfo projectOrderInfo = new ProjectOrderInfo();
|
||||
projectOrderInfo.setOrderCode(outer.getOrderCode());
|
||||
projectOrderInfo.setOuterStatus(ProjectOrderInfo.OuterStatusEnum.PART_OUTER.getCode());
|
||||
// 2. 该订单已无其他有效出库单(countByOrderCode 已过滤已撤回的 delivery_status=3)时,将项目订单出库状态置为未出库
|
||||
if (inventoryOuterMapper.countByOrderCode(outer.getOrderCode()) <= 0) {
|
||||
projectOrderInfo.setOuterStatus(ProjectOrderInfo.OuterStatusEnum.NOT_OUTER.getCode());
|
||||
}
|
||||
projectOrderInfoService.updateProjectOrderInfoByCode(projectOrderInfo);
|
||||
}
|
||||
}
|
||||
// 审批通过:标记撤回审批完成
|
||||
InventoryDelivery update = new InventoryDelivery();
|
||||
update.setId(delivery.getId());
|
||||
|
|
@ -755,6 +905,7 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void recall(Long id) {
|
||||
recall(id, ShiroUtils.getUserId().toString());
|
||||
}
|
||||
|
|
@ -765,11 +916,19 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
* @param id 发货记录主键
|
||||
* @param updateBy 操作人用户id
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void recall(Long id, String updateBy) {
|
||||
InventoryDelivery inventoryDelivery = inventoryDeliveryMapper.selectInventoryDeliveryById(id);
|
||||
// 制造商编码为 P001 的产品,或 撤回审批中“金额是否变化”为“是”时,跳过应收/应付相关的判断与处理
|
||||
boolean skipReceivablePayable = isManufacturerP001(inventoryDelivery.getProductCode())
|
||||
|| isRecallAmountChanged(id);
|
||||
if (inventoryDelivery == null) {
|
||||
throw new ServiceException("发货记录不存在");
|
||||
}
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 幂等保护:仅「已发货」状态可撤回。
|
||||
// 防止对已撤回(delivery_status='2')的记录重复操作
|
||||
// ══════════════════════════════════════════════════════
|
||||
if (!InventoryDelivery.DeliveryStatusEnum.CONFIRM_DELIVERY.getCode().equals(inventoryDelivery.getDeliveryStatus())) {
|
||||
throw new ServiceException("该发货记录当前不是「已发货」状态,无法再次撤回/作废");
|
||||
}
|
||||
ProjectOrderInfo projectOrderInfo = projectOrderInfoService.selectProjectOrderInfoByOrderCode(inventoryDelivery.getOrderCode());
|
||||
deleteInventoryOuterById(id, false, projectOrderInfo.getOrderCode());
|
||||
List<ProjectProductInfo> projectProductInfos = projectProductInfoService.listDeliveryProductByOrderCode(Collections.singletonList(inventoryDelivery.getOrderCode()));
|
||||
|
|
@ -793,37 +952,199 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
inventoryOuterMapper.updateInventoryOuter(updateDto);
|
||||
}
|
||||
long allSum = deliveryList.stream().mapToLong(InventoryDelivery::getQuantity).sum();
|
||||
//修改订单的发货状态
|
||||
//修改订单的发货状态:全部撤回(已确认发货数量为0)时回到「未发货」,保证订单可以重新出库
|
||||
ProjectOrderInfo updateOrder = new ProjectOrderInfo();
|
||||
updateOrder.setOrderCode(inventoryDelivery.getOrderCode());
|
||||
updateOrder.setUpdateTime(new Date());
|
||||
updateOrder.setUpdateBy(updateBy);
|
||||
if (allSum == 0) {
|
||||
updateOrder.setDeliveryStatus(ProjectOrderInfo.DeliveryStatusEnum.NOT_DELIVERY.getCode());
|
||||
} else {
|
||||
updateOrder.setDeliveryStatus(sum == allSum ? ProjectOrderInfo.DeliveryStatusEnum.ALL_DELIVERY.getCode() : ProjectOrderInfo.DeliveryStatusEnum.PART_DELIVERY.getCode());
|
||||
}
|
||||
updateOrder.setOperationVersion(projectOrderInfo.getOperationVersion() + 1);
|
||||
projectOrderInfoService.updateProjectOrderInfoByCode(updateOrder);
|
||||
//修改累计发货数量
|
||||
productInfoService.updateCumulativeCount(-inventoryDelivery.getQuantity(), inventoryDelivery.getProductCode());
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 应收/应付单据挂在出库单号(outer_code)上,故以「出库单」为颗粒度、整单只处理一次:
|
||||
// 出库单下仍有在途(delivery_status='1')发货记录时,本次仅撤回当前记录,不处理财务单据;
|
||||
// 最后一条在途记录撤回(整单撤回完成)时才按整张出库单红冲/删除。
|
||||
// 避免一个出库单多次发货时重复红冲。
|
||||
// ══════════════════════════════════════════════════════
|
||||
Long otherActive = inventoryDeliveryMapper.countOtherActiveDeliveryByOuterCode(inventoryDelivery.getOuterCode(), id);
|
||||
if (otherActive != null && otherActive > 0) {
|
||||
log.info("出库单 {} 仍有 {} 条在途发货记录,本次仅撤回当前记录,应收/应付待整单撤回完成后统一处理",
|
||||
inventoryDelivery.getOuterCode(), otherActive);
|
||||
return;
|
||||
}
|
||||
// 整单撤回完成:存在审批中(approveStatus=1)的财务单据且无审批通过(approveStatus=2)单据时拦截,
|
||||
// 提示用户先驳回审批后再次操作(防止前端校验异常降级或直接调接口绕过)
|
||||
Map<String, Boolean> billStatus = checkFinanceBillApproveStatus(id);
|
||||
if (billStatus.get("approving") && !billStatus.get("approved")) {
|
||||
throw new ServiceException(APPROVING_BILL_TIP);
|
||||
}
|
||||
//撤回时处理当前出库单对应的应付、应收数据(仅处理软件和硬件类型:1、2)
|
||||
handleRecallReceivablePayable(id, inventoryDelivery.getOuterCode(), skipReceivablePayable);
|
||||
|
||||
handleRecallReceivablePayable(id, inventoryDelivery.getOuterCode());
|
||||
//整单撤回完成:出库单作废(标记为已撤回、不物理删除),回补实时库存并回退订单出库状态,
|
||||
// 使订单可以重新出库(再次生成新的出库单)
|
||||
markOuterRecalled(inventoryDelivery.getOuterCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回发货记录时的应收应付处理(发货撤回与订单撤单共用)
|
||||
* 存在应收/应付财务单据(付款/收票/收款/开票单,未完成或已完成)时生成红冲负金额单,否则删除原应收应付
|
||||
* 整张出库单撤回完成后的收尾处理:
|
||||
* 1. 出库单标记为「已撤回」(不物理删除,保证出库单与发货记录可追溯);
|
||||
* 2. 回补出库单维度的实时库存(生成出库单时按整单数量扣减);
|
||||
* 3. 回退项目订单出库状态,使订单可以重新出库。
|
||||
*
|
||||
* @param deliveryId 发货记录主键
|
||||
* @param outerCode 出库单号(用于关联应收/应付单)
|
||||
* @param skipReceivablePayable 是否跳过应收应付处理
|
||||
* @param outerCode 出库单号
|
||||
*/
|
||||
@Override
|
||||
public void handleRecallReceivablePayable(Long deliveryId, String outerCode, boolean skipReceivablePayable) {
|
||||
if (skipReceivablePayable) {
|
||||
private void markOuterRecalled(String outerCode) {
|
||||
InventoryOuter outer = inventoryOuterMapper.selectInventoryOuterByCode(outerCode);
|
||||
if (outer == null) {
|
||||
return;
|
||||
}
|
||||
// 是否存在应收/应付财务单据(付款/收票/收款/开票单):无则删除原应收应付,有则改为生成红冲负金额单
|
||||
List<DeliveryApproveVo> deliveryApproveVoList = inventoryDeliveryMapper.selectDeliveryApproveList(deliveryId);
|
||||
boolean hasFinanceBill = CollUtil.isNotEmpty(deliveryApproveVoList);
|
||||
inventoryOuterMapper.updateDeliveryStatusByOuterCode(outerCode, InventoryOuter.DeliveryStatusEnum.REBACK.getCode());
|
||||
productInfoService.updateAvailableCount(outer.getQuantity(), outer.getProductCode());
|
||||
ProjectOrderInfo projectOrderInfo = new ProjectOrderInfo();
|
||||
projectOrderInfo.setOrderCode(outer.getOrderCode());
|
||||
// 该订单已无其他有效出库单(countByOrderCode 已过滤已撤回的出库单)时置为未出库,否则为部分出库
|
||||
projectOrderInfo.setOuterStatus(inventoryOuterMapper.countByOrderCode(outer.getOrderCode()) <= 0
|
||||
? ProjectOrderInfo.OuterStatusEnum.NOT_OUTER.getCode()
|
||||
: ProjectOrderInfo.OuterStatusEnum.PART_OUTER.getCode());
|
||||
projectOrderInfoService.updateProjectOrderInfoByCode(projectOrderInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询发货记录关联的财务单据审批状态(统一入口)
|
||||
* 优先按产品 SN 精确匹配;若该发货记录无 SN 登记则回退 outer_code 粗粒度匹配,
|
||||
* 保证不会静默放过。被 applyRecall / multiInstanceApproveCallback / handleRecallReceivablePayable 三处调用。
|
||||
*
|
||||
* 用于在发货撤回场景下区分三种情况:
|
||||
* A. 存在审批中(approveStatus=1)的财务单据 → 禁止撤回(已在上层拦截)
|
||||
* B. 存在审批通过(approveStatus=2)的财务单据 → 生成负金额红冲单
|
||||
* C. 无任何关联财务单据 → 直接删除应收/应付单
|
||||
*
|
||||
* @param deliveryId 发货记录主键
|
||||
* @return Map:
|
||||
* - "hasAny" → 是否存在任意关联的财务单据(approving 或 approved)
|
||||
* - "approving" → 是否存在审批中(approveStatus=1)的财务单据
|
||||
* - "approved" → 是否存在审批通过(approveStatus=2)的财务单据
|
||||
*/
|
||||
private Map<String, Boolean> checkFinanceBillApproveStatus(Long deliveryId) {
|
||||
return checkFinanceBillApproveStatus(deliveryId, inventoryInfoService.listByDeliveryId(deliveryId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询财务单据审批状态(指定参与判断的库存明细)
|
||||
* 单条发货撤回时传入该发货记录的 SN 明细;整张出库单撤回时传入该出库单下全部发货记录的 SN 明细
|
||||
*
|
||||
* @param deliveryId 发货记录主键(仅用于 outer_code 路径取数,该出库单下任一条记录均可)
|
||||
* @param infos 参与判断的库存明细(用于 SN 与 inner_code 两条路径)
|
||||
*/
|
||||
private Map<String, Boolean> checkFinanceBillApproveStatus(Long deliveryId, List<InventoryInfo> infos) {
|
||||
Map<String, Boolean> result = new HashMap<>();
|
||||
result.put("hasAny", false);
|
||||
result.put("approving", false);
|
||||
result.put("approved", false);
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 财务单据(应收/应付)是以 outer_code(inventory_code) 为粒度关联的,不含 SN 列。
|
||||
// 因此同时计算两条路径并取并集(OR):
|
||||
// 路径一 SN:inventory_info.product_sn → outer_code → 单据(依赖 inventory_info.outer_code 未被清空)
|
||||
// 路径二 outer:oms_inventory_delivery.outer_code → 单据(始终可靠)
|
||||
// 关键:recall() 会先执行 deleteInventoryOuterById() → clearOutInfo 把 inventory_info.outer_code 置空,
|
||||
// 随后才调用本方法。此时路径一会漏判;并集确保只要该出库单存在审批中的财务单据就一定能识别,
|
||||
// 不受执行顺序影响,也不会静默落入“情况C”导致误删/漏红冲。
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
// 路径二(始终可靠):delivery.id → outer_code → 四段 join
|
||||
List<DeliveryApproveVo> outerList = inventoryDeliveryMapper.selectDeliveryApproveList(deliveryId);
|
||||
boolean approving = outerList.stream().anyMatch(v -> Integer.valueOf(1).equals(v.getApproveStatus()));
|
||||
boolean approved = outerList.stream().anyMatch(v -> Integer.valueOf(2).equals(v.getApproveStatus()));
|
||||
|
||||
// 路径一(SN 精确):取参与判断的库存明细的 SN 列表补充判断(与路径二取并集,只可能放大命中、不会漏)
|
||||
List<String> sns = CollUtil.isEmpty(infos) ? Collections.emptyList() :
|
||||
infos.stream().map(InventoryInfo::getProductSn).filter(StringUtils::isNotEmpty).distinct().collect(Collectors.toList());
|
||||
if (CollUtil.isNotEmpty(sns)) {
|
||||
List<Integer> statuses = inventoryDeliveryMapper.selectApproveStatusBySn(sns);
|
||||
approving = approving || statuses.contains(1);
|
||||
approved = approved || statuses.contains(2);
|
||||
}
|
||||
|
||||
// 路径三(inner_code 回退):INNER_PAY 厂商的应付单按 inner_code 生成,
|
||||
// 上述两条路径查不到,需通过 inventory_info.inner_code 补充检查应付单审批状态
|
||||
if (CollUtil.isNotEmpty(infos)) {
|
||||
List<String> innerCodes = infos.stream()
|
||||
.map(InventoryInfo::getInnerCode)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (CollUtil.isNotEmpty(innerCodes)) {
|
||||
List<Integer> innerStatuses = inventoryDeliveryMapper.selectPayableApproveStatusByInnerCodes(innerCodes);
|
||||
approving = approving || innerStatuses.contains(1);
|
||||
approved = approved || innerStatuses.contains(2);
|
||||
}
|
||||
}
|
||||
|
||||
result.put("hasAny", approving || approved);
|
||||
result.put("approving", approving);
|
||||
result.put("approved", approved);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回发货记录时的应收应付处理(发货撤回与订单撤单共用,是应收/应付处理的统一入口)
|
||||
* 调用前需保证「该出库单下的发货记录已全部撤回」,即整张出库单撤回完成时调用一次
|
||||
*
|
||||
* 统一按财务单据审批状态判断处理(不再考虑 P001 厂商、金额是否变化):
|
||||
* 情况 A - 存在审批中(approveStatus=1) 且无任何审批通过(approveStatus=2) 的财务单据
|
||||
* 发货撤回路径已在 recall() / applyRecall() / 会签回调 上层拦截并提示"请驳回审批后再次操作";
|
||||
* 本方法内按情况 B 的红冲逻辑处理(订单撤单路径不纳入拦截)
|
||||
* 情况 B - 存在审批通过(approveStatus=2) 的财务单据 → 整张生成负金额红冲单,原单保留
|
||||
* 情况 C - 无任何关联财务单据:
|
||||
* · 应收:删除该出库单下全部应收单,并级联清理收款计划、开票计划
|
||||
* · 应付:一律不删
|
||||
* (inner_code 粒度的入库应付单是采购欠款,与发货撤回无关;
|
||||
* outer_code 粒度的应付单仅 OUTER_PAY(P001) 会产生)
|
||||
* 最后:清空该出库单库存明细上的 payable_bill_code,保证撤回后重新确认发货可再次生成应付单
|
||||
*
|
||||
* 审批状态检测(checkFinanceBillApproveStatus)取三条路径并集,任一命中即命中:
|
||||
* 出库单 outer_code(始终可靠)/SN(撤回后 inventory_info.outer_code 被清空会失效)/入库单 inner_code
|
||||
*
|
||||
* 红冲比例:
|
||||
* 应收、应付按出库单粒度:整张出库单已全部撤回,ratio=1(等价"整张取反")
|
||||
* 应付若走 inner_code 回退:按 inner_code 分组,每组 ratio = 该出库单发货中属于该入库单的台数 / 该入库单库存总台数
|
||||
*
|
||||
* @param deliveryId 发货记录主键(仅用于 outer_code 路径取数,该出库单下任一条记录均可)
|
||||
* @param outerCode 出库单号(用于关联应收/应付单)
|
||||
*/
|
||||
@Override
|
||||
public void handleRecallReceivablePayable(Long deliveryId, String outerCode) {
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 该出库单下的全部库存明细:撤回后 inventory_info.outer_code 已被 clearOutInfo 清空,
|
||||
// 故通过发货记录明细(oms_inventory_delivery_detail)反查,保证覆盖该出库单所有发货记录的 SN
|
||||
// ══════════════════════════════════════════════════════
|
||||
List<InventoryInfo> outerInventoryInfos = inventoryInfoMapper.listByOuterCodeViaDelivery(outerCode);
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 统一的审批状态检查(SN 与 outer_code 两条路径取并集,不受 clearOutInfo 执行顺序影响)
|
||||
// hasApprovingBill → 情况 A:存在审批中的财务单据(approveStatus=1)
|
||||
// hasApprovedBill → 情况 B:存在审批通过的财务单据(approveStatus=2)
|
||||
// 两个都是 false → 情况 C:无关联财务单据
|
||||
// ══════════════════════════════════════════════════════
|
||||
Map<String, Boolean> billStatus = checkFinanceBillApproveStatus(deliveryId, outerInventoryInfos);
|
||||
boolean hasApprovingBill = billStatus.get("approving");
|
||||
boolean hasApprovedBill = billStatus.get("approved");
|
||||
boolean hasAnyFinanceBill = hasApprovingBill || hasApprovedBill;
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 按「出库单」为颗粒度处理:一个出库单可能分多仓多次发货,但撤回即整张出库单撤回,
|
||||
// 故应收/应付(挂出库单号的)一律整张冲红或整张删除,不做数量比例拆分。
|
||||
// 仅应付走 inner_code 回退时,因其挂的是入库单号、一张单覆盖整批入库台数,
|
||||
// 才需要按「该出库单发货台数 / 该入库单总台数」的比例冲红。
|
||||
// ══════════════════════════════════════════════════════
|
||||
|
||||
// 仅处理软件和硬件类型:1、2
|
||||
Set<String> deleteProductTypeSet = new HashSet<>(Arrays.asList(
|
||||
ProductInfo.ProductTypeEnum.SOFTWARE.getType(),
|
||||
|
|
@ -834,80 +1155,139 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
List<OmsPayableBill> payableBills = payableBillService.selectOmsPayableBillList(queryPayable).stream()
|
||||
.filter(item -> deleteProductTypeSet.contains(item.getProductType()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// INNER_PAY(pay_type=0) 厂商的应付单在入库时按 inner_code 生成,出库时不生成。
|
||||
// 当 outer_code 查不到应付单时,通过 inventory_info.inner_code 回退查入库应付单。
|
||||
// 一个出库单的 SN 可能来自多个入库单,故按 inner_code 分组,
|
||||
// 每组比例 = 该出库单发货中属于该入库单的台数 / 该入库单的库存总台数。
|
||||
// ══════════════════════════════════════════════════════
|
||||
// inner_code → 该出库单发货中属于该入库单的台数
|
||||
Map<String, Long> deliveredQtyByInnerCode = new LinkedHashMap<>();
|
||||
// inner_code → 该入库单对应的应付单
|
||||
Map<String, List<OmsPayableBill>> innerPayableGroups = new LinkedHashMap<>();
|
||||
if (CollUtil.isEmpty(payableBills)) {
|
||||
if (CollUtil.isNotEmpty(outerInventoryInfos)) {
|
||||
deliveredQtyByInnerCode = outerInventoryInfos.stream()
|
||||
.filter(item -> StringUtils.isNotBlank(item.getInnerCode()))
|
||||
.collect(Collectors.groupingBy(InventoryInfo::getInnerCode, LinkedHashMap::new, Collectors.counting()));
|
||||
}
|
||||
for (Map.Entry<String, Long> entry : deliveredQtyByInnerCode.entrySet()) {
|
||||
OmsPayableBill innerQuery = new OmsPayableBill();
|
||||
innerQuery.setInventoryCode(entry.getKey());
|
||||
List<OmsPayableBill> innerPayables = payableBillService.selectOmsPayableBillList(innerQuery).stream()
|
||||
.filter(item -> deleteProductTypeSet.contains(item.getProductType()))
|
||||
.collect(Collectors.toList());
|
||||
if (CollUtil.isNotEmpty(innerPayables)) {
|
||||
innerPayableGroups.put(entry.getKey(), innerPayables);
|
||||
payableBills.addAll(innerPayables);
|
||||
}
|
||||
}
|
||||
if (CollUtil.isNotEmpty(innerPayableGroups)) {
|
||||
log.info("outer_code 未查到应付单,按 inner_code 回退查到 {} 组入库应付单:{}",
|
||||
innerPayableGroups.size(), innerPayableGroups.keySet());
|
||||
}
|
||||
}
|
||||
|
||||
OmsReceivableBill queryReceivable = new OmsReceivableBill();
|
||||
queryReceivable.setInventoryCode(outerCode);
|
||||
List<OmsReceivableBill> receivableBills = billService.selectOmsReceivableBillList(queryReceivable).stream()
|
||||
.filter(item -> deleteProductTypeSet.contains(item.getProductType()))
|
||||
.collect(Collectors.toList());
|
||||
if (hasFinanceBill) {
|
||||
// 存在应收/应付财务单据(未完成或已完成):不删除原单,改为生成红冲负金额应收、应付单
|
||||
generateRecallRedRushBill(payableBills, receivableBills);
|
||||
} else {
|
||||
// 无应收/应付财务单据:直接删除对应的应付、应收信息
|
||||
|
||||
// INNER_PAY 厂商的入库应付单本身不带合同号,红冲单若原样复制空合同号,
|
||||
// 在应付单列表按合同/项目筛选时查不到,故用出库单的合同号补齐合同号(仅内存对象,不影响原单)
|
||||
if (CollUtil.isNotEmpty(payableBills)) {
|
||||
String ids = payableBills.stream().map(item -> String.valueOf(item.getId())).collect(Collectors.joining(","));
|
||||
payableBillService.deleteOmsPayableBillByIds(ids);
|
||||
InventoryOuter recallOuter = inventoryOuterMapper.selectInventoryOuterByCode(outerCode);
|
||||
String recallOrderCode = recallOuter == null ? null : recallOuter.getOrderCode();
|
||||
if (StringUtils.isNotBlank(recallOrderCode)) {
|
||||
payableBills.stream()
|
||||
.filter(item -> StringUtils.isBlank(item.getOrderCode()))
|
||||
.forEach(item -> item.setOrderCode(recallOrderCode));
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAnyFinanceBill) {
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 情况 A(审批中)或 情况 B(审批通过):
|
||||
// 无条件按整张单据生成负金额红冲单,原单保留(整张出库单已全部撤回,ratio=1)
|
||||
// 应付按 inner_code 分组时各组比例不同,应收统一整张冲红
|
||||
// ══════════════════════════════════════════════════════
|
||||
if (CollUtil.isNotEmpty(innerPayableGroups)) {
|
||||
// INNER_PAY 回退:每个入库单按自身比例红冲(该出库单发货中属于该入库单的台数 / 该入库单总台数)
|
||||
for (Map.Entry<String, List<OmsPayableBill>> entry : innerPayableGroups.entrySet()) {
|
||||
String innerCode = entry.getKey();
|
||||
long deliveredQty = deliveredQtyByInnerCode.getOrDefault(innerCode, 0L);
|
||||
Long innerTotal = inventoryDeliveryMapper.countInventoryByInnerCode(innerCode);
|
||||
long innerTotalQty = innerTotal == null ? 0L : innerTotal;
|
||||
if (innerTotalQty <= 0) {
|
||||
log.warn("inner_code={} 库存总台数为0,跳过应付红冲", innerCode);
|
||||
continue;
|
||||
}
|
||||
BigDecimal groupRatio = BigDecimal.valueOf(deliveredQty)
|
||||
.divide(BigDecimal.valueOf(innerTotalQty), 10, RoundingMode.HALF_UP);
|
||||
if (groupRatio.compareTo(BigDecimal.ONE) > 0) {
|
||||
groupRatio = BigDecimal.ONE;
|
||||
}
|
||||
log.info("应付红冲:inner_code={}, 本出库单发货台数={}, 入库单总台数={}, 比例={}",
|
||||
innerCode, deliveredQty, innerTotalQty, groupRatio);
|
||||
generateRecallRedRushBill(entry.getValue(), Collections.emptyList(), groupRatio);
|
||||
}
|
||||
} else {
|
||||
generateRecallRedRushBill(payableBills, Collections.emptyList(), BigDecimal.ONE);
|
||||
}
|
||||
generateRecallRedRushBill(Collections.emptyList(), receivableBills, BigDecimal.ONE);
|
||||
if (hasApprovedBill) {
|
||||
log.info("发货撤回完成,存在审批通过的财务单据,已按整张出库单生成负金额红冲单。outerCode={}, payableCount={}, receivableCount={}",
|
||||
outerCode, payableBills.size(), receivableBills.size());
|
||||
} else {
|
||||
log.info("发货撤回完成,存在审批中的财务单据,已按整张出库单生成负金额红冲单保护数据。outerCode={}, payableCount={}, receivableCount={}",
|
||||
outerCode, payableBills.size(), receivableBills.size());
|
||||
}
|
||||
} else {
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 情况 C:无关联财务单据(无审批中 且 无审批通过)
|
||||
// 应收:整张出库单已全部撤回,删除该出库单下全部应收单(删除时级联清理收款/开票计划)
|
||||
// 应付:一律不处理
|
||||
// - inner_code 粒度的入库应付单代表采购欠款,与发货撤回无关
|
||||
// - outer_code 粒度的应付单仅 OUTER_PAY(P001) 厂商会产生,而 P001 已在入口处跳过
|
||||
// ══════════════════════════════════════════════════════
|
||||
if (CollUtil.isNotEmpty(receivableBills)) {
|
||||
String ids = receivableBills.stream().map(item -> String.valueOf(item.getId())).collect(Collectors.joining(","));
|
||||
billService.deleteOmsReceivableBillByIds(ids);
|
||||
log.info("情况C全部撤回:已删除该出库单下全部应收单。outerCode={}, count={}", outerCode, receivableBills.size());
|
||||
}
|
||||
log.info("情况C全部撤回:应付单不做处理(不删除)。outerCode={}, payableCount={}", outerCode, payableBills.size());
|
||||
}
|
||||
//清空本发货单库存明细上的应付单号,保证撤回后重新确认发货可再次生成应付单
|
||||
List<InventoryInfo> recallInventoryInfos = inventoryInfoService.listByDeliveryId(deliveryId);
|
||||
if (CollUtil.isNotEmpty(recallInventoryInfos)) {
|
||||
inventoryInfoService.clearPayableBillCodeByIds(recallInventoryInfos.stream().map(InventoryInfo::getId).collect(Collectors.toList()));
|
||||
//清空该出库单库存明细上的应付单号,保证撤回后重新确认发货可再次生成应付单(情况 B 和 C 都要清空)
|
||||
if (CollUtil.isNotEmpty(outerInventoryInfos)) {
|
||||
inventoryInfoService.clearPayableBillCodeByIds(outerInventoryInfos.stream().map(InventoryInfo::getId).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断发货撤回审批中记录的“金额是否变化”是否为“是”
|
||||
* 金额变化存储于待办/已办记录的 extendField3,发货记录id 存储于 extendField2
|
||||
*
|
||||
* @param deliveryId 发货记录主键
|
||||
* @return 金额是否变化为“是”则返回 true
|
||||
*/
|
||||
private boolean isRecallAmountChanged(Long deliveryId) {
|
||||
Todo query = new Todo()
|
||||
.setProcessKey(processConfig.getDefinition().getOuterReback())
|
||||
.setExtendField2(String.valueOf(deliveryId));
|
||||
// 先查待办,再查已办(审批通过后已办记录仍在)
|
||||
List<Todo> todos = todoMapper.selectTodoCompletedList(query);
|
||||
if (CollUtil.isEmpty(todos)) {
|
||||
Todo todo = todoMapper.selectTodo(query);
|
||||
if (todo != null) {
|
||||
todos = Collections.singletonList(todo);
|
||||
}
|
||||
}
|
||||
return CollUtil.isNotEmpty(todos) && "是".equals(todos.get(0).getExtendField3());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断产品制造商编码是否为 P001
|
||||
*
|
||||
* @param productCode 产品编码
|
||||
* @return 制造商编码为 P001 则返回 true
|
||||
*/
|
||||
private boolean isManufacturerP001(String productCode) {
|
||||
if (StringUtils.isEmpty(productCode)) {
|
||||
return false;
|
||||
}
|
||||
List<ProductInfo> products = productInfoService.selectProductInfoByCodeList(Collections.singletonList(productCode));
|
||||
if (CollUtil.isEmpty(products)) {
|
||||
return false;
|
||||
}
|
||||
return "P001".equals(products.get(0).getVendorCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回存在应收/应付财务单据时,为原应收、应付单生成对应的红冲单(负金额),不删除原单
|
||||
* 同时同步生成红冲单的关联明细(付款/收票/收款/开票明细):金额按同一比例取负,
|
||||
* 保留原关联单号(付款单/收票单/收款单/开票单),write_off_id 置空不参与原核销,计划 id 指向红冲单自身
|
||||
*
|
||||
* @param payableBills 原应付单(仅软件/硬件类型)
|
||||
* @param receivableBills 原应收单(仅软件/硬件类型)
|
||||
* @param ratio 红冲金额比例:整张出库单撤回=1;应付按入库单回退时为 该出库单发货台数/入库单总台数(0~1)
|
||||
*/
|
||||
private void generateRecallRedRushBill(List<OmsPayableBill> payableBills, List<OmsReceivableBill> receivableBills) {
|
||||
private void generateRecallRedRushBill(List<OmsPayableBill> payableBills, List<OmsReceivableBill> receivableBills, BigDecimal ratio) {
|
||||
if (ratio == null) {
|
||||
ratio = BigDecimal.ONE;
|
||||
}
|
||||
String operator = ShiroUtils.getUserId().toString();
|
||||
Date now = DateUtils.getNowDate();
|
||||
if (CollUtil.isNotEmpty(payableBills)) {
|
||||
for (OmsPayableBill src : payableBills) {
|
||||
// 已是红冲单(负金额)的不再二次红冲,否则会把上一次的冲销金额又加回来
|
||||
if (src.getTotalPriceWithTax() != null && src.getTotalPriceWithTax().signum() < 0) {
|
||||
log.info("跳过红冲单,不重复红冲。payableBillCode={}, totalPriceWithTax={}",
|
||||
src.getPayableBillCode(), src.getTotalPriceWithTax());
|
||||
continue;
|
||||
}
|
||||
OmsPayableBill redRush = new OmsPayableBill();
|
||||
redRush.setVendorCode(src.getVendorCode());
|
||||
redRush.setOrderCode(src.getOrderCode());
|
||||
|
|
@ -916,15 +1296,26 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
redRush.setProductType(src.getProductType());
|
||||
redRush.setProductLevel2Type(src.getProductLevel2Type());
|
||||
redRush.setTaxRate(src.getTaxRate());
|
||||
// 负金额红冲
|
||||
redRush.setTotalPriceWithTax(src.getTotalPriceWithTax() == null ? null : src.getTotalPriceWithTax().negate());
|
||||
redRush.setTotalPriceWithoutTax(src.getTotalPriceWithoutTax() == null ? null : src.getTotalPriceWithoutTax().negate());
|
||||
redRush.setTaxAmount(src.getTaxAmount() == null ? null : src.getTaxAmount().negate());
|
||||
// 按比例负金额红冲:含税、不含税分别乘比例取负,税额=含税-不含税,保证三者一致
|
||||
BigDecimal withTax = negRatio(src.getTotalPriceWithTax(), ratio);
|
||||
BigDecimal withoutTax = negRatio(src.getTotalPriceWithoutTax(), ratio);
|
||||
redRush.setTotalPriceWithTax(withTax);
|
||||
redRush.setTotalPriceWithoutTax(withoutTax);
|
||||
redRush.setTaxAmount(withTax == null || withoutTax == null ? negRatio(src.getTaxAmount(), ratio)
|
||||
: withTax.subtract(withoutTax));
|
||||
payableBillService.insertOmsPayableBill(redRush, 0);
|
||||
// 同步生成红冲单的付款明细、收票明细
|
||||
copyPayableDetails(src, redRush, ratio, operator, now);
|
||||
}
|
||||
}
|
||||
if (CollUtil.isNotEmpty(receivableBills)) {
|
||||
for (OmsReceivableBill src : receivableBills) {
|
||||
// 已是红冲单(负金额)的不再二次红冲,否则会把上一次的冲销金额又加回来
|
||||
if (src.getTotalPriceWithTax() != null && src.getTotalPriceWithTax().signum() < 0) {
|
||||
log.info("跳过红冲单,不重复红冲。receivableBillCode={}, totalPriceWithTax={}",
|
||||
src.getReceivableBillCode(), src.getTotalPriceWithTax());
|
||||
continue;
|
||||
}
|
||||
OmsReceivableBill redRush = new OmsReceivableBill();
|
||||
redRush.setPartnerCode(src.getPartnerCode());
|
||||
redRush.setPartnerName(src.getPartnerName());
|
||||
|
|
@ -933,15 +1324,124 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
|
|||
redRush.setProductCode(src.getProductCode());
|
||||
redRush.setProductType(src.getProductType());
|
||||
redRush.setTaxRate(src.getTaxRate());
|
||||
// 负金额红冲
|
||||
redRush.setTotalPriceWithTax(src.getTotalPriceWithTax() == null ? null : src.getTotalPriceWithTax().negate());
|
||||
redRush.setTotalPriceWithoutTax(src.getTotalPriceWithoutTax() == null ? null : src.getTotalPriceWithoutTax().negate());
|
||||
redRush.setTaxAmount(src.getTaxAmount() == null ? null : src.getTaxAmount().negate());
|
||||
BigDecimal withTax = negRatio(src.getTotalPriceWithTax(), ratio);
|
||||
BigDecimal withoutTax = negRatio(src.getTotalPriceWithoutTax(), ratio);
|
||||
redRush.setTotalPriceWithTax(withTax);
|
||||
redRush.setTotalPriceWithoutTax(withoutTax);
|
||||
redRush.setTaxAmount(withTax == null || withoutTax == null ? negRatio(src.getTaxAmount(), ratio)
|
||||
: withTax.subtract(withoutTax));
|
||||
billService.insertOmsReceivableBill(redRush);
|
||||
// 同步生成红冲单的收款明细、开票明细
|
||||
copyReceivableDetails(src, redRush, ratio, operator, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制原应付单的付款明细、收票明细到红冲单(金额按比例取负,关联单号保留,计划id指向红冲单、write_off_id 置空)
|
||||
*/
|
||||
private void copyPayableDetails(OmsPayableBill src, OmsPayableBill redRush, BigDecimal ratio, String operator, Date now) {
|
||||
List<OmsPayablePaymentDetail> paymentDetails = payablePaymentDetailService.listByPayableBillId(src.getId());
|
||||
if (CollUtil.isNotEmpty(paymentDetails)) {
|
||||
for (OmsPayablePaymentDetail srcDetail : paymentDetails) {
|
||||
OmsPayablePaymentDetail detail = new OmsPayablePaymentDetail();
|
||||
detail.setPayableBillId(redRush.getId());
|
||||
detail.setPaymentPlanId(redRush.getLastPaymentPlanId());
|
||||
detail.setPaymentAmount(negRatio(srcDetail.getPaymentAmount(), ratio));
|
||||
detail.setPaymentAmountWithoutTax(negRatio(srcDetail.getPaymentAmountWithoutTax(), ratio));
|
||||
detail.setPaymentAmountTax(negRatio(srcDetail.getPaymentAmountTax(), ratio));
|
||||
detail.setPaymentRate(srcDetail.getPaymentRate());
|
||||
detail.setPaymentTime(srcDetail.getPaymentTime());
|
||||
detail.setPaymentBillCode(srcDetail.getPaymentBillCode());
|
||||
detail.setPayableDetailType(srcDetail.getPayableDetailType());
|
||||
detail.setRemark(srcDetail.getRemark());
|
||||
// 不参与原核销
|
||||
detail.setWriteOffId(null);
|
||||
detail.setCreateBy(operator);
|
||||
detail.setCreateTime(now);
|
||||
payablePaymentDetailService.insertOmsPayablePaymentDetail(detail);
|
||||
}
|
||||
}
|
||||
List<OmsPayableTicketDetail> ticketDetails = payableTicketDetailService.listByPayableBillIdList(
|
||||
Collections.singletonList(src.getId()));
|
||||
if (CollUtil.isNotEmpty(ticketDetails)) {
|
||||
for (OmsPayableTicketDetail srcDetail : ticketDetails) {
|
||||
OmsPayableTicketDetail detail = new OmsPayableTicketDetail();
|
||||
detail.setPayableBillId(redRush.getId());
|
||||
detail.setTicketPlanId(redRush.getLastTicketPlanId());
|
||||
detail.setPaymentAmount(negRatio(srcDetail.getPaymentAmount(), ratio));
|
||||
detail.setPaymentAmountWithoutTax(negRatio(srcDetail.getPaymentAmountWithoutTax(), ratio));
|
||||
detail.setPaymentAmountTax(negRatio(srcDetail.getPaymentAmountTax(), ratio));
|
||||
detail.setPaymentRate(srcDetail.getPaymentRate());
|
||||
detail.setPaymentTime(srcDetail.getPaymentTime());
|
||||
detail.setTicketBillCode(srcDetail.getTicketBillCode());
|
||||
detail.setPayableDetailType(srcDetail.getPayableDetailType());
|
||||
detail.setRemark(srcDetail.getRemark());
|
||||
detail.setWriteOffId(null);
|
||||
detail.setCreateBy(operator);
|
||||
detail.setCreateTime(now);
|
||||
payableTicketDetailService.insertOmsPayableTicketDetail(detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制原应收单的收款明细、开票明细到红冲单(金额按比例取负,关联单号保留,计划id指向红冲单、write_off_id 置空)
|
||||
*/
|
||||
private void copyReceivableDetails(OmsReceivableBill src, OmsReceivableBill redRush, BigDecimal ratio, String operator, Date now) {
|
||||
List<OmsReceivableReceiptDetail> receiptDetails = receivableReceiptDetailService.listByReceivableBillId(src.getId());
|
||||
if (CollUtil.isNotEmpty(receiptDetails)) {
|
||||
for (OmsReceivableReceiptDetail srcDetail : receiptDetails) {
|
||||
OmsReceivableReceiptDetail detail = new OmsReceivableReceiptDetail();
|
||||
detail.setReceivableBillId(redRush.getId());
|
||||
detail.setReceiptPlanId(redRush.getLastReceiptPlanId());
|
||||
detail.setReceiptAmount(negRatio(srcDetail.getReceiptAmount(), ratio));
|
||||
detail.setReceiptAmountWithoutTax(negRatio(srcDetail.getReceiptAmountWithoutTax(), ratio));
|
||||
detail.setReceiptAmountTax(negRatio(srcDetail.getReceiptAmountTax(), ratio));
|
||||
detail.setReceiptRate(srcDetail.getReceiptRate());
|
||||
detail.setReceiptTime(srcDetail.getReceiptTime());
|
||||
detail.setReceiptBillCode(srcDetail.getReceiptBillCode());
|
||||
detail.setReceivableDetailType(srcDetail.getReceivableDetailType());
|
||||
detail.setRemark(srcDetail.getRemark());
|
||||
detail.setWriteOffId(null);
|
||||
detail.setCreateBy(operator);
|
||||
detail.setCreateTime(now);
|
||||
receivableReceiptDetailService.insertOmsReceivableReceiptDetail(detail);
|
||||
}
|
||||
}
|
||||
List<OmsReceivableInvoiceDetail> invoiceDetails = receivableInvoiceDetailService.listByReceivableBillId(src.getId());
|
||||
if (CollUtil.isNotEmpty(invoiceDetails)) {
|
||||
for (OmsReceivableInvoiceDetail srcDetail : invoiceDetails) {
|
||||
OmsReceivableInvoiceDetail detail = new OmsReceivableInvoiceDetail();
|
||||
detail.setReceivableBillId(redRush.getId());
|
||||
detail.setInvoicePlanId(redRush.getLastInvoicePlanId());
|
||||
detail.setInvoiceAmount(negRatio(srcDetail.getInvoiceAmount(), ratio));
|
||||
detail.setInvoiceAmountWithoutTax(negRatio(srcDetail.getInvoiceAmountWithoutTax(), ratio));
|
||||
detail.setInvoiceAmountTax(negRatio(srcDetail.getInvoiceAmountTax(), ratio));
|
||||
detail.setInvoiceRate(srcDetail.getInvoiceRate());
|
||||
detail.setInvoiceTime(srcDetail.getInvoiceTime());
|
||||
detail.setInvoiceBillCode(srcDetail.getInvoiceBillCode());
|
||||
detail.setReceivableDetailType(srcDetail.getReceivableDetailType());
|
||||
detail.setRemark(srcDetail.getRemark());
|
||||
detail.setWriteOffId(null);
|
||||
detail.setCreateBy(operator);
|
||||
detail.setCreateTime(now);
|
||||
receivableInvoiceDetailService.insertOmsReceivableInvoiceDetail(detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算红冲金额:原金额 × 比例 × (-1),保留两位小数(HALF_UP)。原金额为 null 时返回 null。
|
||||
*/
|
||||
private BigDecimal negRatio(BigDecimal srcAmount, BigDecimal ratio) {
|
||||
if (srcAmount == null) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal r = ratio == null ? BigDecimal.ONE : ratio;
|
||||
return srcAmount.multiply(r).setScale(2, RoundingMode.HALF_UP).negate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InventoryDeliveryDetailExcelDto> detailExport(InventoryDelivery inventoryDelivery) {
|
||||
InventoryDelivery dbData = inventoryDeliveryMapper.selectInventoryDeliveryById(inventoryDelivery.getId());
|
||||
|
|
|
|||
|
|
@ -175,6 +175,11 @@ public class InventoryOuterServiceImpl implements IInventoryOuterService
|
|||
public int deleteInventoryOuterById(Long id)
|
||||
{
|
||||
InventoryOuter inventoryOuter = inventoryOuterMapper.selectInventoryOuterById(id);
|
||||
// 已撤回/作废的出库单在撤回时已回补过实时库存与订单出库状态,重复撤销会导致重复回补
|
||||
InventoryOuter currentOuter = inventoryOuterMapper.selectInventoryOuterByCode(inventoryOuter.getOuterCode());
|
||||
if (currentOuter != null && InventoryOuter.DeliveryStatusEnum.REBACK.getCode().equals(currentOuter.getDeliveryStatus())) {
|
||||
throw new ServiceException("该出库单已撤回/作废,无需重复撤销");
|
||||
}
|
||||
detailService.deleteByOuterCode(Collections.singletonList(inventoryOuter.getOuterCode()));
|
||||
productInfoService.updateAvailableCount(inventoryOuter.getQuantity(), inventoryOuter.getProductCode());
|
||||
|
||||
|
|
@ -192,6 +197,11 @@ public class InventoryOuterServiceImpl implements IInventoryOuterService
|
|||
|
||||
@Override
|
||||
public int statusUpdate(InventoryOuter inventoryOuter) {
|
||||
// 已撤回/作废的出库单不允许再「确认接收 / 退回」,避免产生与作废状态矛盾的数据
|
||||
InventoryOuter currentOuter = inventoryOuterMapper.selectInventoryOuterById(inventoryOuter.getId());
|
||||
if (currentOuter != null && InventoryOuter.DeliveryStatusEnum.REBACK.getCode().equals(currentOuter.getDeliveryStatus())) {
|
||||
throw new ServiceException("该出库单已撤回/作废,无法再确认接收或退回");
|
||||
}
|
||||
InventoryOuter queryDto = new InventoryOuter();
|
||||
queryDto.setOrderCode(inventoryOuter.getOrderCode());
|
||||
ProjectOrderInfo projectOrderInfo = new ProjectOrderInfo();
|
||||
|
|
@ -283,6 +293,11 @@ public class InventoryOuterServiceImpl implements IInventoryOuterService
|
|||
List<InventoryDelivery> tempDeliveryList = deliveryListMap.get(vo.getWarehouseId());
|
||||
if (CollUtil.isNotEmpty(tempDeliveryList)){
|
||||
for (InventoryDelivery inventoryDelivery : tempDeliveryList) {
|
||||
// 列表不再排除已撤回记录,此处必须显式忽略,否则已撤回数量会被算进"已确认发货数量"
|
||||
if (InventoryDelivery.DeliveryStatusEnum.RECALL_DELIVERY.getCode()
|
||||
.equals(inventoryDelivery.getDeliveryStatus())) {
|
||||
continue;
|
||||
}
|
||||
if (inventoryDelivery.getDeliveryStatus().equals(InventoryDelivery.DeliveryStatusEnum.WAIT_DELIVERY.getCode())) {
|
||||
vo.setDeliveryGenerateQuantity(vo.getDeliveryGenerateQuantity() + inventoryDelivery.getQuantity());
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.service.impl;
|
|||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
|
|
@ -632,6 +633,13 @@ public class OmsInvoiceBillServiceImpl implements IOmsInvoiceBillService, TodoCo
|
|||
|
||||
}
|
||||
|
||||
/** DecimalFormat 非线程安全,导出接口可能被并发调用,故按线程持有,避免金额串位 */
|
||||
private static final ThreadLocal<DecimalFormat> AMOUNT_FORMAT = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00"));
|
||||
|
||||
private String formatAmount(BigDecimal amount) {
|
||||
return amount == null ? "" : AMOUNT_FORMAT.get().format(amount);
|
||||
}
|
||||
|
||||
private List<List<String>> buildExcelData(List<InvoiceDetailItemExcelDto> list) {
|
||||
List<List<String>> dataList = new ArrayList<>();
|
||||
AtomicInteger integer=new AtomicInteger(1);
|
||||
|
|
@ -649,10 +657,10 @@ public class OmsInvoiceBillServiceImpl implements IOmsInvoiceBillService, TodoCo
|
|||
row.add(item.getProductModel());
|
||||
row.add(item.getUnit());
|
||||
row.add(String.valueOf(item.getQuantity()));
|
||||
row.add(String.valueOf(item.getPrice()));
|
||||
row.add(String.valueOf(item.getAllPrice()));
|
||||
row.add(formatAmount(item.getPrice()));
|
||||
row.add(formatAmount(item.getAllPrice()));
|
||||
row.add(String.valueOf(item.getTaxRate()));
|
||||
row.add(String.valueOf(reduce));
|
||||
row.add(formatAmount(reduce));
|
||||
row.add(DictUtils.getDictLabel("finance_invoice_type",item.getInvoiceType()));
|
||||
row.add(item.getRemark());
|
||||
dataList.add(row);
|
||||
|
|
|
|||
|
|
@ -20,9 +20,12 @@ import com.ruoyi.sip.mapper.InventoryInfoMapper;
|
|||
import com.ruoyi.sip.mapper.InventoryOuterMapper;
|
||||
import com.ruoyi.sip.mapper.OmsInventoryInnerMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayableBillMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayablePaymentDetailMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayablePaymentPlanMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayableTicketDetailMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayableTicketPlanMapper;
|
||||
import com.ruoyi.sip.service.*;
|
||||
import com.ruoyi.sip.utils.GoodsDetailUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
|
@ -51,6 +54,10 @@ public class OmsPayableBillServiceImpl implements IOmsPayableBillService {
|
|||
@Autowired
|
||||
private OmsPayableTicketPlanMapper omsPayableTicketPlanMapper;
|
||||
@Autowired
|
||||
private OmsPayablePaymentDetailMapper omsPayablePaymentDetailMapper;
|
||||
@Autowired
|
||||
private OmsPayableTicketDetailMapper omsPayableTicketDetailMapper;
|
||||
@Autowired
|
||||
private IOmsPaymentBillService omsPaymentBillService;
|
||||
|
||||
@Autowired
|
||||
|
|
@ -184,15 +191,19 @@ public class OmsPayableBillServiceImpl implements IOmsPayableBillService {
|
|||
|
||||
/**
|
||||
* 批量删除采购应付单
|
||||
* 级联清理其付款计划、收票计划、付款明细、收票明细,避免删除单据后子表残留孤儿数据
|
||||
*
|
||||
* @param ids 需要删除的采购应付单主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public int deleteOmsPayableBillByIds(String ids) {
|
||||
// Also delete payment plans
|
||||
for (String id : Convert.toStrArray(ids)) {
|
||||
omsPayablePaymentPlanMapper.deleteOmsPayablePaymentPlanByPayableBillId(Long.valueOf(id));
|
||||
Long payableBillId = Long.valueOf(id);
|
||||
omsPayablePaymentPlanMapper.deleteOmsPayablePaymentPlanByPayableBillId(payableBillId);
|
||||
omsPayableTicketPlanMapper.deleteByPayableBillId(payableBillId);
|
||||
omsPayablePaymentDetailMapper.deleteByPayableBillId(payableBillId);
|
||||
omsPayableTicketDetailMapper.deleteByPayableBillId(payableBillId);
|
||||
}
|
||||
return omsPayableBillMapper.deleteOmsPayableBillByIds(Convert.toStrArray(ids));
|
||||
}
|
||||
|
|
@ -563,20 +574,21 @@ public class OmsPayableBillServiceImpl implements IOmsPayableBillService {
|
|||
List<OmsPaymentBill> omsPaymentBills = omsPaymentBillService.listPreResidueAmountByVendorCodeList(Collections.singletonList(omsPayableBill.getVendorCode()));
|
||||
Map<String, BigDecimal> decimalMap = omsPaymentBills.stream().filter(item -> item.getPreResidueAmount() != null).collect(Collectors.toMap(OmsPaymentBill::getVendorCode, OmsPaymentBill::getPreResidueAmount, BigDecimal::add));
|
||||
omsPayableBill.setPreResidueAmount(decimalMap.getOrDefault(omsPayableBill.getVendorCode(), BigDecimal.ZERO));
|
||||
// 商品明细(来源:入库单)
|
||||
omsPayableBill.setGoodsDetailList(queryGoodsDetailList(omsPayableBill.getInventoryCode()));
|
||||
// 商品明细(来源:入库单/出库单,冲红单按单据金额缩放)
|
||||
omsPayableBill.setGoodsDetailList(queryGoodsDetailList(omsPayableBill));
|
||||
|
||||
return omsPayableBill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过入库/出库单号查询应付单商品明细
|
||||
* 查询应付单商品明细
|
||||
* inventoryCode 可能为入库单号(inner_code)或出库单号(outer_code)
|
||||
*
|
||||
* @param inventoryCode 入库/出库单号
|
||||
* @param omsPayableBill 应付单
|
||||
* @return 商品明细列表
|
||||
*/
|
||||
private List<PayableGoodsDetailDto> queryGoodsDetailList(String inventoryCode) {
|
||||
private List<PayableGoodsDetailDto> queryGoodsDetailList(OmsPayableBill omsPayableBill) {
|
||||
String inventoryCode = omsPayableBill.getInventoryCode();
|
||||
if (StringUtils.isEmpty(inventoryCode)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
|
@ -588,67 +600,29 @@ public class OmsPayableBillServiceImpl implements IOmsPayableBillService {
|
|||
// 产品类型为3(服务)/22(硬件维保)时 含税小计 = 单价 × 数量,其他为入库价合计
|
||||
boolean multiplyQuantity = Arrays.asList("3", "22").contains(inventoryInner.getProductType());
|
||||
BigDecimal taxRate = inventoryInner.getTaxRate() != null ? inventoryInner.getTaxRate() : defaultTaxRate();
|
||||
return buildGoodsDetailList(inventoryInner.getProductType(), taxRate, inventoryInfoList, multiplyQuantity);
|
||||
return GoodsDetailUtils.build(inventoryInfoList, inventoryInner.getProductType(), taxRate, false, multiplyQuantity,
|
||||
omsPayableBill.getTotalPriceWithTax());
|
||||
}
|
||||
// 出库单来源
|
||||
InventoryOuter inventoryOuter = inventoryOuterMapper.selectInventoryOuterByCode(inventoryCode);
|
||||
if (inventoryOuter != null) {
|
||||
List<InventoryInfo> inventoryInfoList = inventoryInfoMapper.selectInventoryInfoByOuterCodeList(Collections.singletonList(inventoryCode));
|
||||
if (CollUtil.isEmpty(inventoryInfoList)) {
|
||||
// 撤回后 inventory_info.outer_code 被清空,改用发货记录+SN明细反查
|
||||
inventoryInfoList = inventoryInfoMapper.listByOuterCodeViaDelivery(inventoryCode);
|
||||
}
|
||||
if (CollUtil.isEmpty(inventoryInfoList)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String productType = inventoryInfoList.get(0).getProductType();
|
||||
BigDecimal taxRate = inventoryInfoList.get(0).getTaxRate() != null ? inventoryInfoList.get(0).getTaxRate() : defaultTaxRate();
|
||||
// 出库单生成的应付单:含税小计直接为入库价合计,不乘数量
|
||||
return buildGoodsDetailList(productType, taxRate, inventoryInfoList, false);
|
||||
return GoodsDetailUtils.build(inventoryInfoList, productType, taxRate, false, false,
|
||||
omsPayableBill.getTotalPriceWithTax());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按产品编码分组组装商品明细
|
||||
*
|
||||
* @param productType 产品类型
|
||||
* @param taxRate 税率(小数)
|
||||
* @param inventoryInfoList 库存明细
|
||||
* @param multiplyQuantity 是否 含税小计 = 单价 × 数量
|
||||
* @return 商品明细列表
|
||||
*/
|
||||
private List<PayableGoodsDetailDto> buildGoodsDetailList(String productType, BigDecimal taxRate,
|
||||
List<InventoryInfo> inventoryInfoList,
|
||||
boolean multiplyQuantity) {
|
||||
if (CollUtil.isEmpty(inventoryInfoList)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// 按产品编码分组(同一单据可能存在多行SN明细)
|
||||
Map<String, List<InventoryInfo>> groupMap = inventoryInfoList.stream()
|
||||
.filter(item -> StringUtils.isNotEmpty(item.getProductCode()))
|
||||
.collect(Collectors.groupingBy(InventoryInfo::getProductCode));
|
||||
List<PayableGoodsDetailDto> goodsDetailList = new ArrayList<>();
|
||||
for (Map.Entry<String, List<InventoryInfo>> entry : groupMap.entrySet()) {
|
||||
List<InventoryInfo> infos = entry.getValue();
|
||||
InventoryInfo first = infos.get(0);
|
||||
PayableGoodsDetailDto dto = new PayableGoodsDetailDto();
|
||||
dto.setProductType(productType);
|
||||
dto.setProductCode(entry.getKey());
|
||||
dto.setProductModel(first.getModel());
|
||||
dto.setProductDescription(first.getProductDesc());
|
||||
dto.setQuantity((long) infos.size());
|
||||
dto.setUnit("个");
|
||||
dto.setPrice(first.getInnerPrice());
|
||||
dto.setTaxRate(taxRate);
|
||||
if (multiplyQuantity) {
|
||||
dto.setAmountTotal(first.getInnerPrice() == null ? BigDecimal.ZERO
|
||||
: first.getInnerPrice().multiply(new BigDecimal(infos.size())));
|
||||
} else {
|
||||
dto.setAmountTotal(infos.stream().map(InventoryInfo::getInnerPrice).filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
}
|
||||
goodsDetailList.add(dto);
|
||||
}
|
||||
return goodsDetailList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认税率(小数)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.ruoyi.sip.service.impl;
|
|||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -219,9 +221,9 @@ public class OmsPaymentBillServiceImpl implements IOmsPaymentBillService , TodoC
|
|||
formatDateTime(bill.getPaymentTime()),
|
||||
bill.getVendorName(),
|
||||
bill.getOrderCode(),
|
||||
bill.getTotalPriceWithTax(),
|
||||
bill.getTotalPriceWithoutTax(),
|
||||
bill.getTaxAmount(),
|
||||
formatAmount(bill.getTotalPriceWithTax()),
|
||||
formatAmount(bill.getTotalPriceWithoutTax()),
|
||||
formatAmount(bill.getTaxAmount()),
|
||||
formatDateTime(bill.getActualPaymentTime()),
|
||||
DictUtils.getDictLabel("payment_status", bill.getPaymentStatus()),
|
||||
DictUtils.getDictLabel("approve_status", bill.getApproveStatus()),
|
||||
|
|
@ -240,8 +242,8 @@ public class OmsPaymentBillServiceImpl implements IOmsPaymentBillService , TodoC
|
|||
row.add(detail.getProjectCode());
|
||||
row.add(detail.getProjectName());
|
||||
row.add(detail.getPayableBillCode());
|
||||
row.add(detail.getTotalPriceWithTax());
|
||||
row.add(detail.getPaymentAmount());
|
||||
row.add(formatAmount(detail.getTotalPriceWithTax()));
|
||||
row.add(formatAmount(detail.getPaymentAmount()));
|
||||
row.add(detail.getPaymentRate());
|
||||
} else {
|
||||
row.add("");
|
||||
|
|
@ -261,6 +263,13 @@ public class OmsPaymentBillServiceImpl implements IOmsPaymentBillService , TodoC
|
|||
return date == null ? "" : DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", date);
|
||||
}
|
||||
|
||||
/** DecimalFormat 非线程安全,导出接口可能被并发调用,故按线程持有,避免金额串位 */
|
||||
private static final ThreadLocal<DecimalFormat> AMOUNT_FORMAT = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00"));
|
||||
|
||||
private String formatAmount(BigDecimal amount) {
|
||||
return amount == null ? "" : AMOUNT_FORMAT.get().format(amount);
|
||||
}
|
||||
|
||||
private String resolvePaymentBillTypeDesc(String paymentBillType) {
|
||||
if (OmsPaymentBill.PaymentBillTypeEnum.FROM_PAYABLE.getCode().equals(paymentBillType)) {
|
||||
return OmsPaymentBill.PaymentBillTypeEnum.FROM_PAYABLE.getDesc();
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService, TodoCo
|
|||
omsReceiptBillMapper.update(omsReceiptBill);
|
||||
|
||||
// 上传文件路径
|
||||
if (file != null && !file.isEmpty()) {
|
||||
String filePath = RuoYiConfig.getUploadPath();
|
||||
// 上传并返回新文件名称
|
||||
String fileName = FileUploadUtils.upload(filePath, file);
|
||||
|
|
@ -214,6 +215,7 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService, TodoCo
|
|||
attachment.setFileType(file.getContentType());
|
||||
attachment.setCreateBy(loginUser.getUserId().toString());
|
||||
attachmentService.insertOmsFinAttachment(attachment);
|
||||
}
|
||||
//开始审批
|
||||
todoService.startProcessDeleteBefore(receiptBill.getReceiptBillCode(), receiptBill.getReceiptBillCode()
|
||||
, new HashMap<String, Object>() {{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import cn.hutool.core.collection.CollUtil;
|
|||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.core.text.Convert;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.PageUtils;
|
||||
import com.ruoyi.common.utils.ShiroUtils;
|
||||
|
|
@ -19,11 +20,18 @@ import com.ruoyi.common.utils.spring.SpringUtils;
|
|||
import com.ruoyi.sip.domain.*;
|
||||
import com.ruoyi.sip.domain.dto.*;
|
||||
import com.ruoyi.sip.service.*;
|
||||
import com.ruoyi.sip.utils.GoodsDetailUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.sip.mapper.InventoryInfoMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableBillMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableInvoicePlanMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableReceiptPlanMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableReceiptDetailMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableInvoiceDetailMapper;
|
||||
|
||||
/**
|
||||
* 销售应收单Service业务层处理
|
||||
|
|
@ -61,6 +69,16 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
|
|||
private IOmsCompanyInfoService companyInfoService;
|
||||
@Autowired
|
||||
private IOmsReceivableInvoiceDetailService invoiceDetailService;
|
||||
@Autowired
|
||||
private InventoryInfoMapper inventoryInfoMapper;
|
||||
@Autowired
|
||||
private OmsReceivableReceiptPlanMapper omsReceivableReceiptPlanMapper;
|
||||
@Autowired
|
||||
private OmsReceivableInvoicePlanMapper omsReceivableInvoicePlanMapper;
|
||||
@Autowired
|
||||
private OmsReceivableReceiptDetailMapper omsReceivableReceiptDetailMapper;
|
||||
@Autowired
|
||||
private OmsReceivableInvoiceDetailMapper omsReceivableInvoiceDetailMapper;
|
||||
@Value("${oms.inventory.innerTax:0.13}")
|
||||
private String defaultTax;
|
||||
/**
|
||||
|
|
@ -114,6 +132,7 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
|
|||
|
||||
/**
|
||||
* 批量删除销售应收单
|
||||
* 级联清理其收款计划、开票计划、收款明细、开票明细,避免删除单据后子表残留孤儿数据
|
||||
*
|
||||
* @param ids 需要删除的销售应收单主键
|
||||
* @return 结果
|
||||
|
|
@ -121,6 +140,13 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
|
|||
@Override
|
||||
public int deleteOmsReceivableBillByIds(String ids)
|
||||
{
|
||||
for (String id : Convert.toStrArray(ids)) {
|
||||
Long receivableBillId = Long.valueOf(id);
|
||||
omsReceivableReceiptPlanMapper.deleteByReceivableBillId(receivableBillId);
|
||||
omsReceivableInvoicePlanMapper.deleteByReceivableBillId(receivableBillId);
|
||||
omsReceivableReceiptDetailMapper.deleteByReceivableBillId(receivableBillId);
|
||||
omsReceivableInvoiceDetailMapper.deleteByReceivableBillId(receivableBillId);
|
||||
}
|
||||
return omsReceivableBillMapper.deleteOmsReceivableBillByIds(ids);
|
||||
}
|
||||
|
||||
|
|
@ -155,10 +181,51 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
|
|||
Map<String, BigDecimal> decimalMap = receiptBills.stream().filter(item -> item.getRemainingAmount() != null)
|
||||
.collect(Collectors.toMap(OmsReceiptBill::getPartnerCode, OmsReceiptBill::getRemainingAmount, BigDecimal::add));
|
||||
omsReceivableBill.setRemainingAmount(decimalMap.getOrDefault(omsReceivableBill.getPartnerCode(), BigDecimal.ZERO));
|
||||
// 商品明细(来源:出库单,冲红单按单据金额缩放)
|
||||
omsReceivableBill.setGoodsDetailList(queryGoodsDetailList(omsReceivableBill));
|
||||
}
|
||||
return omsReceivableBill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询应收单商品明细(来源:出库单)
|
||||
* inventoryCode 为出库单号;撤回后 inventory_info.outer_code 会被清空,
|
||||
* 此时改用发货记录+SN明细反查,保证撤回后仍能查到明细
|
||||
*
|
||||
* @param omsReceivableBill 应收单
|
||||
* @return 商品明细列表
|
||||
*/
|
||||
private List<PayableGoodsDetailDto> queryGoodsDetailList(OmsReceivableBill omsReceivableBill) {
|
||||
String inventoryCode = omsReceivableBill.getInventoryCode();
|
||||
if (StringUtils.isEmpty(inventoryCode)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<InventoryInfo> inventoryInfoList = inventoryInfoMapper.selectInventoryInfoByOuterCodeList(Collections.singletonList(inventoryCode));
|
||||
if (CollUtil.isEmpty(inventoryInfoList)) {
|
||||
// 撤回后 inventory_info.outer_code 被清空,改用发货记录+SN明细反查
|
||||
inventoryInfoList = inventoryInfoMapper.listByOuterCodeViaDelivery(inventoryCode);
|
||||
}
|
||||
if (CollUtil.isEmpty(inventoryInfoList)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String productType = inventoryInfoList.get(0).getProductType();
|
||||
BigDecimal taxRate = inventoryInfoList.get(0).getTaxRate() != null ? inventoryInfoList.get(0).getTaxRate() : defaultTaxRate();
|
||||
// 应收单取价方式为出库价,含税小计为出库价合计
|
||||
return GoodsDetailUtils.build(inventoryInfoList, productType, taxRate, true, false,
|
||||
omsReceivableBill.getTotalPriceWithTax());
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认税率(小数)
|
||||
*/
|
||||
private BigDecimal defaultTaxRate() {
|
||||
try {
|
||||
return new BigDecimal(defaultTax);
|
||||
} catch (Exception e) {
|
||||
return new BigDecimal("0.13");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int mergeAndInitiateReceipt(MergedReceviableReceiptDataDto dto) {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import java.math.BigDecimal;
|
|||
import java.math.RoundingMode;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
|
|
@ -583,6 +584,13 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
|
|||
return DateUtils.parseDateToStr(pattern, date);
|
||||
}
|
||||
|
||||
/** DecimalFormat 非线程安全,导出接口可能被并发调用,故按线程持有,避免金额串位 */
|
||||
private static final ThreadLocal<DecimalFormat> AMOUNT_FORMAT = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00"));
|
||||
|
||||
private String formatAmount(BigDecimal amount) {
|
||||
return amount == null ? "" : AMOUNT_FORMAT.get().format(amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较产品信息列表并生成日志内容
|
||||
*
|
||||
|
|
@ -1157,7 +1165,7 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
|
|||
row.add(info.getPartnerName());
|
||||
row.add(info.getPartnerUserName());
|
||||
row.add(info.getContactWay());
|
||||
row.add(info.getEstimatedAmount() != null ? info.getEstimatedAmount().toString() : "");
|
||||
row.add(formatAmount(info.getEstimatedAmount()));
|
||||
row.add(DateUtil.format(info.getEstimatedOrderTime(), "yyyy-MM-dd"));
|
||||
row.add(formatterStr(info.getPoc()));
|
||||
row.add(info.getCompetitor());
|
||||
|
|
@ -1197,7 +1205,7 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
|
|||
}
|
||||
}
|
||||
|
||||
row.add(totalPrice.toString());
|
||||
row.add(formatAmount(totalPrice));
|
||||
|
||||
|
||||
for (int i = 0; i < maxWorkIndex; i++) {
|
||||
|
|
@ -1237,7 +1245,7 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
|
|||
row.add(productInfo.getProductBomCode());
|
||||
row.add(productInfo.getModel());
|
||||
row.add(productInfo.getQuantity() == null ? "" : productInfo.getQuantity().toString());
|
||||
row.add(productInfo.getAllPrice() == null ? "" : productInfo.getAllPrice().toString());
|
||||
row.add(productInfo.getAllPrice() == null ? "" : AMOUNT_FORMAT.get().format(productInfo.getAllPrice()));
|
||||
if (productInfo.getAllPrice() != null) {
|
||||
totalPrice = totalPrice.add(productInfo.getAllPrice());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import java.math.BigDecimal;
|
|||
import java.math.RoundingMode;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
|
@ -1372,13 +1373,13 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
totalPrice = processProducts(maintenanceList, maxMaintenanceService, row, totalPrice);
|
||||
int insertIndex=23;
|
||||
row.add(insertIndex++, wssDto.getQuantity());
|
||||
row.add(insertIndex++, wssDto.getAllPrice());
|
||||
row.add(insertIndex++, formatAmount(wssDto.getAllPrice()));
|
||||
row.add(insertIndex++, wssDto.getTaxRate());
|
||||
row.add(insertIndex++, wspDto.getQuantity());
|
||||
row.add(insertIndex++, wspDto.getAllPrice());
|
||||
row.add(insertIndex++, formatAmount(wspDto.getAllPrice()));
|
||||
row.add(insertIndex++, wspDto.getTaxRate());
|
||||
row.add(insertIndex++, lsDto.getQuantity());
|
||||
row.add(insertIndex++, lsDto.getAllPrice());
|
||||
row.add(insertIndex++, formatAmount(lsDto.getAllPrice()));
|
||||
row.add(insertIndex++, lsDto.getTaxRate());
|
||||
for (int i = 0; i < maxOne; i++) {
|
||||
ProjectProductInfo projectProductInfo = i < oneList.size() ? oneList.get(i) : null;
|
||||
|
|
@ -1394,19 +1395,16 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
// row.add(17, nVIDIADto.getAllPrice().toString());
|
||||
// row.add(StrUtil.toStringOrNull(info.getOrderChannel().equals(ProjectOrderInfo.OrderChannelEnum.TOTAL_GENERATION.getCode()) ?
|
||||
// info.getShipmentAmount() : info.getActualPurchaseAmount()));
|
||||
row.add(info.getShipmentAmount() != null ? info.getShipmentAmount() : "");
|
||||
row.add(totalPrice);
|
||||
row.add(info.getShipmentAmount() != null ? formatAmount(info.getShipmentAmount()) : "");
|
||||
row.add(formatAmount(totalPrice));
|
||||
//维保金额
|
||||
row.add(maintenancePrice);
|
||||
row.add(info.getSoftwareProjectProductInfoList() == null ? 0 :
|
||||
// info.getSoftwareProjectProductInfoList().stream()
|
||||
// .map(item -> item.getPrice().multiply(info.getDiscountFold() == null ||info.getOrderStatus().equals(ProjectOrderInfo.OrderStatus.WAIT_APPROVE.getCode()) ? BigDecimal.ONE : info.getDiscountFold()).setScale(2,RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(item.getQuantity())))
|
||||
// .reduce(BigDecimal.ZERO, BigDecimal::add)));
|
||||
info.getSoftwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
row.add(info.getHardwareProjectProductInfoList() == null ? 0 :
|
||||
info.getHardwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
row.add(info.getMaintenanceProjectProductInfoList() == null ? 0 :
|
||||
info.getMaintenanceProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
row.add(formatAmount(maintenancePrice));
|
||||
row.add(info.getSoftwareProjectProductInfoList() == null ? "" :
|
||||
formatAmount(info.getSoftwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
|
||||
row.add(info.getHardwareProjectProductInfoList() == null ? "" :
|
||||
formatAmount(info.getHardwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
|
||||
row.add(info.getMaintenanceProjectProductInfoList() == null ? "" :
|
||||
formatAmount(info.getMaintenanceProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
|
||||
dataList.add(row);
|
||||
}
|
||||
return dataList;
|
||||
|
|
@ -1494,6 +1492,13 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
return "0".equals(value) ? "否" : "是";
|
||||
}
|
||||
|
||||
/** DecimalFormat 非线程安全,导出接口可能被并发调用,故按线程持有,避免金额串位 */
|
||||
private static final ThreadLocal<DecimalFormat> AMOUNT_FORMAT = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00"));
|
||||
|
||||
private static String formatAmount(BigDecimal amount) {
|
||||
return amount == null ? "" : AMOUNT_FORMAT.get().format(amount);
|
||||
}
|
||||
|
||||
private static BigDecimal addProductRow(ProjectProductInfo productInfo, List<Object> row, BigDecimal totalPrice) {
|
||||
if (productInfo == null) {
|
||||
row.add("");
|
||||
|
|
@ -1506,7 +1511,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
row.add(productInfo.getProductBomCode());
|
||||
row.add(productInfo.getModel());
|
||||
row.add(productInfo.getQuantity() == null ? "" : productInfo.getQuantity());
|
||||
row.add(productInfo.getAllPrice() == null ? "" : productInfo.getAllPrice());
|
||||
row.add(formatAmount(productInfo.getAllPrice()));
|
||||
row.add(productInfo.getTaxRate() == null ? "" : productInfo.getTaxRate());
|
||||
if (productInfo.getAllPrice() != null) {
|
||||
totalPrice = totalPrice.add(productInfo.getAllPrice());
|
||||
|
|
@ -1526,7 +1531,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
row.add(index++, productInfo.getProductBomCode());
|
||||
row.add(index++, productInfo.getModel());
|
||||
row.add(index++, productInfo.getQuantity() == null ? "" : productInfo.getQuantity());
|
||||
row.add(index++, productInfo.getAllPrice() == null ? "" : productInfo.getAllPrice());
|
||||
row.add(index++, formatAmount(productInfo.getAllPrice()));
|
||||
row.add(index++, productInfo.getTaxRate() == null ? "" : productInfo.getTaxRate());
|
||||
return index;
|
||||
}
|
||||
|
|
@ -2450,7 +2455,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
|
||||
|
||||
|
||||
row.add(info.getShipmentAmount() != null ? info.getShipmentAmount() : "");
|
||||
row.add(info.getShipmentAmount() != null ? formatAmount(info.getShipmentAmount()) : "");
|
||||
row.add(info.getDiscountFold());
|
||||
BigDecimal softPrice = info.getSoftwareProjectProductInfoList() == null ? BigDecimal.ZERO :
|
||||
info.getSoftwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
|
@ -2458,12 +2463,12 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
info.getHardwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal mainPrice = info.getMaintenanceProjectProductInfoList() == null ? BigDecimal.ZERO :
|
||||
info.getMaintenanceProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
row.add(softPrice.add(hardPrice).add(mainPrice));
|
||||
row.add(softPrice);
|
||||
row.add(formatAmount(softPrice.add(hardPrice).add(mainPrice)));
|
||||
row.add(formatAmount(softPrice));
|
||||
|
||||
row.add(hardPrice);
|
||||
row.add(formatAmount(hardPrice));
|
||||
|
||||
row.add(mainPrice);
|
||||
row.add(formatAmount(mainPrice));
|
||||
row.add(DateUtil.format(info.getApproveTime(), "yyyy-MM-dd HH:mm:ss"));
|
||||
row.add(DateUtil.format(info.getOrderEndTime(), "yyyy-MM-dd"));
|
||||
row.add(info.getAgentName());
|
||||
|
|
@ -2572,11 +2577,11 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
|
|||
row.add(productInfo.getModel());
|
||||
row.add(productInfo.getProductDesc());
|
||||
row.add(productInfo.getQuantity() == null ? "" : productInfo.getQuantity());
|
||||
row.add(productInfo.getCataloguePrice() == null ? "" : productInfo.getCataloguePrice());
|
||||
row.add(formatAmount(productInfo.getCataloguePrice()));
|
||||
row.add(productInfo.getDiscount() == null ? "" : productInfo.getDiscount());
|
||||
row.add(productInfo.getPrice() == null ? "" : productInfo.getPrice());
|
||||
row.add(formatAmount(productInfo.getPrice()));
|
||||
row.add(orderInfo.getDiscountFold() == null ? "" : orderInfo.getDiscountFold());
|
||||
row.add(productInfo.getAllPrice() == null ? "" : productInfo.getAllPrice());
|
||||
row.add(formatAmount(productInfo.getAllPrice()));
|
||||
row.add(productInfo.getTaxRate() == null ? "" : productInfo.getTaxRate());
|
||||
data.add(row);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import javax.annotation.Resource;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
|
|
@ -59,6 +60,13 @@ public class QuotationServiceImpl implements IQuotationService {
|
|||
@Autowired
|
||||
private IQuotationProductInfoService quotationProductInfoService;
|
||||
|
||||
/** DecimalFormat 非线程安全,导出接口可能被并发调用,故按线程持有,避免金额串位 */
|
||||
private static final ThreadLocal<DecimalFormat> AMOUNT_FORMAT = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00"));
|
||||
|
||||
private String formatAmount(BigDecimal amount) {
|
||||
return amount == null ? "" : AMOUNT_FORMAT.get().format(amount);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private IProjectInfoService projectInfoService;
|
||||
|
|
@ -464,12 +472,12 @@ public class QuotationServiceImpl implements IQuotationService {
|
|||
row.add(item.getModel());
|
||||
row.add(item.getProductDesc());
|
||||
row.add(item.getQuantity());
|
||||
row.add(item.getCataloguePrice());
|
||||
row.add(formatAmount(item.getCataloguePrice()));
|
||||
row.add(item.getGuidanceDiscount()==null?"":item.getGuidanceDiscount().multiply(new BigDecimal("100")));
|
||||
row.add(item.getDiscount()==null?"":item.getDiscount().multiply(new BigDecimal("100")));
|
||||
row.add(item.getPrice());
|
||||
row.add(item.getAllPrice());
|
||||
row.add(item.getCatalogueAllPrice());
|
||||
row.add(formatAmount(item.getPrice()));
|
||||
row.add(formatAmount(item.getAllPrice()));
|
||||
row.add(formatAmount(item.getCatalogueAllPrice()));
|
||||
row.add(""); // CID信息
|
||||
row.add(item.getRemark());
|
||||
rows.add(row);
|
||||
|
|
@ -493,8 +501,8 @@ public class QuotationServiceImpl implements IQuotationService {
|
|||
subTotalRow1.add("");
|
||||
subTotalRow1.add("");
|
||||
subTotalRow1.add("");
|
||||
subTotalRow1.add(sumAllPrice);
|
||||
subTotalRow1.add(sumCatalogueAllPrice);
|
||||
subTotalRow1.add(formatAmount(sumAllPrice));
|
||||
subTotalRow1.add(formatAmount(sumCatalogueAllPrice));
|
||||
subTotalRow1.add("");
|
||||
subTotalRow1.add("");
|
||||
rows.add(subTotalRow1);
|
||||
|
|
@ -509,9 +517,8 @@ public class QuotationServiceImpl implements IQuotationService {
|
|||
subTotalRow2.add("");
|
||||
subTotalRow2.add("");
|
||||
subTotalRow2.add("");
|
||||
subTotalRow2.add("");
|
||||
subTotalRow2.add(sumAllPrice);
|
||||
subTotalRow2.add(sumCatalogueAllPrice);
|
||||
subTotalRow2.add(formatAmount(sumAllPrice));
|
||||
subTotalRow2.add(formatAmount(sumCatalogueAllPrice));
|
||||
subTotalRow2.add("");
|
||||
subTotalRow2.add("");
|
||||
rows.add(subTotalRow2);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
package com.ruoyi.sip.utils;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.ruoyi.sip.domain.InventoryInfo;
|
||||
import com.ruoyi.sip.domain.dto.PayableGoodsDetailDto;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 商品明细组装工具(应付单、应收单详情共用)
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class GoodsDetailUtils {
|
||||
|
||||
private GoodsDetailUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装商品明细,并在单据为冲红单(含税金额为负)时按单据金额缩放,保证明细与单据头一致。
|
||||
*
|
||||
* @param inventoryInfoList 库存明细(来源:入库单或出库单)
|
||||
* @param productType 产品类型
|
||||
* @param taxRate 税率(小数)
|
||||
* @param useOuterPrice 取价方式:true=出库价(应收),false=入库价(应付)
|
||||
* @param multiplyQuantity 是否 含税小计 = 单价 × 数量
|
||||
* @param billAmount 单据含税金额。为负数时按 单据金额/明细合计 的比例缩放明细;否则明细原样返回
|
||||
* @return 商品明细列表
|
||||
*/
|
||||
public static List<PayableGoodsDetailDto> build(List<InventoryInfo> inventoryInfoList,
|
||||
String productType,
|
||||
BigDecimal taxRate,
|
||||
boolean useOuterPrice,
|
||||
boolean multiplyQuantity,
|
||||
BigDecimal billAmount) {
|
||||
if (CollUtil.isEmpty(inventoryInfoList)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 按产品编码分组(同一单据可能存在多行SN明细)
|
||||
Map<String, List<InventoryInfo>> groupMap = inventoryInfoList.stream()
|
||||
.filter(item -> StringUtils.isNotEmpty(item.getProductCode()))
|
||||
.collect(Collectors.groupingBy(InventoryInfo::getProductCode, LinkedHashMap::new, Collectors.toList()));
|
||||
|
||||
List<PayableGoodsDetailDto> goodsDetailList = new ArrayList<>();
|
||||
for (Map.Entry<String, List<InventoryInfo>> entry : groupMap.entrySet()) {
|
||||
List<InventoryInfo> infos = entry.getValue();
|
||||
InventoryInfo first = infos.get(0);
|
||||
BigDecimal price = useOuterPrice ? first.getOuterPrice() : first.getInnerPrice();
|
||||
PayableGoodsDetailDto dto = new PayableGoodsDetailDto();
|
||||
dto.setProductType(productType);
|
||||
dto.setProductCode(entry.getKey());
|
||||
dto.setProductModel(first.getModel());
|
||||
dto.setProductDescription(first.getProductDesc());
|
||||
dto.setQuantity((long) infos.size());
|
||||
dto.setUnit("个");
|
||||
dto.setPrice(price);
|
||||
dto.setTaxRate(taxRate);
|
||||
if (multiplyQuantity) {
|
||||
dto.setAmountTotal(price == null ? BigDecimal.ZERO : price.multiply(new BigDecimal(infos.size())));
|
||||
} else {
|
||||
dto.setAmountTotal(infos.stream()
|
||||
.map(item -> useOuterPrice ? item.getOuterPrice() : item.getInnerPrice())
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
}
|
||||
goodsDetailList.add(dto);
|
||||
}
|
||||
return scaleToBillAmount(goodsDetailList, billAmount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 冲红单明细缩放:明细合计与单据金额不一致时(红冲单为部分/全部冲销),
|
||||
* 按 单据金额/明细合计 的比例缩放各行的含税小计与数量,使明细金额合计等于单据金额。
|
||||
* 仅处理负金额(冲红)单据,正数单据原样返回,避免影响正常单据的展示。
|
||||
*
|
||||
* @param goodsDetailList 商品明细
|
||||
* @param billAmount 单据含税金额
|
||||
* @return 缩放后的商品明细
|
||||
*/
|
||||
private static List<PayableGoodsDetailDto> scaleToBillAmount(List<PayableGoodsDetailDto> goodsDetailList,
|
||||
BigDecimal billAmount) {
|
||||
if (CollUtil.isEmpty(goodsDetailList) || billAmount == null || billAmount.signum() >= 0) {
|
||||
return goodsDetailList;
|
||||
}
|
||||
BigDecimal total = goodsDetailList.stream()
|
||||
.map(PayableGoodsDetailDto::getAmountTotal)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (total.signum() == 0) {
|
||||
return goodsDetailList;
|
||||
}
|
||||
BigDecimal ratio = billAmount.divide(total, 10, RoundingMode.HALF_UP);
|
||||
if (ratio.compareTo(BigDecimal.ONE) == 0) {
|
||||
return goodsDetailList;
|
||||
}
|
||||
for (PayableGoodsDetailDto dto : goodsDetailList) {
|
||||
if (dto.getAmountTotal() != null) {
|
||||
dto.setAmountTotal(dto.getAmountTotal().multiply(ratio).setScale(2, RoundingMode.HALF_UP));
|
||||
}
|
||||
if (dto.getQuantity() != null) {
|
||||
// 数量展示为实际撤回的数量(正数),金额为负以与红冲单据一致
|
||||
dto.setQuantity(BigDecimal.valueOf(dto.getQuantity()).multiply(ratio.abs())
|
||||
.setScale(0, RoundingMode.HALF_UP).longValue());
|
||||
}
|
||||
}
|
||||
return goodsDetailList;
|
||||
}
|
||||
}
|
||||
|
|
@ -118,9 +118,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="projectCode != null and projectCode != ''">and t3.project_code = #{projectCode}</if>
|
||||
<if test="projectName != null and projectName != ''">and t3.project_name = #{projectName}</if>
|
||||
<if test="orderCode != null and orderCode != ''">and t1.order_code like concat( #{orderCode},'%')</if>
|
||||
<if test="inventoryCode != null and inventoryCode != ''">and t1.inventory_code like concat(
|
||||
#{inventoryCode},'%')
|
||||
</if>
|
||||
<if test="inventoryCode != null and inventoryCode != ''">and t1.inventory_code like concat('%', #{inventoryCode}, '%')</if>
|
||||
<if test="productType != null and productType != ''">and t1.product_type = #{productType}</if>
|
||||
<if test="productCode != null and productCode != ''">and t1.product_code = #{productCode}</if>
|
||||
<if test="totalPriceWithTax != null ">and t1.total_price_with_tax = #{totalPriceWithTax}</if>
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<delete id="deleteByPaymentCode">
|
||||
delete from oms_payable_payment_detail where payment_bill_code = #{paymentBillCode}
|
||||
</delete>
|
||||
|
||||
<!-- 按应付单主键删除其付款明细(删除应付单时级联清理,避免孤儿数据) -->
|
||||
<delete id="deleteByPayableBillId" parameterType="Long">
|
||||
delete from oms_payable_payment_detail where payable_bill_id = #{payableBillId}
|
||||
</delete>
|
||||
<select id="list" resultType="com.ruoyi.sip.domain.OmsPayablePaymentDetail">
|
||||
|
||||
SELECT
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<select id="selectInventoryDeliveryList" parameterType="InventoryDelivery" resultMap="InventoryDeliveryResult">
|
||||
<include refid="selectInventoryDeliveryVo"/>
|
||||
<where>
|
||||
<!-- 默认排除已撤回记录;显式指定状态查询时(如查看已撤回记录)不排除 -->
|
||||
<if test="deliveryStatus == null or deliveryStatus == ''">and t1.delivery_status != 2</if>
|
||||
<!-- 撤回/作废后的记录需要保留可见,不再默认排除已撤回(delivery_status='2')记录;
|
||||
指定 deliveryStatus 时按其过滤 -->
|
||||
<if test="deliveryStatus != null and deliveryStatus != ''">and t1.delivery_status = #{deliveryStatus}</if>
|
||||
<if test="outerCode != null and outerCode != ''">and t1.outer_code = #{outerCode}</if>
|
||||
<if test="warehouseId != null ">and t1.warehouse_id = #{warehouseId}</if>
|
||||
|
|
@ -247,4 +247,113 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
and t1.id = #{id}
|
||||
</select>
|
||||
|
||||
<!--
|
||||
按产品 SN 判断是否存在审批中/审批通过的财务单据
|
||||
通过 SN -> oms_inventory_info.outer_code -> 应收/应付单 -> 明细 -> 收付款/收开票单
|
||||
返回去重后的 approve_status 集合(1=审批中,2=审批通过)
|
||||
-->
|
||||
<select id="selectApproveStatusBySn" resultType="java.lang.Integer">
|
||||
select distinct u.approve_status from (
|
||||
select t4.approve_status as approve_status
|
||||
from oms_inventory_info as i
|
||||
inner join oms_payable_bill as t2
|
||||
on i.outer_code = t2.inventory_code
|
||||
inner join oms_payable_payment_detail as t3
|
||||
on t2.id = t3.payable_bill_id
|
||||
inner join oms_payment_bill as t4
|
||||
on t3.payment_bill_code = t4.payment_bill_code
|
||||
where i.product_sn in
|
||||
<foreach collection="sns" item="sn" open="(" separator="," close=")">#{sn}</foreach>
|
||||
and ifnull(t4.approve_status,0) in (1,2)
|
||||
union all
|
||||
select t4.approve_status as approve_status
|
||||
from oms_inventory_info as i
|
||||
inner join oms_payable_bill as t2
|
||||
on i.outer_code = t2.inventory_code
|
||||
inner join oms_payable_ticket_detail as t3
|
||||
on t2.id = t3.payable_bill_id
|
||||
inner join oms_ticket_bill as t4
|
||||
on t3.ticket_bill_code = t4.ticket_bill_code
|
||||
where i.product_sn in
|
||||
<foreach collection="sns" item="sn" open="(" separator="," close=")">#{sn}</foreach>
|
||||
and ifnull(t4.approve_status,0) in (1,2)
|
||||
union all
|
||||
select t4.approve_status as approve_status
|
||||
from oms_inventory_info as i
|
||||
inner join oms_receivable_bill as t2
|
||||
on i.outer_code = t2.inventory_code
|
||||
inner join oms_receivable_receipt_detail as t3
|
||||
on t2.id = t3.receivable_bill_id
|
||||
inner join oms_receipt_bill as t4
|
||||
on t3.receipt_bill_code = t4.receipt_bill_code
|
||||
where i.product_sn in
|
||||
<foreach collection="sns" item="sn" open="(" separator="," close=")">#{sn}</foreach>
|
||||
and ifnull(t4.approve_status,0) in (1,2)
|
||||
union all
|
||||
select t4.approve_status as approve_status
|
||||
from oms_inventory_info as i
|
||||
inner join oms_receivable_bill as t2
|
||||
on i.outer_code = t2.inventory_code
|
||||
inner join oms_receivable_invoice_detail as t3
|
||||
on t2.id = t3.receivable_bill_id
|
||||
inner join oms_invoice_bill as t4
|
||||
on t3.invoice_bill_code = t4.invoice_bill_code
|
||||
where i.product_sn in
|
||||
<foreach collection="sns" item="sn" open="(" separator="," close=")">#{sn}</foreach>
|
||||
and ifnull(t4.approve_status,0) in (1,2)
|
||||
) u
|
||||
</select>
|
||||
|
||||
<!--
|
||||
按入库单号(inner_code)查询关联应付单的付款/收票审批状态
|
||||
INNER_PAY 厂商的应付单在入库时按 inner_code 生成,需通过 inner_code 关联
|
||||
-->
|
||||
<select id="selectPayableApproveStatusByInnerCodes" resultType="java.lang.Integer">
|
||||
select distinct u.approve_status from (
|
||||
select t4.approve_status as approve_status
|
||||
from oms_payable_bill as t2
|
||||
inner join oms_payable_payment_detail as t3
|
||||
on t2.id = t3.payable_bill_id
|
||||
inner join oms_payment_bill as t4
|
||||
on t3.payment_bill_code = t4.payment_bill_code
|
||||
where t2.inventory_code in
|
||||
<foreach collection="innerCodes" item="code" open="(" separator="," close=")">#{code}</foreach>
|
||||
and ifnull(t4.approve_status,0) in (1,2)
|
||||
union all
|
||||
select t4.approve_status as approve_status
|
||||
from oms_payable_bill as t2
|
||||
inner join oms_payable_ticket_detail as t3
|
||||
on t2.id = t3.payable_bill_id
|
||||
inner join oms_ticket_bill as t4
|
||||
on t3.ticket_bill_code = t4.ticket_bill_code
|
||||
where t2.inventory_code in
|
||||
<foreach collection="innerCodes" item="code" open="(" separator="," close=")">#{code}</foreach>
|
||||
and ifnull(t4.approve_status,0) in (1,2)
|
||||
) u
|
||||
</select>
|
||||
|
||||
<!-- 统计某出库单下除当前记录外仍在途(delivery_status='1' 已发货未撤回)的发货记录数 -->
|
||||
<select id="countOtherActiveDeliveryByOuterCode" resultType="java.lang.Long">
|
||||
select count(1)
|
||||
from oms_inventory_delivery t1
|
||||
where t1.outer_code = #{outerCode}
|
||||
and t1.id <> #{excludeId}
|
||||
and t1.delivery_status = '1'
|
||||
</select>
|
||||
|
||||
<!-- 统计某个入库单号(inner_code)对应的库存明细总数 -->
|
||||
<select id="countInventoryByInnerCode" resultType="java.lang.Long">
|
||||
select count(1)
|
||||
from oms_inventory_info
|
||||
where inner_code = #{innerCode}
|
||||
</select>
|
||||
|
||||
<!-- 查询某出库单最近一次撤回的时间(该出库单从未撤回过时返回 null) -->
|
||||
<select id="selectLastRecallTimeByOuterCode" resultType="java.util.Date">
|
||||
select max(t1.update_time)
|
||||
from oms_inventory_delivery t1
|
||||
where t1.outer_code = #{outerCode}
|
||||
and t1.delivery_status = '2'
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -121,6 +121,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
select product_sn from oms_inventory_delivery_detail where delivery_id = #{id}
|
||||
)
|
||||
</select>
|
||||
<!-- 按出库单号查库存明细(通过发货记录+SN明细反查,撤回后 inventory_info.outer_code 被清空时仍可用) -->
|
||||
<select id="listByOuterCodeViaDelivery" resultType="com.ruoyi.sip.domain.InventoryInfo">
|
||||
<include refid="selectInventoryInfoVo"/>
|
||||
where t1.product_sn in (
|
||||
select dd.product_sn
|
||||
from oms_inventory_delivery d
|
||||
inner join oms_inventory_delivery_detail dd on d.id = dd.delivery_id
|
||||
where d.outer_code = #{outerCode}
|
||||
)
|
||||
</select>
|
||||
<select id="selectInventoryInfoByOrderCode" resultType="com.ruoyi.sip.domain.InventoryInfo">
|
||||
<include refid="selectInventoryInfoVo"/>
|
||||
where t1.outer_code in (
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
t1.product_code,
|
||||
t1.quantity,
|
||||
t1.outer_status,
|
||||
t1.delivery_status,
|
||||
t1.order_code,
|
||||
t1.contact_person,
|
||||
t1.contact_phone,
|
||||
|
|
|
|||
|
|
@ -342,4 +342,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
delete from oms_payable_ticket_detail where ticket_bill_code = #{code}
|
||||
</delete>
|
||||
|
||||
<!-- 按应付单主键删除其收票明细(删除应付单时级联清理,避免孤儿数据) -->
|
||||
<delete id="deleteByPayableBillId" parameterType="Long">
|
||||
delete from oms_payable_ticket_detail where payable_bill_id = #{payableBillId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -164,4 +164,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- 按应付单主键删除其收票计划(删除应付单时级联清理,避免孤儿数据) -->
|
||||
<delete id="deleteByPayableBillId" parameterType="Long">
|
||||
delete from oms_payable_ticket_plan where payable_bill_id = #{payableBillId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -67,7 +67,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="partnerCode != null and partnerCode != ''"> and t1.partner_code = #{partnerCode}</if>
|
||||
<if test="partnerName != null and partnerName != ''"> and t1.partner_name like concat('%', #{partnerName}, '%')</if>
|
||||
<if test="orderCode != null and orderCode != ''"> and t1.order_code = #{orderCode}</if>
|
||||
<if test="inventoryCode != null and inventoryCode != ''"> and t1.inventory_code = #{inventoryCode}</if>
|
||||
<if test="inventoryCode != null and inventoryCode != ''"> and t1.inventory_code like concat('%', #{inventoryCode}, '%')</if>
|
||||
<if test="productType != null and productType != ''"> and t1.product_type = #{productType}</if>
|
||||
<if test="productCode != null and productCode != ''"> and t1.product_code = #{productCode}</if>
|
||||
<if test="createBy != null and createBy != ''"> and t1.create_by = #{createBy}</if>
|
||||
|
|
|
|||
|
|
@ -217,4 +217,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- 按应收单主键删除其开票明细(删除应收单时级联清理,避免孤儿数据) -->
|
||||
<delete id="deleteByReceivableBillId" parameterType="Long">
|
||||
delete from oms_receivable_invoice_detail where receivable_bill_id = #{receivableBillId}
|
||||
</delete>
|
||||
</mapper>
|
||||
|
|
@ -99,4 +99,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByReceivableBillId" parameterType="Long">
|
||||
delete from oms_receivable_invoice_plan where receivable_bill_id = #{receivableBillId}
|
||||
</delete>
|
||||
</mapper>
|
||||
|
|
@ -219,4 +219,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
from oms_receivable_receipt_detail
|
||||
where receipt_bill_code = #{receiptBillCode}
|
||||
</delete>
|
||||
|
||||
<!-- 按应收单主键删除其收款明细(删除应收单时级联清理,避免孤儿数据) -->
|
||||
<delete id="deleteByReceivableBillId" parameterType="Long">
|
||||
delete from oms_receivable_receipt_detail where receivable_bill_id = #{receivableBillId}
|
||||
</delete>
|
||||
</mapper>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
-- =============================================================
|
||||
-- 发货「撤回 / 作废」性能优化:补充缺失索引
|
||||
-- 功能说明:
|
||||
-- 撤回 / 作废会按出库单号、入库单号、SN 等维度反复查询应收 / 应付单及其明细,
|
||||
-- 相关列缺少索引时会走全表扫描(例如 oms_inventory_delivery_detail 原先只有主键索引,
|
||||
-- 按 delivery_id 查询需要扫描 4.8 万行,单次耗时约 43ms),导致撤回过程明显变慢。
|
||||
-- 本脚本仅新增二级索引,不改表结构、不改动任何数据。
|
||||
-- 测试库实测效果:
|
||||
-- 财务单据审批状态检查 193ms -> 35ms
|
||||
-- listByOuterCodeViaDelivery 43ms -> 20ms
|
||||
-- 说明:
|
||||
-- 使用 ALGORITHM=INPLACE, LOCK=NONE 在线加索引,过程中不锁表;
|
||||
-- 若目标环境 MySQL 版本不支持该子句,可去掉后执行(表数据量大时注意选择低峰期)。
|
||||
-- =============================================================
|
||||
|
||||
-- 1. 发货记录明细:撤回时按发货记录、SN 反复查询
|
||||
ALTER TABLE `oms_inventory_delivery_detail` ADD INDEX `idx_delivery_id` (`delivery_id`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE `oms_inventory_delivery_detail` ADD INDEX `idx_product_sn` (`product_sn`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- 2. 应收 / 应付单:按出库单号、入库单号查询
|
||||
ALTER TABLE `oms_payable_bill` ADD INDEX `idx_inventory_code` (`inventory_code`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE `oms_receivable_bill` ADD INDEX `idx_inventory_code` (`inventory_code`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- 3. 四类财务单据明细:审批状态检查时按关联单号(付款单/收票单/收款单/开票单)查询
|
||||
ALTER TABLE `oms_payable_payment_detail` ADD INDEX `idx_payment_bill_code` (`payment_bill_code`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE `oms_payable_ticket_detail` ADD INDEX `idx_ticket_bill_code` (`ticket_bill_code`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE `oms_receivable_receipt_detail` ADD INDEX `idx_receipt_bill_code` (`receipt_bill_code`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
ALTER TABLE `oms_receivable_invoice_detail` ADD INDEX `idx_invoice_bill_code` (`invoice_bill_code`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- 4. 出库单:按合同号统计有效出库单数量(撤回后回退订单出库状态时使用)
|
||||
ALTER TABLE `oms_inventory_outer` ADD INDEX `idx_order_code` (`order_code`), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
Loading…
Reference in New Issue