feat: 完成发货撤回优化、财务单据清理与多模块功能完善

本次提交完成了一系列核心功能优化与修复:
1.  **发货流程优化**:实现发货撤回校验、发货状态展示与控制,限制一次性发满剩余应发数量,新增通过出库单号反查库存明细的方法
2.  **财务单据优化**:新增多级财务单据级联删除方法,修复应收/应付单搜索模糊匹配问题,实现红冲单负数金额高亮展示
3.  **前端页面优化**:新增发货状态标签、应收/应付单商品明细展示,修复产品表单预装系统类型显示逻辑,优化文件上传非必填校验
4.  **性能优化**:为高频查询表添加二级索引优化撤回操作性能
5.  **工具类新增**:新增商品明细组装工具类,支持冲红单明细金额缩放适配
6.  **业务逻辑修复**:修复订单撤单时的应收应付处理逻辑,移除冗余的权限默认过滤,新增重复撤回拦截校验
dev_1.0.3
kangwenjing 2026-09-20 17:52:51 +08:00
parent 054c0a1789
commit 9cc5c64fee
53 changed files with 2159 additions and 804 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -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) { export function exportDelivery(query) {
return request({ return request({

View File

@ -41,14 +41,14 @@
<!-- @keyup.enter.native="handleQuery"--> <!-- @keyup.enter.native="handleQuery"-->
<!-- />--> <!-- />-->
<!-- </el-form-item>--> <!-- </el-form-item>-->
<!-- <el-form-item label="出入库单号" prop="inventoryCode">--> <el-form-item label="出入库单号" prop="inventoryCode">
<!-- <el-input--> <el-input
<!-- v-model="queryParams.inventoryCode"--> v-model="queryParams.inventoryCode"
<!-- placeholder="请输入出入库单号"--> placeholder="请输入出入库单号"
<!-- clearable--> clearable
<!-- @keyup.enter.native="handleQuery"--> @keyup.enter.native="handleQuery"
<!-- />--> />
<!-- </el-form-item>--> </el-form-item>
<el-form-item label="产品类型" prop="productType"> <el-form-item label="产品类型" prop="productType">
<el-select v-model="queryParams.productType" placeholder="请选择产品类型" clearable> <el-select v-model="queryParams.productType" placeholder="请选择产品类型" clearable>
<el-option <el-option
@ -122,7 +122,7 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </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 type="selection" width="50" />
<el-table-column label="项目编号" align="center" prop="projectCode" width="120" /> <el-table-column label="项目编号" align="center" prop="projectCode" width="120" />
<el-table-column label="项目名称" align="center" prop="projectName" width="260" /> <el-table-column label="项目名称" align="center" prop="projectName" width="260" />
@ -467,6 +467,15 @@ export default {
} }
return {}; 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) { timeProcessing(value) {
if (value === null || value === undefined || value === '') return ''; if (value === null || value === undefined || value === '') return '';
@ -476,8 +485,13 @@ export default {
}; };
</script> </script>
<style scoped> <style scoped lang="scss">
.operation-column .el-button--text:hover { .operation-column .el-button--text:hover {
color: red !important; color: red !important;
} }
/* 红冲单负数金额显示红色 */
::v-deep .amount-negative {
color: #f56c6c;
font-weight: bold;
}
</style> </style>

View File

@ -1,22 +1,19 @@
<template> <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"> <div v-if="loading" class="loading-spinner">
<i class="el-icon-loading"></i> <i class="el-icon-loading"></i>
</div> </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 <div v-else-if="hasUploaded" class="receipt-view">
v-for="attachment in attachments" <el-card v-for="attachment in activeAttachments" :key="attachment.id" class="receipt-view-card">
:key="attachment.id"
:timestamp="parseTime(attachment.createTime, '{y}-{m}-{d} {h}:{i}:{s}')"
placement="top"
>
<el-card>
<div class="receipt-card-content">
<div class="receipt-details"> <div class="receipt-details">
<div class="detail-item"> <div class="detail-item">
<span class="item-label">支付方式</span> <span class="item-label">支付方式</span>
@ -32,40 +29,20 @@
v-if="!isPdf(attachment.filePath)" v-if="!isPdf(attachment.filePath)"
:src="getImageUrl(attachment.filePath)" :src="getImageUrl(attachment.filePath)"
:preview-src-list="previewList" :preview-src-list="previewList"
style="width: 200px; height: 150px;" style="width: 100%; height: 100%;"
fit="contain" fit="contain"
></el-image> ></el-image>
<div v-else-if="pdfUrls[attachment.filePath]" class="pdf-thumbnail-container" @click="openPdfPreview(pdfUrls[attachment.filePath])"> <div
<iframe :src="pdfUrls[attachment.filePath]" width="100%" height="150px" frameborder="0"></iframe> 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"> <div class="pdf-hover-overlay">
<i class="el-icon-zoom-in"></i> <i class="el-icon-zoom-in"></i>
</div> </div>
</div> </div>
<div v-if="attachment.delFlag === '2'" class="void-overlay"></div>
</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> </div>
<div class="detail-item"> <div class="detail-item">
@ -76,37 +53,19 @@
<span class="item-label">备注</span> <span class="item-label">备注</span>
<span class="item-value">{{ attachment.remark }}</span> <span class="item-value">{{ attachment.remark }}</span>
</div> </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> </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-card>
</el-timeline-item>
</el-timeline>
<el-empty v-else :description="'暂无' + titleText"></el-empty>
</div> </div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
<!-- PDF Preview Dialog --> <!-- 未上传上传表单整个弹窗区域均可拖拽上传拖拽事件在 document 上统一处理 -->
<el-dialog <div v-else class="upload-area">
: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"
>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form :model="uploadForm" ref="uploadForm" label-width="120px" size="medium" > <el-form :model="uploadForm" ref="uploadForm" label-width="120px" size="medium" >
@ -121,20 +80,35 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="paymentData.paymentBillType==='FROM_PAYABLE'?'回执单': '退款图'" required> <el-form-item :label="paymentData.paymentBillType==='FROM_PAYABLE'?'回执单': '退款图'" required>
<div style="display: flex; flex-direction: column; align-items: flex-start;"> <div
<el-upload ref="pasteZone"
ref="upload" class="paste-zone"
action="#" :class="{ 'is-focused': pasteZoneFocused, 'is-dragover': dragOver, 'is-selected': !!uploadForm.file }"
:auto-upload="false" tabindex="0"
:on-change="handleFileChange" @click="chooseFile"
:on-remove="handleFileRemove" @focus="pasteZoneFocused = true"
:show-file-list="false" @blur="pasteZoneFocused = false"
accept=".jpg,.jpeg,.png,.pdf"
> >
<el-button size="small" type="primary" icon="el-icon-upload2">{{ uploadForm.file ? '重新上传' : '点击上传' }}</el-button> <template v-if="uploadForm.file">
</el-upload> <img v-if="previewUrl && !isPreviewPdf" :src="previewUrl" class="paste-zone-thumb" />
<div class="el-upload__tip" style="line-height: 1.5; margin-top: 5px;">支持上传PNGJPGPDF文件格式</div> <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> </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>
<el-form-item label="含税总价"> <el-form-item label="含税总价">
<span>{{ paymentData.totalPriceWithTax }}</span> <span>{{ paymentData.totalPriceWithTax }}</span>
@ -153,32 +127,63 @@
</el-form> </el-form>
</el-col> </el-col>
<el-col :span="12"> <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 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" /> <img v-if="!isPreviewPdf" :src="previewUrl" class="preview-image" />
<iframe v-else :src="previewUrl" width="100%" height="100%" frameborder="0"></iframe> <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>
<div v-else class="preview-placeholder"> <div v-else class="preview-placeholder">
<div class="placeholder-icon"> <div class="placeholder-icon">
<i class="el-icon-picture"></i> <i class="el-icon-picture"></i>
</div> </div>
<div class="placeholder-text">点击图片进入预览</div> <div class="placeholder-text">尚未选择文件</div>
</div> </div>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
</div>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitNewUpload"></el-button> <template v-if="!loading && !hasUploaded">
<el-button @click="closeUploadDialog"></el-button> <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> </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>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import {deleteFile, getPaymentAttachments, uploadPaymentAttachment} from "@/api/finance/payment"; import { getPaymentAttachments, uploadPaymentAttachment } from "@/api/finance/payment";
import request from '@/utils/request'; import request from '@/utils/request';
export default { export default {
@ -190,7 +195,7 @@ export default {
}, },
paymentData: { paymentData: {
type: Object, type: Object,
default: () => {}, default: () => ({}),
}, },
dicts: { dicts: {
type: Object, type: Object,
@ -200,9 +205,8 @@ export default {
data() { data() {
return { return {
loading: false, loading: false,
//
attachments: [], attachments: [],
// Upload Dialog Data
uploadDialogVisible: false,
uploadForm: { uploadForm: {
paymentMethod: '', paymentMethod: '',
confirmPrice: '', confirmPrice: '',
@ -211,7 +215,12 @@ export default {
}, },
previewUrl: '', previewUrl: '',
isPreviewPdf: false, isPreviewPdf: false,
// PDF Preview Data pasteZoneFocused: false,
dragOver: false,
// /
uploading: false,
fetchingUrl: false,
// PDF
pdfUrls: {}, pdfUrls: {},
pdfPreviewVisible: false, pdfPreviewVisible: false,
currentPdfUrl: '', currentPdfUrl: '',
@ -226,31 +235,68 @@ export default {
this.$emit("update:visible", val); 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() { titleText() {
return this.paymentData && this.paymentData.paymentBillType === 'REFUND' ? '退款图' : '回执单'; 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: { watch: {
visible(val) { visible(val) {
if (val && this.paymentData) { if (val) {
this.initUploadForm();
this.fetchAttachments(); 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: { 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() { fetchAttachments() {
if (!this.paymentData.id) return; if (!this.paymentData.id) {
this.loading = false;
return;
}
this.loading = true; this.loading = true;
getPaymentAttachments(this.paymentData.id, { type: 'payment' }) getPaymentAttachments(this.paymentData.id, { type: 'payment' })
.then(response => { .then(response => {
@ -265,6 +311,40 @@ export default {
this.loading = false; 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() { loadPdfPreviews() {
this.attachments.forEach(att => { this.attachments.forEach(att => {
if (this.isPdf(att.filePath) && !this.pdfUrls[att.filePath]) { if (this.isPdf(att.filePath) && !this.pdfUrls[att.filePath]) {
@ -286,17 +366,6 @@ export default {
this.currentPdfUrl = url; this.currentPdfUrl = url;
this.pdfPreviewVisible = true; 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) { downloadFile(attachment) {
const link = document.createElement('a'); const link = document.createElement('a');
link.href = this.getImageUrl(attachment.filePath); link.href = this.getImageUrl(attachment.filePath);
@ -306,34 +375,171 @@ export default {
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
}, },
handleClose() { /** 点击区域选择文件 */
this.attachments = []; chooseFile() {
// Clean up object URLs if (this.loading || this.hasUploaded || this.fetchingUrl) return;
Object.values(this.pdfUrls).forEach(url => URL.revokeObjectURL(url)); const input = this.$refs.fileInput;
this.pdfUrls = {}; if (input) {
input.click();
}
}, },
// New Upload Dialog Methods /** 选择文件回调 */
openUploadDialog() { handleFileInputChange(event) {
this.uploadForm = { const files = event.target.files;
paymentMethod: this.paymentData.paymentMethod, if (files && files.length > 0) {
confirmPrice: '', this.processFile(files[0]);
remark: '', }
file: null // value change
}; event.target.value = '';
this.previewUrl = '';
this.isPreviewPdf = false;
this.uploadDialogVisible = true;
}, },
closeUploadDialog() { /** 全局 dragover阻止浏览器默认打开文件并给出可放置提示Edge/Chrome/Firefox 通用) */
this.uploadDialogVisible = false; handleGlobalDragOver(event) {
this.uploadForm.file = null; event.preventDefault();
this.previewUrl = ''; if (event.dataTransfer) {
try {
event.dataTransfer.dropEffect = 'copy';
} catch (e) {
// dropEffect
}
}
if (this.canAcceptFile) {
this.dragOver = true;
}
}, },
handleFileChange(file) { /** 全局 dragleave仅当拖出窗口时清除高亮 */
const isLt2M = file.size / 1024 / 1024 < 2; handleGlobalDragLeave(event) {
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.raw.type); 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 格式!'); this.$message.error('上传文件只能是 JPG/PNG/PDF 格式!');
return; return;
} }
@ -342,16 +548,25 @@ export default {
return; return;
} }
this.uploadForm.file = file.raw; if (this.previewUrl) {
this.isPreviewPdf = file.raw.type === 'application/pdf'; 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.uploadForm.file = null;
this.previewUrl = ''; this.previewUrl = '';
this.isPreviewPdf = false;
}, },
submitNewUpload() { submitNewUpload() {
if (this.uploading || this.fetchingUrl) return;
if (!this.uploadForm.file) { if (!this.uploadForm.file) {
this.$message.warning("请选择要上传的文件"); this.$message.warning("请选择要上传的文件");
return; return;
@ -366,18 +581,22 @@ export default {
} }
const formData = new FormData(); 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("relatedBillId", this.paymentData.id);
formData.append("remark", this.uploadForm.remark); formData.append("remark", this.uploadForm.remark);
this.uploading = true;
uploadPaymentAttachment(formData) uploadPaymentAttachment(formData)
.then(response => { .then(() => {
this.$message.success("上传成功"); this.$message.success("上传成功");
this.closeUploadDialog();
this.fetchAttachments(); this.fetchAttachments();
}) })
.catch(error => { .catch(() => {
this.$message.error("上传失败"); this.$message.error("上传失败");
})
.then(() => {
this.uploading = false;
}); });
}, },
}, },
@ -385,17 +604,19 @@ export default {
</script> </script>
<style scoped> <style scoped>
.receipt-dialog-body {
max-height: 60vh;
overflow-y: auto;
}
.loading-spinner { .loading-spinner {
text-align: center; text-align: center;
font-size: 24px; font-size: 24px;
padding: 20px; padding: 40px;
} }
.receipt-card-content { /* 回执单查看 */
.receipt-view {
display: flex; display: flex;
flex-direction: column;
gap: 15px;
}
.receipt-view-card {
margin-bottom: 0;
} }
.receipt-details { .receipt-details {
flex-grow: 1; flex-grow: 1;
@ -417,104 +638,22 @@ export default {
} }
.image-wrapper { .image-wrapper {
position: relative; position: relative;
width: 200px; width: 320px;
min-height: 150px; height: 260px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid #DCDFE6; border: 1px solid #DCDFE6;
border-radius: 4px; 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; overflow: hidden;
} }
.preview-content { .receipt-view-footer {
width: 100%; margin-top: 10px;
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;
} }
.pdf-thumbnail-container { .pdf-thumbnail-container {
position: relative; position: relative;
width: 100%; width: 100%;
height: 150px; height: 100%;
cursor: pointer; cursor: pointer;
} }
.pdf-hover-overlay { .pdf-hover-overlay {
@ -531,14 +670,144 @@ export default {
transition: opacity 0.3s; transition: opacity 0.3s;
color: #fff; color: #fff;
font-size: 24px; 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; pointer-events: auto;
} }
.pdf-thumbnail-container:hover .pdf-hover-overlay { .pdf-thumbnail-container:hover .pdf-hover-overlay {
opacity: 1; 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> </style>

View File

@ -92,6 +92,27 @@
<div style="padding: 20px"> <div style="padding: 20px">
<el-tabs v-model="activeTab"> <el-tabs v-model="activeTab">
<el-tab-pane label="明细" name="details"> <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-divider content-position="left">销售-收款单</el-divider>
<el-table :data="formData.detailList" style="width: 100%" show-summary :summary-method="getSummaries"> <el-table :data="formData.detailList" style="width: 100%" show-summary :summary-method="getSummaries">
<el-table-column type="index" label="序号" width="50"></el-table-column> <el-table-column type="index" label="序号" width="50"></el-table-column>

View File

@ -33,6 +33,14 @@
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </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-form-item label="产品类型" prop="productType">
<el-select v-model="queryParams.productType" placeholder="请选择产品类型" clearable> <el-select v-model="queryParams.productType" placeholder="请选择产品类型" clearable>
<el-option <el-option
@ -95,7 +103,7 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </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 type="selection" width="50" />
<el-table-column label="项目编号" align="center" prop="projectCode" width="120" /> <el-table-column label="项目编号" align="center" prop="projectCode" width="120" />
<el-table-column label="项目名称" align="center" prop="projectName" width="200" /> <el-table-column label="项目名称" align="center" prop="projectName" width="200" />
@ -204,6 +212,7 @@ export default {
projectName: null, projectName: null,
receivableBillCode: null, receivableBillCode: null,
partnerName: null, partnerName: null,
inventoryCode: null,
productType: null, productType: null,
collectionStatus: null, collectionStatus: null,
createTimeStart: null, createTimeStart: null,
@ -329,6 +338,15 @@ export default {
this.getList(); // Refresh the list 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) { timeProcessing(value) {
if (value === null || value === undefined || value === '') return ''; if (value === null || value === undefined || value === '') return '';
@ -338,8 +356,13 @@ export default {
}; };
</script> </script>
<style scoped> <style scoped lang="scss">
.operation-column .el-button--text:hover { .operation-column .el-button--text:hover {
color: red !important; color: red !important;
} }
/* 红冲单负数金额显示红色 */
::v-deep .amount-negative {
color: #f56c6c;
font-weight: bold;
}
</style> </style>

View File

@ -1,9 +1,9 @@
<template> <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"> custom-class="apply-payment-dialog">
<el-row :gutter="20"> <el-row :gutter="20">
<!-- Left Side: Form Data --> <!-- Left Side: Form Data -->
<el-col :span="12"> <el-col :span="24">
<div class="form-tip">请选择客户的支付方式并确认客户打款的账户信息提交至财务审批</div> <div class="form-tip">请选择客户的支付方式并确认客户打款的账户信息提交至财务审批</div>
<el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small"> <el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small">
<el-form-item label="支付方式" prop="receiptMethod"> <el-form-item label="支付方式" prop="receiptMethod">
@ -19,36 +19,22 @@
<el-form-item label="账户名称" prop="receiptAccountName"> <el-form-item label="账户名称" prop="receiptAccountName">
<el-input v-model="form.receiptAccountName" placeholder="请输入账户名称"/> <el-input v-model="form.receiptAccountName" placeholder="请输入账户名称"/>
</el-form-item> </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-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>
<el-form-item label="银行行号" prop="bankNumber"> <el-form-item label="银行行号" prop="bankNumber" class="readonly-item">
<el-input v-model="form.bankNumber" placeholder="请输入银行行号"/> <el-input v-model="form.bankNumber" :disabled="true" placeholder="选择银行开户行后自动带出"/>
</el-form-item> </el-form-item>
<el-form-item label="银行账号" prop="receiptBankNumber" class="readonly-item">
<!-- New Field: Client Payment Image Upload --> <el-input v-model="form.receiptBankNumber" :disabled="true" placeholder="选择银行开户行后自动带出"/>
<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> </el-form-item>
<el-form-item label="收款金额" prop="totalPriceWithTax"> <el-form-item label="收款金额" prop="totalPriceWithTax">
@ -56,7 +42,7 @@
</el-form-item> </el-form-item>
<!-- New Field: Confirm Receipt Amount --> <!-- 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-input v-model="form.confirmAmount" placeholder="请输入确认收款金额"/>
</el-form-item> </el-form-item>
@ -66,26 +52,6 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-col> </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> </el-row>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
@ -133,9 +99,6 @@ export default {
bankNumber: [ bankNumber: [
{ required: true, message: "请输入银行行号", trigger: "blur" } { required: true, message: "请输入银行行号", trigger: "blur" }
], ],
file: [
{ required: true, message: "请上传客户付款图", trigger: "change" }
],
confirmAmount: [ confirmAmount: [
{ required: true, message: "请输入确认收款金额", trigger: "blur" } { required: true, message: "请输入确认收款金额", trigger: "blur" }
] ]
@ -149,12 +112,8 @@ export default {
totalPriceWithTax: null, totalPriceWithTax: null,
confirmAmount: null, confirmAmount: null,
remark: null, remark: null,
file: null,
fileName: '',
id: this.receiptData.id id: this.receiptData.id
}, }
previewUrl: '',
isPreviewPdf: false
}; };
}, },
watch: { watch: {
@ -165,19 +124,23 @@ export default {
this.form = { this.form = {
id: this.receiptData.id, id: this.receiptData.id,
receiptMethod: this.receiptData.receiptMethod, receiptMethod: this.receiptData.receiptMethod,
receiptAccountName: this.receiptData.receiptAccountName, receiptAccountName: this.receiptData.receiptAccountName || '紫光汇智信息技术有限公司',
receiptBankNumber: this.receiptData.receiptBankNumber, receiptBankNumber: this.receiptData.receiptBankNumber,
receiptBankOpenAddress: this.receiptData.receiptBankOpenAddress, receiptBankOpenAddress: this.receiptData.receiptBankOpenAddress,
bankNumber: this.receiptData.bankNumber, bankNumber: this.receiptData.bankNumber,
totalPriceWithTax: this.receiptData.totalPriceWithTax, totalPriceWithTax: this.receiptData.totalPriceWithTax,
confirmAmount: null, confirmAmount: this.receiptData.totalPriceWithTax,
remark: null, remark: null
file: null,
fileName: ''
}; };
} }
} }
}, },
computed: {
//
isConfirmAmountOver() {
return this.$calc.sub(this.form.confirmAmount, this.form.totalPriceWithTax) > 0;
}
},
methods: { methods: {
reset() { reset() {
this.form = { this.form = {
@ -188,12 +151,8 @@ export default {
bankNumber: null, bankNumber: null,
totalPriceWithTax: null, totalPriceWithTax: null,
confirmAmount: null, confirmAmount: null,
remark: null, remark: null
file: null,
fileName: ''
}; };
this.previewUrl = '';
this.isPreviewPdf = false;
if (this.$refs.form) { if (this.$refs.form) {
this.$refs.form.resetFields(); this.$refs.form.resetFields();
} }
@ -201,31 +160,22 @@ export default {
handleClose() { handleClose() {
this.$emit("update:visible", false); this.$emit("update:visible", false);
}, },
handleFileChange(file) { // (json)
const isLt10M = file.size / 1024 / 1024 < 10; handleBankInfoChange(label) {
const isAcceptedType = ['image/jpeg', 'image/png', 'application/pdf'].includes(file.raw.type); const bank = (this.dicts.bank_info || []).find(item => item.label === label);
if (!bank || !bank.value) {
if (!isAcceptedType) {
this.$modal.msgError('上传文件只能是 JPG/PNG/PDF 格式!');
// Remove file from upload list if needed, though we use show-file-list="false"
return; return;
} }
if (!isLt10M) { try {
this.$modal.msgError('上传文件大小不能超过 10MB!'); const bankInfo = JSON.parse(bank.value);
return; 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.$refs.form.validateField(['receiptBankNumber', 'bankNumber']);
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');
}, },
handleSubmit() { handleSubmit() {
if (this.$calc.sub(this.form.totalPriceWithTax,this.form.confirmAmount)!=0){ if (this.$calc.sub(this.form.totalPriceWithTax,this.form.confirmAmount)!=0){
@ -238,24 +188,17 @@ export default {
const formData = new FormData(); const formData = new FormData();
// Append regular fields // Append regular fields
Object.keys(this.form).forEach(key => { 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]); 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 => { applyReceipt(formData).then(response => {
this.$modal.msgSuccess("申请款提交成功"); this.$modal.msgSuccess("申请收款提交成功");
this.$emit("submit"); this.$emit("submit");
this.handleClose(); this.handleClose();
}).catch(error => { }).catch(error => {
console.error("申请款提交失败", error); console.error("申请款提交失败", error);
}); });
} }
}); });
@ -275,54 +218,21 @@ export default {
border-radius: 4px; border-radius: 4px;
} }
.preview-container { /* 只读展示字段:值使用深色文字,便于查看 */
width: 100%; .readonly-item ::v-deep .el-input.is-disabled .el-input__inner {
height: 500px; color: #303133;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #f5f7fa; background-color: #f5f7fa;
display: flex; -webkit-text-fill-color: #303133;
justify-content: center; cursor: default;
align-items: center;
overflow: hidden;
} }
.preview-content { .readonly-item ::v-deep .el-input.is-disabled .el-input__inner::placeholder {
width: 100%; color: #c0c4cc;
height: 100%; -webkit-text-fill-color: #c0c4cc;
display: flex;
justify-content: center;
align-items: center;
} }
.image-preview img { /* 确认收款金额大于收款金额时,值显示为红色 */
max-width: 100%; .over-amount-item ::v-deep .el-input__inner {
max-height: 100%; color: #f56c6c;
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;
} }
</style> </style>

View File

@ -249,7 +249,7 @@ export default {
ApplyPaymentDialog, ApplyPaymentDialog,
ApplyRefundDialog 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() { data() {
return { return {
// //

View File

@ -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="productCode" show-overflow-tooltip/>
<el-table-column label="产品型号" align="center" width="180" prop="model" 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" 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"> <el-table-column label="发货方式" align="center" width="100" prop="deliveryType">
<template slot-scope="scope"> <template slot-scope="scope">
<dict-tag :options="dict.type.delivery_type" :value="scope.row.deliveryType"/> <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"> <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width" fixed="right">
<template slot-scope="scope"> <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-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']">撤回 v-hasPermi="['inventory:delivery:recall']">撤回
</el-button> </el-button>
</template> </template>
@ -222,6 +228,16 @@ export default {
this.deliveryId = row.id; this.deliveryId = row.id;
this.viewOpen = true; 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) { handleRecall(row) {
const id = row.id; const id = row.id;

View File

@ -75,7 +75,8 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" min-width="90"> <el-table-column label="操作" align="center" min-width="90">
<template slot-scope="scope"> <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="danger" @click="handleDeleteOuter(scope.row)"></el-button>
<el-button size="mini" type="default" @click="handleConfirmOuter(scope.row)"></el-button> <el-button size="mini" type="default" @click="handleConfirmOuter(scope.row)"></el-button>
</div> </div>

View File

@ -437,6 +437,16 @@ export default {
return; 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) { if (this.isImported) {
this.snList.forEach(item => item.taxRate = this.taxRate) this.snList.forEach(item => item.taxRate = this.taxRate)
} }

View File

@ -43,7 +43,10 @@
<el-table-column label="仓库" prop="warehouseName" /> <el-table-column label="仓库" prop="warehouseName" />
<el-table-column v-if="!viewOnly" label="操作" align="center"> <el-table-column v-if="!viewOnly" label="操作" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<!-- 出库单已撤回/作废记录保留展示但不再允许发货需回到订单重新出库 -->
<el-tag v-if="form.deliveryStatus === '3'" type="danger"></el-tag>
<el-button <el-button
v-else
size="mini" size="mini"
type="success" type="success"
@click="handleDeliver(scope.row)" @click="handleDeliver(scope.row)"
@ -61,10 +64,16 @@
<el-table-column label="仓库" prop="warehouseName" /> <el-table-column label="仓库" prop="warehouseName" />
<el-table-column label="发货时间" prop="deliveryTime" /> <el-table-column label="发货时间" prop="deliveryTime" />
<el-table-column label="发货数量" prop="quantity" /> <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="发货人" prop="createByName" />
<el-table-column label="操作" align="center" width="200"> <el-table-column label="操作" align="center" width="200">
<template slot-scope="scope"> <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="danger" @click="handleDeleteDelivery(scope.row.id)"></el-button>
<el-button size="mini" type="primary" @click="handleConfirmDelivery(scope.row)"></el-button> <el-button size="mini" type="primary" @click="handleConfirmDelivery(scope.row)"></el-button>
</div> </div>
@ -72,7 +81,6 @@
<el-button size="mini" type="info" @click="handleViewDelivery(scope.row.id)"></el-button> <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" 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> </div>
</template> </template>
</el-table-column> </el-table-column>
@ -109,15 +117,18 @@
</el-dialog> </el-dialog>
<div slot="footer" class="dialog-footer"> <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> <el-button @click="handleCancel"> </el-button>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import {changeOuterStatus, getOuter, queryInfo} from '@/api/inventory/outer'; import { changeOuterStatus, getOuter, queryInfo } from '@/api/inventory/outer';
import { removeDelivery, updateDeliveryStatus, recallDelivery, recallApply } from '@/api/inventory/delivery'; import { removeDelivery, updateDeliveryStatus, recallDelivery, recallApply, checkRecallByDelivery } from '@/api/inventory/delivery';
import GenerateDeliveryForm from './GenerateDeliveryForm.vue'; import GenerateDeliveryForm from './GenerateDeliveryForm.vue';
import DeliveryDetail from '@/views/inventory/delivery/Detail.vue'; import DeliveryDetail from '@/views/inventory/delivery/Detail.vue';
import OuterRebackDetail from '@/views/approve/all/components/OuterRebackDetail.vue'; import OuterRebackDetail from '@/views/approve/all/components/OuterRebackDetail.vue';
@ -147,7 +158,8 @@ export default {
// //
recallSubmitOpen: false, recallSubmitOpen: false,
recallSubmitting: false, recallSubmitting: false,
recallSubmitDeliveryId: null, //
recallSubmitDeliveryIds: [],
recallSubmitForm: { recallSubmitForm: {
amountChanged: '', amountChanged: '',
reason: '' 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: { methods: {
open(id) { open(id) {
this.reset(); this.reset();
@ -168,7 +194,7 @@ export default {
this.form = response.data.inventoryOuter; this.form = response.data.inventoryOuter;
this.productList = response.data.productVoList || []; this.productList = response.data.productVoList || [];
this.deliveryList = response.data.deliveryList || []; 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.recallApplyOuterCode = '';
this.recallSubmitOpen = false; this.recallSubmitOpen = false;
this.recallSubmitting = false; this.recallSubmitting = false;
this.recallSubmitDeliveryId = null; this.recallSubmitDeliveryIds = [];
this.recallSubmitForm = { amountChanged: '', reason: '' }; this.recallSubmitForm = { amountChanged: '', reason: '' };
if (this.$refs.recallSubmitForm) { if (this.$refs.recallSubmitForm) {
this.$refs.recallSubmitForm.clearValidate(); this.$refs.recallSubmitForm.clearValidate();
@ -196,9 +222,11 @@ export default {
// //
refreshTables() { refreshTables() {
queryInfo(this.form.id).then(res => { queryInfo(this.form.id).then(res => {
// /
this.form = res.data.inventoryOuter || this.form;
this.productList = res.data.productVoList || []; this.productList = res.data.productVoList || [];
this.deliveryList = res.data.deliveryList || []; this.deliveryList = res.data.deliveryList || [];
this.showReturn = this.deliveryList.length <= 0; this.showReturn = this.activeDeliveries.length <= 0;
}); });
}, },
// //
@ -245,6 +273,16 @@ export default {
}); });
}).catch(() => {}); }).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) { handleViewDelivery(deliveryId) {
this.deliveryId = deliveryId; this.deliveryId = deliveryId;
@ -256,15 +294,69 @@ export default {
this.recallApplyOuterCode = row.outerCode; this.recallApplyOuterCode = row.outerCode;
this.recallApplyOpen = true; this.recallApplyOpen = true;
}, },
// / //
handleRecall(row) { // /
if (this.isToday(row.createTime)) { //
// // /
this.handleRecallDelivery(row); //
} else { async handleRecallBatch() {
// const rows = this.recallableDeliveries;
this.handleRecallNotToday(row); 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) { isToday(dateStr) {
@ -276,56 +368,36 @@ export default {
date.getMonth() === today.getMonth() && date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate(); 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() { handleRecallSubmitCancel() {
this.recallSubmitOpen = false; this.recallSubmitOpen = false;
this.recallSubmitting = false; this.recallSubmitting = false;
}, },
// //
submitRecallApply() { submitRecallApply() {
this.$refs.recallSubmitForm.validate(valid => { this.$refs.recallSubmitForm.validate(async valid => {
if (!valid) { if (!valid) {
return; return;
} }
const { amountChanged, reason } = this.recallSubmitForm; const { amountChanged, reason } = this.recallSubmitForm;
this.recallSubmitting = true; this.recallSubmitting = true;
this.$modal.loading(); this.$modal.loading();
recallApply(this.recallSubmitDeliveryId, reason.trim(), amountChanged) try {
.then(() => { for (const id of this.recallSubmitDeliveryIds) {
this.$modal.closeLoading(); // msg msg
const res = await recallApply(id, reason.trim(), amountChanged);
if (res && res.msg) {
this.$message.warning(res.msg);
}
}
this.$message.success('已提交出库撤回审批'); this.$message.success('已提交出库撤回审批');
this.recallSubmitOpen = false; this.recallSubmitOpen = false;
this.recallSubmitting = false;
this.refreshTables(); this.refreshTables();
}) } finally {
.catch(() => {
this.$modal.closeLoading(); this.$modal.closeLoading();
this.recallSubmitting = false; 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) { handleStatusChange(row, status) {
const actionText = status === '3' ? '确认接收' : '退回'; const actionText = status === '3' ? '确认接收' : '退回';

View File

@ -63,7 +63,11 @@
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="220"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="220">
<template slot-scope="scope"> <template slot-scope="scope">
<!-- <el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)"></el-button>--> <!-- <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-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> <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> </template>

View File

@ -189,7 +189,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </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-select v-model="form.preSystemType" placeholder="请选择预装系统类型" style="width: 100%">
<el-option <el-option
v-for="dict in dict.type.pre_system_type" v-for="dict in dict.type.pre_system_type"
@ -365,7 +365,8 @@ export default {
required: true, required: true,
trigger: "change", trigger: "change",
validator: (rule, value, callback) => { 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("预装系统类型不能为空")); callback(new Error("预装系统类型不能为空"));
} else { } else {
callback(); callback();
@ -638,6 +639,10 @@ export default {
this.form.localization = null; this.form.localization = null;
this.form.cpuBrand = null; this.form.cpuBrand = null;
this.form.cpuArchitecture = null; this.form.cpuArchitecture = null;
// -
if (isAccessory) {
this.form.preSystemType = null;
}
} }
} }
} }

View File

@ -71,4 +71,4 @@ unis:
enabled: false enabled: false
opportunity: opportunity:
integration: integration:
base-url: http://192.168.2.158:8080 base-url: http://localhost:8080

View File

@ -186,7 +186,7 @@ public class OmsReceiptBillController extends BaseController {
@Log(title = "申请收款", businessType = BusinessType.UPDATE) @Log(title = "申请收款", businessType = BusinessType.UPDATE)
@PostMapping("/applyReceipt") @PostMapping("/applyReceipt")
@ResponseBody @ResponseBody
public AjaxResult applyReceipt(OmsReceiptBill omsReceiptBill, @RequestParam("file") MultipartFile file) { public AjaxResult applyReceipt(OmsReceiptBill omsReceiptBill, @RequestParam(value = "file", required = false) MultipartFile file) {
try { try {
omsReceiptBillService.applyReceipt(omsReceiptBill, file); omsReceiptBillService.applyReceipt(omsReceiptBill, file);
} catch (IOException e) { } catch (IOException e) {

View File

@ -5,7 +5,6 @@ import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.sip.domain.InventoryDelivery; import com.ruoyi.sip.domain.InventoryDelivery;
import com.ruoyi.sip.domain.OmsInventoryDeliveryDetail; import com.ruoyi.sip.domain.OmsInventoryDeliveryDetail;
@ -48,10 +47,7 @@ public class VueDeliveryController extends BaseController {
@RequiresPermissions("inventory:delivery:list") @RequiresPermissions("inventory:delivery:list")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(InventoryDelivery inventoryDelivery) { public TableDataInfo list(InventoryDelivery inventoryDelivery) {
// 默认只查已发货,前端显式指定状态时按指定状态查询 // 撤回/作废后的记录需要保留可见:不再默认只查已发货,前端指定发货状态时按指定状态过滤
if (StringUtils.isEmpty(inventoryDelivery.getDeliveryStatus())) {
inventoryDelivery.setDeliveryStatus(InventoryDelivery.DeliveryStatusEnum.CONFIRM_DELIVERY.getCode());
}
if (!inventoryAuthService.authAll()) { if (!inventoryAuthService.authAll()) {
List<String> productCodeList = inventoryAuthService.authProductCode(); List<String> productCodeList = inventoryAuthService.authProductCode();
if (CollUtil.isEmpty(productCodeList)) { if (CollUtil.isEmpty(productCodeList)) {
@ -122,15 +118,47 @@ public class VueDeliveryController extends BaseController {
/** /**
* *
*
*/ */
@RequiresPermissions("inventory:delivery:recall") @RequiresPermissions("inventory:delivery:recall")
@Log(title = "发货记录", businessType = BusinessType.UPDATE) @Log(title = "发货记录", businessType = BusinessType.UPDATE)
@PostMapping("/recall/apply") @PostMapping("/recall/apply")
public AjaxResult recallApply(@RequestBody Map<String, Object> params) { 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("reason")),
String.valueOf(params.get("amountChanged"))); 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));
} }
/** /**

View File

@ -50,7 +50,7 @@ public class VueInventoryOuterController extends BaseController
{ {
inventoryOuter.setOuterStatusList(Arrays.asList(InventoryOuter.OuterStatusEnum.WAIT_RECEIVE.getCode(), InventoryOuter.OuterStatusEnum.RECEIVED.getCode())); inventoryOuter.setOuterStatusList(Arrays.asList(InventoryOuter.OuterStatusEnum.WAIT_RECEIVE.getCode(), InventoryOuter.OuterStatusEnum.RECEIVED.getCode()));
inventoryOuter.setExcludeRecalled(true); // 撤回/作废后的出库单delivery_status='3')保留可见,前端仅提供「发货记录」查看入口
if (!inventoryAuthService.authAll()){ if (!inventoryAuthService.authAll()){
List<String> productCodeList = inventoryAuthService.authProductCode(); List<String> productCodeList = inventoryAuthService.authProductCode();
if (CollUtil.isEmpty(productCodeList)){ if (CollUtil.isEmpty(productCodeList)){

View File

@ -6,6 +6,7 @@ import java.util.List;
import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity; import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.sip.domain.dto.PayableGoodsDetailDto;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
@ -96,6 +97,8 @@ public class OmsReceivableBill extends BaseEntity
private List<OmsReceivableReceiptDetail> detailList; private List<OmsReceivableReceiptDetail> detailList;
private List<OmsReceivableInvoiceDetail> invoiceDetailList; private List<OmsReceivableInvoiceDetail> invoiceDetailList;
/** 商品明细(来源:出库单) */
private List<PayableGoodsDetailDto> goodsDetailList;

View File

@ -5,7 +5,7 @@ import lombok.Data;
import java.math.BigDecimal; import java.math.BigDecimal;
/** /**
* DTO * DTO
* *
* @author ruoyi * @author ruoyi
*/ */

View File

@ -1,5 +1,6 @@
package com.ruoyi.sip.mapper; package com.ruoyi.sip.mapper;
import java.util.Date;
import java.util.List; import java.util.List;
import com.ruoyi.sip.domain.InventoryDelivery; import com.ruoyi.sip.domain.InventoryDelivery;
import com.ruoyi.sip.dto.ApiDataQueryDto; import com.ruoyi.sip.dto.ApiDataQueryDto;
@ -71,4 +72,42 @@ public interface InventoryDeliveryMapper
List<DeliveryApproveVo> selectDeliveryApproveList(@Param("id") Long id); List<DeliveryApproveVo> selectDeliveryApproveList(@Param("id") Long id);
/**
* SN approve_status1=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);
} }

View File

@ -78,6 +78,15 @@ public interface InventoryInfoMapper
List<InventoryInfo> listByDeliveryId(Long id); 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> selectInventoryInfoByOrderCode(List<String> strings);
List<InventoryInfo> selectInventoryInfoByOuterCodeList(List<String> outerCodeList); List<InventoryInfo> selectInventoryInfoByOuterCodeList(List<String> outerCodeList);

View File

@ -31,4 +31,12 @@ public interface OmsPayablePaymentDetailMapper {
void deleteByPaymentCode(String payableBillCode); void deleteByPaymentCode(String payableBillCode);
List<OmsPayablePaymentDetail> listByPaymentUpdateTime(Date startTime, Date endTime); List<OmsPayablePaymentDetail> listByPaymentUpdateTime(Date startTime, Date endTime);
/**
*
*
* @param payableBillId
* @return
*/
int deleteByPayableBillId(Long payableBillId);
} }

View File

@ -78,4 +78,12 @@ public interface OmsPayableTicketDetailMapper
void updateBatch(List<OmsPayableTicketDetail> updateList); void updateBatch(List<OmsPayableTicketDetail> updateList);
void deleteByTicketBillCode(String ticketBillCode); void deleteByTicketBillCode(String ticketBillCode);
/**
*
*
* @param payableBillId
* @return
*/
int deleteByPayableBillId(Long payableBillId);
} }

View File

@ -60,4 +60,12 @@ public interface OmsPayableTicketPlanMapper
public int deleteOmsPayableTicketPlanByIds(Long[] ids); public int deleteOmsPayableTicketPlanByIds(Long[] ids);
OmsPayableTicketPlan firstUnPayPlan(Long payableBillId); OmsPayableTicketPlan firstUnPayPlan(Long payableBillId);
/**
*
*
* @param payableBillId
* @return
*/
public int deleteByPayableBillId(Long payableBillId);
} }

View File

@ -82,4 +82,12 @@ public interface OmsReceivableInvoiceDetailMapper
void updateWriteOffIdBatch(List<OmsPayableTicketDetail> updateList); void updateWriteOffIdBatch(List<OmsPayableTicketDetail> updateList);
/**
*
*
* @param receivableBillId
* @return
*/
int deleteByReceivableBillId(Long receivableBillId);
} }

View File

@ -61,4 +61,12 @@ public interface OmsReceivableInvoicePlanMapper
OmsReceivableInvoicePlan firstUnPayPlan(Long receivableBillId); OmsReceivableInvoicePlan firstUnPayPlan(Long receivableBillId);
/**
*
*
* @param receivableBillId
* @return
*/
public int deleteByReceivableBillId(Long receivableBillId);
} }

View File

@ -75,4 +75,12 @@ public interface OmsReceivableReceiptDetailMapper
void clearWriteOffByWriteOffId(List<Long> ids); void clearWriteOffByWriteOffId(List<Long> ids);
void deleteByBillCode(String receiptBillCode); void deleteByBillCode(String receiptBillCode);
/**
*
*
* @param receivableBillId
* @return
*/
int deleteByReceivableBillId(Long receivableBillId);
} }

View File

@ -1,6 +1,7 @@
package com.ruoyi.sip.service; package com.ruoyi.sip.service;
import java.util.List; import java.util.List;
import java.util.Map;
import com.ruoyi.sip.domain.InventoryDelivery; import com.ruoyi.sip.domain.InventoryDelivery;
import com.ruoyi.sip.dto.ApiDataQueryDto; import com.ruoyi.sip.dto.ApiDataQueryDto;
import com.ruoyi.sip.dto.inventory.InventoryDeliveryDetailExcelDto; import com.ruoyi.sip.dto.inventory.InventoryDeliveryDetailExcelDto;
@ -76,17 +77,34 @@ public interface IInventoryDeliveryService
* @param id * @param id
* @param reason * @param reason
* @param amountChanged * @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);
/** /**
* *
* /
* ABC
* *
* @param deliveryId * @param deliveryId outer_code
* @param outerCode / * @param outerCode /
* @param skipReceivablePayable
*/ */
void handleRecallReceivablePayable(Long deliveryId, String outerCode, boolean skipReceivablePayable); void handleRecallReceivablePayable(Long deliveryId, String outerCode);
List<InventoryDeliveryDetailExcelDto> detailExport(InventoryDelivery inventoryDelivery); List<InventoryDeliveryDetailExcelDto> detailExport(InventoryDelivery inventoryDelivery);

View File

@ -359,7 +359,10 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
InventoryOuter.DeliveryStatusEnum.REBACK.getCode()); 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, Long> deliveryMap = inventoryDeliveries.stream().collect(Collectors.toMap(InventoryDelivery::getProductCode, InventoryDelivery::getQuantity, Long::sum));
Map<String, ProductInfo> updateMap = new HashMap<>(); Map<String, ProductInfo> updateMap = new HashMap<>();
@ -394,54 +397,19 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
if (CollUtil.isNotEmpty(updateMap.values())) { if (CollUtil.isNotEmpty(updateMap.values())) {
productInfoService.updateCount(new ArrayList<>(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) { for (InventoryDelivery inventoryDelivery : inventoryDeliveryList) {
boolean skipReceivablePayable = isManufacturerP001(inventoryDelivery.getProductCode()) || orderAmountChanged; outerDeliveryMap.putIfAbsent(inventoryDelivery.getOuterCode(), inventoryDelivery);
}
for (InventoryDelivery inventoryDelivery : outerDeliveryMap.values()) {
inventoryDeliveryService.handleRecallReceivablePayable( 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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void applyRecall(Long id, String reason, String amountChanged) { public void applyRecall(Long id, String reason, String amountChanged) {

View File

@ -52,6 +52,9 @@ import javax.annotation.Resource;
@Service @Service
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, TodoCommonTemplate { public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService, TodoCommonTemplate {
/** 情况 A存在审批中的财务单据且无审批通过单据的统一拦截提示 */
private static final String APPROVING_BILL_TIP = "存在审批中的财务单据(收款单/开票单/付款单/收票单),请驳回审批后再次操作";
@Autowired @Autowired
private InventoryDeliveryMapper inventoryDeliveryMapper; private InventoryDeliveryMapper inventoryDeliveryMapper;
@Autowired @Autowired
@ -80,6 +83,14 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
@Autowired @Autowired
private IOmsReceivableBillService billService; private IOmsReceivableBillService billService;
@Autowired @Autowired
private IOmsReceivableReceiptDetailService receivableReceiptDetailService;
@Autowired
private IOmsReceivableInvoiceDetailService receivableInvoiceDetailService;
@Autowired
private IOmsPayablePaymentDetailService payablePaymentDetailService;
@Autowired
private IOmsPayableTicketDetailService payableTicketDetailService;
@Autowired
private IOmsPurchaseOrderService omsPurchaseOrderService; private IOmsPurchaseOrderService omsPurchaseOrderService;
@Resource @Resource
private OmsPurchaseOrderMapMapper omsPurchaseOrderMapMapper; private OmsPurchaseOrderMapMapper omsPurchaseOrderMapMapper;
@ -213,6 +224,38 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
inventoryDelivery.setUpdateBy(currentUserId); inventoryDelivery.setUpdateBy(currentUserId);
inventoryDelivery.setUpdateTime(nowDate); inventoryDelivery.setUpdateTime(nowDate);
inventoryDelivery.setDeliveryStatus(InventoryDelivery.DeliveryStatusEnum.WAIT_DELIVERY.getCode()); 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()); BigDecimal bigDecimal = inventoryOuterMapper.selectOutPriceByCode(inventoryDelivery.getOuterCode());
@ -405,12 +448,22 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
InventoryDelivery inventoryDelivery1 = selectInventoryDeliveryById(inventoryDelivery.getId()); InventoryDelivery inventoryDelivery1 = selectInventoryDeliveryById(inventoryDelivery.getId());
BigDecimal allPrice = price.multiply(new BigDecimal(inventoryDelivery1.getQuantity().toString())); BigDecimal allPrice = price.multiply(new BigDecimal(inventoryDelivery1.getQuantity().toString()));
//防重:同一出库单+产品且金额一致的应收单已生成过则不重复生成(避免重复确认/并发导致重复) //防重:同一出库单+产品且金额一致的应收单已生成过则不重复生成(避免重复确认/并发导致重复)
//撤回后重新发货撤回时原应收单已通过红冲情况B或删除情况C处理完毕
// 故仅当同金额的应收单是在最近一次撤回之后生成时才算重复,否则需要重新生成
OmsReceivableBill receivableQuery = new OmsReceivableBill(); OmsReceivableBill receivableQuery = new OmsReceivableBill();
receivableQuery.setInventoryCode(inventoryDelivery.getOuterCode()); receivableQuery.setInventoryCode(inventoryDelivery.getOuterCode());
receivableQuery.setProductCode(inventoryOuter.getProductCode()); receivableQuery.setProductCode(inventoryOuter.getProductCode());
List<OmsReceivableBill> existReceivableBills = billService.selectOmsReceivableBillList(receivableQuery); 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() 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) { if (!receivableDuplicated) {
receivableBill.setTotalPriceWithTax(allPrice); receivableBill.setTotalPriceWithTax(allPrice);
BigDecimal defaultTaxRate = projectProductInfo.getTaxRate() == null ? new BigDecimal(defaultTax) : projectProductInfo.getTaxRate().divide(new BigDecimal("100")); 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 * @param amountChanged
*/ */
@Override @Override
public void applyRecall(Long id, String reason, String amountChanged) { public String applyRecall(Long id, String reason, String amountChanged) {
InventoryDelivery inventoryDelivery = inventoryDeliveryMapper.selectInventoryDeliveryById(id); InventoryDelivery inventoryDelivery = inventoryDeliveryMapper.selectInventoryDeliveryById(id);
if (inventoryDelivery == null) { if (inventoryDelivery == null) {
throw new ServiceException("发货记录不存在"); throw new ServiceException("发货记录不存在");
@ -593,6 +646,23 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
if (processInstance != null) { if (processInstance != null) {
throw new ServiceException("该发货记录已提交撤回审批,请勿重复提交"); 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()); inventoryDelivery.setApproveStatus(ApproveStatusEnum.WAIT_APPROVE.getCode());
inventoryDeliveryMapper.updateInventoryDelivery(inventoryDelivery); inventoryDeliveryMapper.updateInventoryDelivery(inventoryDelivery);
@ -607,6 +677,84 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
put("extendField3", amountChanged); put("extendField3", amountChanged);
put("deliveryId", id); put("deliveryId", id);
}}, processConfig.getDefinition().getOuterReback()); }}, 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 @Override
@Transactional(rollbackFor = Exception.class)
public boolean multiInstanceApproveCallback(String activityName, ProcessInstance processInstance) { public boolean multiInstanceApproveCallback(String activityName, ProcessInstance processInstance) {
if (processConfig.getDefinition().getOuterReback().equals(processInstance.getProcessDefinitionKey()) if (processConfig.getDefinition().getOuterReback().equals(processInstance.getProcessDefinitionKey())
&& "领导".equals(activityName)) { && "领导".equals(activityName)) {
@ -692,26 +841,27 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
Todo firstCompleted = todoMapper.selectFirstCompletedByProcessInstanceId(processInstance.getId()); Todo firstCompleted = todoMapper.selectFirstCompletedByProcessInstanceId(processInstance.getId());
String updateBy = firstCompleted != null && StringUtils.isNotBlank(firstCompleted.getApproveUser()) String updateBy = firstCompleted != null && StringUtils.isNotBlank(firstCompleted.getApproveUser())
? firstCompleted.getApproveUser() : ShiroUtils.getUserId().toString(); ? 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); 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(); InventoryDelivery update = new InventoryDelivery();
update.setId(delivery.getId()); update.setId(delivery.getId());
@ -755,6 +905,7 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
} }
@Override @Override
@Transactional(rollbackFor = Exception.class)
public void recall(Long id) { public void recall(Long id) {
recall(id, ShiroUtils.getUserId().toString()); recall(id, ShiroUtils.getUserId().toString());
} }
@ -765,11 +916,19 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
* @param id * @param id
* @param updateBy id * @param updateBy id
*/ */
@Transactional(rollbackFor = Exception.class)
public void recall(Long id, String updateBy) { public void recall(Long id, String updateBy) {
InventoryDelivery inventoryDelivery = inventoryDeliveryMapper.selectInventoryDeliveryById(id); InventoryDelivery inventoryDelivery = inventoryDeliveryMapper.selectInventoryDeliveryById(id);
// 制造商编码为 P001 的产品,或 撤回审批中“金额是否变化”为“是”时,跳过应收/应付相关的判断与处理 if (inventoryDelivery == null) {
boolean skipReceivablePayable = isManufacturerP001(inventoryDelivery.getProductCode()) throw new ServiceException("发货记录不存在");
|| isRecallAmountChanged(id); }
// ══════════════════════════════════════════════════════
// 幂等保护:仅「已发货」状态可撤回。
// 防止对已撤回(delivery_status='2')的记录重复操作
// ══════════════════════════════════════════════════════
if (!InventoryDelivery.DeliveryStatusEnum.CONFIRM_DELIVERY.getCode().equals(inventoryDelivery.getDeliveryStatus())) {
throw new ServiceException("该发货记录当前不是「已发货」状态,无法再次撤回/作废");
}
ProjectOrderInfo projectOrderInfo = projectOrderInfoService.selectProjectOrderInfoByOrderCode(inventoryDelivery.getOrderCode()); ProjectOrderInfo projectOrderInfo = projectOrderInfoService.selectProjectOrderInfoByOrderCode(inventoryDelivery.getOrderCode());
deleteInventoryOuterById(id, false, projectOrderInfo.getOrderCode()); deleteInventoryOuterById(id, false, projectOrderInfo.getOrderCode());
List<ProjectProductInfo> projectProductInfos = projectProductInfoService.listDeliveryProductByOrderCode(Collections.singletonList(inventoryDelivery.getOrderCode())); List<ProjectProductInfo> projectProductInfos = projectProductInfoService.listDeliveryProductByOrderCode(Collections.singletonList(inventoryDelivery.getOrderCode()));
@ -793,37 +952,199 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
inventoryOuterMapper.updateInventoryOuter(updateDto); inventoryOuterMapper.updateInventoryOuter(updateDto);
} }
long allSum = deliveryList.stream().mapToLong(InventoryDelivery::getQuantity).sum(); long allSum = deliveryList.stream().mapToLong(InventoryDelivery::getQuantity).sum();
//修改订单的发货状态 //修改订单的发货状态全部撤回已确认发货数量为0时回到「未发货」保证订单可以重新出库
ProjectOrderInfo updateOrder = new ProjectOrderInfo(); ProjectOrderInfo updateOrder = new ProjectOrderInfo();
updateOrder.setOrderCode(inventoryDelivery.getOrderCode()); updateOrder.setOrderCode(inventoryDelivery.getOrderCode());
updateOrder.setUpdateTime(new Date()); updateOrder.setUpdateTime(new Date());
updateOrder.setUpdateBy(updateBy); 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.setDeliveryStatus(sum == allSum ? ProjectOrderInfo.DeliveryStatusEnum.ALL_DELIVERY.getCode() : ProjectOrderInfo.DeliveryStatusEnum.PART_DELIVERY.getCode());
}
updateOrder.setOperationVersion(projectOrderInfo.getOperationVersion() + 1); updateOrder.setOperationVersion(projectOrderInfo.getOperationVersion() + 1);
projectOrderInfoService.updateProjectOrderInfoByCode(updateOrder); projectOrderInfoService.updateProjectOrderInfoByCode(updateOrder);
//修改累计发货数量 //修改累计发货数量
productInfoService.updateCumulativeCount(-inventoryDelivery.getQuantity(), inventoryDelivery.getProductCode()); 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 //撤回时处理当前出库单对应的应付、应收数据仅处理软件和硬件类型1、2
handleRecallReceivablePayable(id, inventoryDelivery.getOuterCode(), skipReceivablePayable); handleRecallReceivablePayable(id, inventoryDelivery.getOuterCode());
//整单撤回完成:出库单作废(标记为已撤回、不物理删除),回补实时库存并回退订单出库状态,
// 使订单可以重新出库(再次生成新的出库单)
markOuterRecalled(inventoryDelivery.getOuterCode());
} }
/** /**
* *
* //// * 1.
* 2.
* 3. 退使
* *
* @param deliveryId * @param outerCode
* @param outerCode /
* @param skipReceivablePayable
*/ */
@Override private void markOuterRecalled(String outerCode) {
public void handleRecallReceivablePayable(Long deliveryId, String outerCode, boolean skipReceivablePayable) { InventoryOuter outer = inventoryOuterMapper.selectInventoryOuterByCode(outerCode);
if (skipReceivablePayable) { if (outer == null) {
return; return;
} }
// 是否存在应收/应付财务单据(付款/收票/收款/开票单):无则删除原应收应付,有则改为生成红冲负金额单 inventoryOuterMapper.updateDeliveryStatusByOuterCode(outerCode, InventoryOuter.DeliveryStatusEnum.REBACK.getCode());
List<DeliveryApproveVo> deliveryApproveVoList = inventoryDeliveryMapper.selectDeliveryApproveList(deliveryId); productInfoService.updateAvailableCount(outer.getQuantity(), outer.getProductCode());
boolean hasFinanceBill = CollUtil.isNotEmpty(deliveryApproveVoList); 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
// 路径一 SNinventory_info.product_sn → outer_code → 单据(依赖 inventory_info.outer_code 未被清空)
// 路径二 outeroms_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_codeSN 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 // 仅处理软件和硬件类型1、2
Set<String> deleteProductTypeSet = new HashSet<>(Arrays.asList( Set<String> deleteProductTypeSet = new HashSet<>(Arrays.asList(
ProductInfo.ProductTypeEnum.SOFTWARE.getType(), ProductInfo.ProductTypeEnum.SOFTWARE.getType(),
@ -834,80 +1155,139 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
List<OmsPayableBill> payableBills = payableBillService.selectOmsPayableBillList(queryPayable).stream() List<OmsPayableBill> payableBills = payableBillService.selectOmsPayableBillList(queryPayable).stream()
.filter(item -> deleteProductTypeSet.contains(item.getProductType())) .filter(item -> deleteProductTypeSet.contains(item.getProductType()))
.collect(Collectors.toList()); .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(); OmsReceivableBill queryReceivable = new OmsReceivableBill();
queryReceivable.setInventoryCode(outerCode); queryReceivable.setInventoryCode(outerCode);
List<OmsReceivableBill> receivableBills = billService.selectOmsReceivableBillList(queryReceivable).stream() List<OmsReceivableBill> receivableBills = billService.selectOmsReceivableBillList(queryReceivable).stream()
.filter(item -> deleteProductTypeSet.contains(item.getProductType())) .filter(item -> deleteProductTypeSet.contains(item.getProductType()))
.collect(Collectors.toList()); .collect(Collectors.toList());
if (hasFinanceBill) {
// 存在应收/应付财务单据(未完成或已完成):不删除原单,改为生成红冲负金额应收、应付单 // INNER_PAY 厂商的入库应付单本身不带合同号,红冲单若原样复制空合同号,
generateRecallRedRushBill(payableBills, receivableBills); // 在应付单列表按合同/项目筛选时查不到,故用出库单的合同号补齐合同号(仅内存对象,不影响原单)
} else {
// 无应收/应付财务单据:直接删除对应的应付、应收信息
if (CollUtil.isNotEmpty(payableBills)) { if (CollUtil.isNotEmpty(payableBills)) {
String ids = payableBills.stream().map(item -> String.valueOf(item.getId())).collect(Collectors.joining(",")); InventoryOuter recallOuter = inventoryOuterMapper.selectInventoryOuterByCode(outerCode);
payableBillService.deleteOmsPayableBillByIds(ids); 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)) { if (CollUtil.isNotEmpty(receivableBills)) {
String ids = receivableBills.stream().map(item -> String.valueOf(item.getId())).collect(Collectors.joining(",")); String ids = receivableBills.stream().map(item -> String.valueOf(item.getId())).collect(Collectors.joining(","));
billService.deleteOmsReceivableBillByIds(ids); billService.deleteOmsReceivableBillByIds(ids);
log.info("情况C全部撤回已删除该出库单下全部应收单。outerCode={}, count={}", outerCode, receivableBills.size());
} }
log.info("情况C全部撤回应付单不做处理不删除。outerCode={}, payableCount={}", outerCode, payableBills.size());
} }
//清空本发货单库存明细上的应付单号,保证撤回后重新确认发货可再次生成应付单 //清空该出库单库存明细上的应付单号,保证撤回后重新确认发货可再次生成应付单(情况 B 和 C 都要清空)
List<InventoryInfo> recallInventoryInfos = inventoryInfoService.listByDeliveryId(deliveryId); if (CollUtil.isNotEmpty(outerInventoryInfos)) {
if (CollUtil.isNotEmpty(recallInventoryInfos)) { inventoryInfoService.clearPayableBillCodeByIds(outerInventoryInfos.stream().map(InventoryInfo::getId).collect(Collectors.toList()));
inventoryInfoService.clearPayableBillCodeByIds(recallInventoryInfos.stream().map(InventoryInfo::getId).collect(Collectors.toList()));
} }
} }
/**
*
* / extendField3id 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 payableBills /
* @param receivableBills / * @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)) { if (CollUtil.isNotEmpty(payableBills)) {
for (OmsPayableBill src : 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(); OmsPayableBill redRush = new OmsPayableBill();
redRush.setVendorCode(src.getVendorCode()); redRush.setVendorCode(src.getVendorCode());
redRush.setOrderCode(src.getOrderCode()); redRush.setOrderCode(src.getOrderCode());
@ -916,15 +1296,26 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
redRush.setProductType(src.getProductType()); redRush.setProductType(src.getProductType());
redRush.setProductLevel2Type(src.getProductLevel2Type()); redRush.setProductLevel2Type(src.getProductLevel2Type());
redRush.setTaxRate(src.getTaxRate()); redRush.setTaxRate(src.getTaxRate());
// 负金额红冲 // 按比例负金额红冲:含税、不含税分别乘比例取负,税额=含税-不含税,保证三者一致
redRush.setTotalPriceWithTax(src.getTotalPriceWithTax() == null ? null : src.getTotalPriceWithTax().negate()); BigDecimal withTax = negRatio(src.getTotalPriceWithTax(), ratio);
redRush.setTotalPriceWithoutTax(src.getTotalPriceWithoutTax() == null ? null : src.getTotalPriceWithoutTax().negate()); BigDecimal withoutTax = negRatio(src.getTotalPriceWithoutTax(), ratio);
redRush.setTaxAmount(src.getTaxAmount() == null ? null : src.getTaxAmount().negate()); redRush.setTotalPriceWithTax(withTax);
redRush.setTotalPriceWithoutTax(withoutTax);
redRush.setTaxAmount(withTax == null || withoutTax == null ? negRatio(src.getTaxAmount(), ratio)
: withTax.subtract(withoutTax));
payableBillService.insertOmsPayableBill(redRush, 0); payableBillService.insertOmsPayableBill(redRush, 0);
// 同步生成红冲单的付款明细、收票明细
copyPayableDetails(src, redRush, ratio, operator, now);
} }
} }
if (CollUtil.isNotEmpty(receivableBills)) { if (CollUtil.isNotEmpty(receivableBills)) {
for (OmsReceivableBill src : 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(); OmsReceivableBill redRush = new OmsReceivableBill();
redRush.setPartnerCode(src.getPartnerCode()); redRush.setPartnerCode(src.getPartnerCode());
redRush.setPartnerName(src.getPartnerName()); redRush.setPartnerName(src.getPartnerName());
@ -933,15 +1324,124 @@ public class InventoryDeliveryServiceImpl implements IInventoryDeliveryService,
redRush.setProductCode(src.getProductCode()); redRush.setProductCode(src.getProductCode());
redRush.setProductType(src.getProductType()); redRush.setProductType(src.getProductType());
redRush.setTaxRate(src.getTaxRate()); redRush.setTaxRate(src.getTaxRate());
// 负金额红冲 BigDecimal withTax = negRatio(src.getTotalPriceWithTax(), ratio);
redRush.setTotalPriceWithTax(src.getTotalPriceWithTax() == null ? null : src.getTotalPriceWithTax().negate()); BigDecimal withoutTax = negRatio(src.getTotalPriceWithoutTax(), ratio);
redRush.setTotalPriceWithoutTax(src.getTotalPriceWithoutTax() == null ? null : src.getTotalPriceWithoutTax().negate()); redRush.setTotalPriceWithTax(withTax);
redRush.setTaxAmount(src.getTaxAmount() == null ? null : src.getTaxAmount().negate()); redRush.setTotalPriceWithoutTax(withoutTax);
redRush.setTaxAmount(withTax == null || withoutTax == null ? negRatio(src.getTaxAmount(), ratio)
: withTax.subtract(withoutTax));
billService.insertOmsReceivableBill(redRush); billService.insertOmsReceivableBill(redRush);
// 同步生成红冲单的收款明细、开票明细
copyReceivableDetails(src, redRush, ratio, operator, now);
} }
} }
} }
/**
* idwrite_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);
}
}
}
/**
* idwrite_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 @Override
public List<InventoryDeliveryDetailExcelDto> detailExport(InventoryDelivery inventoryDelivery) { public List<InventoryDeliveryDetailExcelDto> detailExport(InventoryDelivery inventoryDelivery) {
InventoryDelivery dbData = inventoryDeliveryMapper.selectInventoryDeliveryById(inventoryDelivery.getId()); InventoryDelivery dbData = inventoryDeliveryMapper.selectInventoryDeliveryById(inventoryDelivery.getId());

View File

@ -175,6 +175,11 @@ public class InventoryOuterServiceImpl implements IInventoryOuterService
public int deleteInventoryOuterById(Long id) public int deleteInventoryOuterById(Long id)
{ {
InventoryOuter inventoryOuter = inventoryOuterMapper.selectInventoryOuterById(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())); detailService.deleteByOuterCode(Collections.singletonList(inventoryOuter.getOuterCode()));
productInfoService.updateAvailableCount(inventoryOuter.getQuantity(), inventoryOuter.getProductCode()); productInfoService.updateAvailableCount(inventoryOuter.getQuantity(), inventoryOuter.getProductCode());
@ -192,6 +197,11 @@ public class InventoryOuterServiceImpl implements IInventoryOuterService
@Override @Override
public int statusUpdate(InventoryOuter inventoryOuter) { 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(); InventoryOuter queryDto = new InventoryOuter();
queryDto.setOrderCode(inventoryOuter.getOrderCode()); queryDto.setOrderCode(inventoryOuter.getOrderCode());
ProjectOrderInfo projectOrderInfo = new ProjectOrderInfo(); ProjectOrderInfo projectOrderInfo = new ProjectOrderInfo();
@ -283,6 +293,11 @@ public class InventoryOuterServiceImpl implements IInventoryOuterService
List<InventoryDelivery> tempDeliveryList = deliveryListMap.get(vo.getWarehouseId()); List<InventoryDelivery> tempDeliveryList = deliveryListMap.get(vo.getWarehouseId());
if (CollUtil.isNotEmpty(tempDeliveryList)){ if (CollUtil.isNotEmpty(tempDeliveryList)){
for (InventoryDelivery inventoryDelivery : tempDeliveryList) { for (InventoryDelivery inventoryDelivery : tempDeliveryList) {
// 列表不再排除已撤回记录,此处必须显式忽略,否则已撤回数量会被算进"已确认发货数量"
if (InventoryDelivery.DeliveryStatusEnum.RECALL_DELIVERY.getCode()
.equals(inventoryDelivery.getDeliveryStatus())) {
continue;
}
if (inventoryDelivery.getDeliveryStatus().equals(InventoryDelivery.DeliveryStatusEnum.WAIT_DELIVERY.getCode())) { if (inventoryDelivery.getDeliveryStatus().equals(InventoryDelivery.DeliveryStatusEnum.WAIT_DELIVERY.getCode())) {
vo.setDeliveryGenerateQuantity(vo.getDeliveryGenerateQuantity() + inventoryDelivery.getQuantity()); vo.setDeliveryGenerateQuantity(vo.getDeliveryGenerateQuantity() + inventoryDelivery.getQuantity());
} else { } else {

View File

@ -2,6 +2,7 @@ package com.ruoyi.sip.service.impl;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function; 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) { private List<List<String>> buildExcelData(List<InvoiceDetailItemExcelDto> list) {
List<List<String>> dataList = new ArrayList<>(); List<List<String>> dataList = new ArrayList<>();
AtomicInteger integer=new AtomicInteger(1); AtomicInteger integer=new AtomicInteger(1);
@ -649,10 +657,10 @@ public class OmsInvoiceBillServiceImpl implements IOmsInvoiceBillService, TodoCo
row.add(item.getProductModel()); row.add(item.getProductModel());
row.add(item.getUnit()); row.add(item.getUnit());
row.add(String.valueOf(item.getQuantity())); row.add(String.valueOf(item.getQuantity()));
row.add(String.valueOf(item.getPrice())); row.add(formatAmount(item.getPrice()));
row.add(String.valueOf(item.getAllPrice())); row.add(formatAmount(item.getAllPrice()));
row.add(String.valueOf(item.getTaxRate())); 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(DictUtils.getDictLabel("finance_invoice_type",item.getInvoiceType()));
row.add(item.getRemark()); row.add(item.getRemark());
dataList.add(row); dataList.add(row);

View File

@ -20,9 +20,12 @@ import com.ruoyi.sip.mapper.InventoryInfoMapper;
import com.ruoyi.sip.mapper.InventoryOuterMapper; import com.ruoyi.sip.mapper.InventoryOuterMapper;
import com.ruoyi.sip.mapper.OmsInventoryInnerMapper; import com.ruoyi.sip.mapper.OmsInventoryInnerMapper;
import com.ruoyi.sip.mapper.OmsPayableBillMapper; import com.ruoyi.sip.mapper.OmsPayableBillMapper;
import com.ruoyi.sip.mapper.OmsPayablePaymentDetailMapper;
import com.ruoyi.sip.mapper.OmsPayablePaymentPlanMapper; import com.ruoyi.sip.mapper.OmsPayablePaymentPlanMapper;
import com.ruoyi.sip.mapper.OmsPayableTicketDetailMapper;
import com.ruoyi.sip.mapper.OmsPayableTicketPlanMapper; import com.ruoyi.sip.mapper.OmsPayableTicketPlanMapper;
import com.ruoyi.sip.service.*; import com.ruoyi.sip.service.*;
import com.ruoyi.sip.utils.GoodsDetailUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -51,6 +54,10 @@ public class OmsPayableBillServiceImpl implements IOmsPayableBillService {
@Autowired @Autowired
private OmsPayableTicketPlanMapper omsPayableTicketPlanMapper; private OmsPayableTicketPlanMapper omsPayableTicketPlanMapper;
@Autowired @Autowired
private OmsPayablePaymentDetailMapper omsPayablePaymentDetailMapper;
@Autowired
private OmsPayableTicketDetailMapper omsPayableTicketDetailMapper;
@Autowired
private IOmsPaymentBillService omsPaymentBillService; private IOmsPaymentBillService omsPaymentBillService;
@Autowired @Autowired
@ -184,15 +191,19 @@ public class OmsPayableBillServiceImpl implements IOmsPayableBillService {
/** /**
* *
*
* *
* @param ids * @param ids
* @return * @return
*/ */
@Override @Override
public int deleteOmsPayableBillByIds(String ids) { public int deleteOmsPayableBillByIds(String ids) {
// Also delete payment plans
for (String id : Convert.toStrArray(ids)) { 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)); return omsPayableBillMapper.deleteOmsPayableBillByIds(Convert.toStrArray(ids));
} }
@ -563,20 +574,21 @@ public class OmsPayableBillServiceImpl implements IOmsPayableBillService {
List<OmsPaymentBill> omsPaymentBills = omsPaymentBillService.listPreResidueAmountByVendorCodeList(Collections.singletonList(omsPayableBill.getVendorCode())); 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)); 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.setPreResidueAmount(decimalMap.getOrDefault(omsPayableBill.getVendorCode(), BigDecimal.ZERO));
// 商品明细(来源:入库单 // 商品明细(来源:入库单/出库单,冲红单按单据金额缩放
omsPayableBill.setGoodsDetailList(queryGoodsDetailList(omsPayableBill.getInventoryCode())); omsPayableBill.setGoodsDetailList(queryGoodsDetailList(omsPayableBill));
return omsPayableBill; return omsPayableBill;
} }
/** /**
* / *
* inventoryCode inner_codeouter_code * inventoryCode inner_codeouter_code
* *
* @param inventoryCode / * @param omsPayableBill
* @return * @return
*/ */
private List<PayableGoodsDetailDto> queryGoodsDetailList(String inventoryCode) { private List<PayableGoodsDetailDto> queryGoodsDetailList(OmsPayableBill omsPayableBill) {
String inventoryCode = omsPayableBill.getInventoryCode();
if (StringUtils.isEmpty(inventoryCode)) { if (StringUtils.isEmpty(inventoryCode)) {
return Collections.emptyList(); return Collections.emptyList();
} }
@ -588,67 +600,29 @@ public class OmsPayableBillServiceImpl implements IOmsPayableBillService {
// 产品类型为3服务/22硬件维保时 含税小计 = 单价 × 数量,其他为入库价合计 // 产品类型为3服务/22硬件维保时 含税小计 = 单价 × 数量,其他为入库价合计
boolean multiplyQuantity = Arrays.asList("3", "22").contains(inventoryInner.getProductType()); boolean multiplyQuantity = Arrays.asList("3", "22").contains(inventoryInner.getProductType());
BigDecimal taxRate = inventoryInner.getTaxRate() != null ? inventoryInner.getTaxRate() : defaultTaxRate(); 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); InventoryOuter inventoryOuter = inventoryOuterMapper.selectInventoryOuterByCode(inventoryCode);
if (inventoryOuter != null) { if (inventoryOuter != null) {
List<InventoryInfo> inventoryInfoList = inventoryInfoMapper.selectInventoryInfoByOuterCodeList(Collections.singletonList(inventoryCode)); 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)) { if (CollUtil.isEmpty(inventoryInfoList)) {
return Collections.emptyList(); return Collections.emptyList();
} }
String productType = inventoryInfoList.get(0).getProductType(); String productType = inventoryInfoList.get(0).getProductType();
BigDecimal taxRate = inventoryInfoList.get(0).getTaxRate() != null ? inventoryInfoList.get(0).getTaxRate() : defaultTaxRate(); 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(); 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;
}
/** /**
* *
*/ */

View File

@ -2,6 +2,8 @@ package com.ruoyi.sip.service.impl;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -219,9 +221,9 @@ public class OmsPaymentBillServiceImpl implements IOmsPaymentBillService , TodoC
formatDateTime(bill.getPaymentTime()), formatDateTime(bill.getPaymentTime()),
bill.getVendorName(), bill.getVendorName(),
bill.getOrderCode(), bill.getOrderCode(),
bill.getTotalPriceWithTax(), formatAmount(bill.getTotalPriceWithTax()),
bill.getTotalPriceWithoutTax(), formatAmount(bill.getTotalPriceWithoutTax()),
bill.getTaxAmount(), formatAmount(bill.getTaxAmount()),
formatDateTime(bill.getActualPaymentTime()), formatDateTime(bill.getActualPaymentTime()),
DictUtils.getDictLabel("payment_status", bill.getPaymentStatus()), DictUtils.getDictLabel("payment_status", bill.getPaymentStatus()),
DictUtils.getDictLabel("approve_status", bill.getApproveStatus()), DictUtils.getDictLabel("approve_status", bill.getApproveStatus()),
@ -240,8 +242,8 @@ public class OmsPaymentBillServiceImpl implements IOmsPaymentBillService , TodoC
row.add(detail.getProjectCode()); row.add(detail.getProjectCode());
row.add(detail.getProjectName()); row.add(detail.getProjectName());
row.add(detail.getPayableBillCode()); row.add(detail.getPayableBillCode());
row.add(detail.getTotalPriceWithTax()); row.add(formatAmount(detail.getTotalPriceWithTax()));
row.add(detail.getPaymentAmount()); row.add(formatAmount(detail.getPaymentAmount()));
row.add(detail.getPaymentRate()); row.add(detail.getPaymentRate());
} else { } else {
row.add(""); row.add("");
@ -261,6 +263,13 @@ public class OmsPaymentBillServiceImpl implements IOmsPaymentBillService , TodoC
return date == null ? "" : DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", date); 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) { private String resolvePaymentBillTypeDesc(String paymentBillType) {
if (OmsPaymentBill.PaymentBillTypeEnum.FROM_PAYABLE.getCode().equals(paymentBillType)) { if (OmsPaymentBill.PaymentBillTypeEnum.FROM_PAYABLE.getCode().equals(paymentBillType)) {
return OmsPaymentBill.PaymentBillTypeEnum.FROM_PAYABLE.getDesc(); return OmsPaymentBill.PaymentBillTypeEnum.FROM_PAYABLE.getDesc();

View File

@ -199,6 +199,7 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService, TodoCo
omsReceiptBillMapper.update(omsReceiptBill); omsReceiptBillMapper.update(omsReceiptBill);
// 上传文件路径 // 上传文件路径
if (file != null && !file.isEmpty()) {
String filePath = RuoYiConfig.getUploadPath(); String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称 // 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file); String fileName = FileUploadUtils.upload(filePath, file);
@ -214,6 +215,7 @@ public class OmsReceiptBillServiceImpl implements IOmsReceiptBillService, TodoCo
attachment.setFileType(file.getContentType()); attachment.setFileType(file.getContentType());
attachment.setCreateBy(loginUser.getUserId().toString()); attachment.setCreateBy(loginUser.getUserId().toString());
attachmentService.insertOmsFinAttachment(attachment); attachmentService.insertOmsFinAttachment(attachment);
}
//开始审批 //开始审批
todoService.startProcessDeleteBefore(receiptBill.getReceiptBillCode(), receiptBill.getReceiptBillCode() todoService.startProcessDeleteBefore(receiptBill.getReceiptBillCode(), receiptBill.getReceiptBillCode()
, new HashMap<String, Object>() {{ , new HashMap<String, Object>() {{

View File

@ -12,6 +12,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.PageUtils; import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.common.utils.ShiroUtils; 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.*;
import com.ruoyi.sip.domain.dto.*; import com.ruoyi.sip.domain.dto.*;
import com.ruoyi.sip.service.*; 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.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.sip.mapper.InventoryInfoMapper;
import com.ruoyi.sip.mapper.OmsReceivableBillMapper; 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 * Service
@ -61,6 +69,16 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
private IOmsCompanyInfoService companyInfoService; private IOmsCompanyInfoService companyInfoService;
@Autowired @Autowired
private IOmsReceivableInvoiceDetailService invoiceDetailService; 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}") @Value("${oms.inventory.innerTax:0.13}")
private String defaultTax; private String defaultTax;
/** /**
@ -114,6 +132,7 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
/** /**
* *
*
* *
* @param ids * @param ids
* @return * @return
@ -121,6 +140,13 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
@Override @Override
public int deleteOmsReceivableBillByIds(String ids) 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); return omsReceivableBillMapper.deleteOmsReceivableBillByIds(ids);
} }
@ -155,10 +181,51 @@ public class OmsReceivableBillServiceImpl implements IOmsReceivableBillService
Map<String, BigDecimal> decimalMap = receiptBills.stream().filter(item -> item.getRemainingAmount() != null) Map<String, BigDecimal> decimalMap = receiptBills.stream().filter(item -> item.getRemainingAmount() != null)
.collect(Collectors.toMap(OmsReceiptBill::getPartnerCode, OmsReceiptBill::getRemainingAmount, BigDecimal::add)); .collect(Collectors.toMap(OmsReceiptBill::getPartnerCode, OmsReceiptBill::getRemainingAmount, BigDecimal::add));
omsReceivableBill.setRemainingAmount(decimalMap.getOrDefault(omsReceivableBill.getPartnerCode(), BigDecimal.ZERO)); omsReceivableBill.setRemainingAmount(decimalMap.getOrDefault(omsReceivableBill.getPartnerCode(), BigDecimal.ZERO));
// 商品明细(来源:出库单,冲红单按单据金额缩放)
omsReceivableBill.setGoodsDetailList(queryGoodsDetailList(omsReceivableBill));
} }
return 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 @Override
@Transactional @Transactional
public int mergeAndInitiateReceipt(MergedReceviableReceiptDataDto dto) { public int mergeAndInitiateReceipt(MergedReceviableReceiptDataDto dto) {

View File

@ -51,6 +51,7 @@ import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.Period; import java.time.Period;
@ -583,6 +584,13 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
return DateUtils.parseDateToStr(pattern, date); 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.getPartnerName());
row.add(info.getPartnerUserName()); row.add(info.getPartnerUserName());
row.add(info.getContactWay()); 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(DateUtil.format(info.getEstimatedOrderTime(), "yyyy-MM-dd"));
row.add(formatterStr(info.getPoc())); row.add(formatterStr(info.getPoc()));
row.add(info.getCompetitor()); 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++) { for (int i = 0; i < maxWorkIndex; i++) {
@ -1237,7 +1245,7 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
row.add(productInfo.getProductBomCode()); row.add(productInfo.getProductBomCode());
row.add(productInfo.getModel()); row.add(productInfo.getModel());
row.add(productInfo.getQuantity() == null ? "" : productInfo.getQuantity().toString()); 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) { if (productInfo.getAllPrice() != null) {
totalPrice = totalPrice.add(productInfo.getAllPrice()); totalPrice = totalPrice.add(productInfo.getAllPrice());
} }

View File

@ -4,6 +4,7 @@ import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
@ -1372,13 +1373,13 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
totalPrice = processProducts(maintenanceList, maxMaintenanceService, row, totalPrice); totalPrice = processProducts(maintenanceList, maxMaintenanceService, row, totalPrice);
int insertIndex=23; int insertIndex=23;
row.add(insertIndex++, wssDto.getQuantity()); row.add(insertIndex++, wssDto.getQuantity());
row.add(insertIndex++, wssDto.getAllPrice()); row.add(insertIndex++, formatAmount(wssDto.getAllPrice()));
row.add(insertIndex++, wssDto.getTaxRate()); row.add(insertIndex++, wssDto.getTaxRate());
row.add(insertIndex++, wspDto.getQuantity()); row.add(insertIndex++, wspDto.getQuantity());
row.add(insertIndex++, wspDto.getAllPrice()); row.add(insertIndex++, formatAmount(wspDto.getAllPrice()));
row.add(insertIndex++, wspDto.getTaxRate()); row.add(insertIndex++, wspDto.getTaxRate());
row.add(insertIndex++, lsDto.getQuantity()); row.add(insertIndex++, lsDto.getQuantity());
row.add(insertIndex++, lsDto.getAllPrice()); row.add(insertIndex++, formatAmount(lsDto.getAllPrice()));
row.add(insertIndex++, lsDto.getTaxRate()); row.add(insertIndex++, lsDto.getTaxRate());
for (int i = 0; i < maxOne; i++) { for (int i = 0; i < maxOne; i++) {
ProjectProductInfo projectProductInfo = i < oneList.size() ? oneList.get(i) : null; 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(17, nVIDIADto.getAllPrice().toString());
// row.add(StrUtil.toStringOrNull(info.getOrderChannel().equals(ProjectOrderInfo.OrderChannelEnum.TOTAL_GENERATION.getCode()) ? // row.add(StrUtil.toStringOrNull(info.getOrderChannel().equals(ProjectOrderInfo.OrderChannelEnum.TOTAL_GENERATION.getCode()) ?
// info.getShipmentAmount() : info.getActualPurchaseAmount())); // info.getShipmentAmount() : info.getActualPurchaseAmount()));
row.add(info.getShipmentAmount() != null ? info.getShipmentAmount() : ""); row.add(info.getShipmentAmount() != null ? formatAmount(info.getShipmentAmount()) : "");
row.add(totalPrice); row.add(formatAmount(totalPrice));
//维保金额 //维保金额
row.add(maintenancePrice); row.add(formatAmount(maintenancePrice));
row.add(info.getSoftwareProjectProductInfoList() == null ? 0 : row.add(info.getSoftwareProjectProductInfoList() == null ? "" :
// info.getSoftwareProjectProductInfoList().stream() formatAmount(info.getSoftwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
// .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()))) row.add(info.getHardwareProjectProductInfoList() == null ? "" :
// .reduce(BigDecimal.ZERO, BigDecimal::add))); formatAmount(info.getHardwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
info.getSoftwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); row.add(info.getMaintenanceProjectProductInfoList() == null ? "" :
row.add(info.getHardwareProjectProductInfoList() == null ? 0 : formatAmount(info.getMaintenanceProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add)));
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));
dataList.add(row); dataList.add(row);
} }
return dataList; return dataList;
@ -1494,6 +1492,13 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
return "0".equals(value) ? "否" : "是"; 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) { private static BigDecimal addProductRow(ProjectProductInfo productInfo, List<Object> row, BigDecimal totalPrice) {
if (productInfo == null) { if (productInfo == null) {
row.add(""); row.add("");
@ -1506,7 +1511,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
row.add(productInfo.getProductBomCode()); row.add(productInfo.getProductBomCode());
row.add(productInfo.getModel()); row.add(productInfo.getModel());
row.add(productInfo.getQuantity() == null ? "" : productInfo.getQuantity()); 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()); row.add(productInfo.getTaxRate() == null ? "" : productInfo.getTaxRate());
if (productInfo.getAllPrice() != null) { if (productInfo.getAllPrice() != null) {
totalPrice = totalPrice.add(productInfo.getAllPrice()); totalPrice = totalPrice.add(productInfo.getAllPrice());
@ -1526,7 +1531,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
row.add(index++, productInfo.getProductBomCode()); row.add(index++, productInfo.getProductBomCode());
row.add(index++, productInfo.getModel()); row.add(index++, productInfo.getModel());
row.add(index++, productInfo.getQuantity() == null ? "" : productInfo.getQuantity()); 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()); row.add(index++, productInfo.getTaxRate() == null ? "" : productInfo.getTaxRate());
return index; 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()); row.add(info.getDiscountFold());
BigDecimal softPrice = info.getSoftwareProjectProductInfoList() == null ? BigDecimal.ZERO : BigDecimal softPrice = info.getSoftwareProjectProductInfoList() == null ? BigDecimal.ZERO :
info.getSoftwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add); 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); info.getHardwareProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal mainPrice = info.getMaintenanceProjectProductInfoList() == null ? BigDecimal.ZERO : BigDecimal mainPrice = info.getMaintenanceProjectProductInfoList() == null ? BigDecimal.ZERO :
info.getMaintenanceProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add); info.getMaintenanceProjectProductInfoList().stream().map(ProjectProductInfo::getAllPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
row.add(softPrice.add(hardPrice).add(mainPrice)); row.add(formatAmount(softPrice.add(hardPrice).add(mainPrice)));
row.add(softPrice); 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.getApproveTime(), "yyyy-MM-dd HH:mm:ss"));
row.add(DateUtil.format(info.getOrderEndTime(), "yyyy-MM-dd")); row.add(DateUtil.format(info.getOrderEndTime(), "yyyy-MM-dd"));
row.add(info.getAgentName()); row.add(info.getAgentName());
@ -2572,11 +2577,11 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
row.add(productInfo.getModel()); row.add(productInfo.getModel());
row.add(productInfo.getProductDesc()); row.add(productInfo.getProductDesc());
row.add(productInfo.getQuantity() == null ? "" : productInfo.getQuantity()); 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.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(orderInfo.getDiscountFold() == null ? "" : orderInfo.getDiscountFold());
row.add(productInfo.getAllPrice() == null ? "" : productInfo.getAllPrice()); row.add(formatAmount(productInfo.getAllPrice()));
row.add(productInfo.getTaxRate() == null ? "" : productInfo.getTaxRate()); row.add(productInfo.getTaxRate() == null ? "" : productInfo.getTaxRate());
data.add(row); data.add(row);
} }

View File

@ -33,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.InputStream; import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
@ -59,6 +60,13 @@ public class QuotationServiceImpl implements IQuotationService {
@Autowired @Autowired
private IQuotationProductInfoService quotationProductInfoService; 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 @Autowired
@Lazy @Lazy
private IProjectInfoService projectInfoService; private IProjectInfoService projectInfoService;
@ -464,12 +472,12 @@ public class QuotationServiceImpl implements IQuotationService {
row.add(item.getModel()); row.add(item.getModel());
row.add(item.getProductDesc()); row.add(item.getProductDesc());
row.add(item.getQuantity()); 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.getGuidanceDiscount()==null?"":item.getGuidanceDiscount().multiply(new BigDecimal("100")));
row.add(item.getDiscount()==null?"":item.getDiscount().multiply(new BigDecimal("100"))); row.add(item.getDiscount()==null?"":item.getDiscount().multiply(new BigDecimal("100")));
row.add(item.getPrice()); row.add(formatAmount(item.getPrice()));
row.add(item.getAllPrice()); row.add(formatAmount(item.getAllPrice()));
row.add(item.getCatalogueAllPrice()); row.add(formatAmount(item.getCatalogueAllPrice()));
row.add(""); // CID信息 row.add(""); // CID信息
row.add(item.getRemark()); row.add(item.getRemark());
rows.add(row); rows.add(row);
@ -493,8 +501,8 @@ public class QuotationServiceImpl implements IQuotationService {
subTotalRow1.add(""); subTotalRow1.add("");
subTotalRow1.add(""); subTotalRow1.add("");
subTotalRow1.add(""); subTotalRow1.add("");
subTotalRow1.add(sumAllPrice); subTotalRow1.add(formatAmount(sumAllPrice));
subTotalRow1.add(sumCatalogueAllPrice); subTotalRow1.add(formatAmount(sumCatalogueAllPrice));
subTotalRow1.add(""); subTotalRow1.add("");
subTotalRow1.add(""); subTotalRow1.add("");
rows.add(subTotalRow1); rows.add(subTotalRow1);
@ -509,9 +517,8 @@ public class QuotationServiceImpl implements IQuotationService {
subTotalRow2.add(""); subTotalRow2.add("");
subTotalRow2.add(""); subTotalRow2.add("");
subTotalRow2.add(""); subTotalRow2.add("");
subTotalRow2.add(""); subTotalRow2.add(formatAmount(sumAllPrice));
subTotalRow2.add(sumAllPrice); subTotalRow2.add(formatAmount(sumCatalogueAllPrice));
subTotalRow2.add(sumCatalogueAllPrice);
subTotalRow2.add(""); subTotalRow2.add("");
subTotalRow2.add(""); subTotalRow2.add("");
rows.add(subTotalRow2); rows.add(subTotalRow2);

View File

@ -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;
}
}

View File

@ -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="projectCode != null and projectCode != ''">and t3.project_code = #{projectCode}</if>
<if test="projectName != null and projectName != ''">and t3.project_name = #{projectName}</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="orderCode != null and orderCode != ''">and t1.order_code like concat( #{orderCode},'%')</if>
<if test="inventoryCode != null and inventoryCode != ''">and t1.inventory_code like concat( <if test="inventoryCode != null and inventoryCode != ''">and t1.inventory_code like concat('%', #{inventoryCode}, '%')</if>
#{inventoryCode},'%')
</if>
<if test="productType != null and productType != ''">and t1.product_type = #{productType}</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="productCode != null and productCode != ''">and t1.product_code = #{productCode}</if>
<if test="totalPriceWithTax != null ">and t1.total_price_with_tax = #{totalPriceWithTax}</if> <if test="totalPriceWithTax != null ">and t1.total_price_with_tax = #{totalPriceWithTax}</if>

View File

@ -78,6 +78,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<delete id="deleteByPaymentCode"> <delete id="deleteByPaymentCode">
delete from oms_payable_payment_detail where payment_bill_code = #{paymentBillCode} delete from oms_payable_payment_detail where payment_bill_code = #{paymentBillCode}
</delete> </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 id="list" resultType="com.ruoyi.sip.domain.OmsPayablePaymentDetail">
SELECT SELECT

View File

@ -44,8 +44,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectInventoryDeliveryList" parameterType="InventoryDelivery" resultMap="InventoryDeliveryResult"> <select id="selectInventoryDeliveryList" parameterType="InventoryDelivery" resultMap="InventoryDeliveryResult">
<include refid="selectInventoryDeliveryVo"/> <include refid="selectInventoryDeliveryVo"/>
<where> <where>
<!-- 默认排除已撤回记录;显式指定状态查询时(如查看已撤回记录)不排除 --> <!-- 撤回/作废后的记录需要保留可见,不再默认排除已撤回(delivery_status='2')记录;
<if test="deliveryStatus == null or deliveryStatus == ''">and t1.delivery_status != 2</if> 指定 deliveryStatus 时按其过滤 -->
<if test="deliveryStatus != null and deliveryStatus != ''">and t1.delivery_status = #{deliveryStatus}</if> <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="outerCode != null and outerCode != ''">and t1.outer_code = #{outerCode}</if>
<if test="warehouseId != null ">and t1.warehouse_id = #{warehouseId}</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} and t1.id = #{id}
</select> </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 &lt;&gt; #{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> </mapper>

View File

@ -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 product_sn from oms_inventory_delivery_detail where delivery_id = #{id}
) )
</select> </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"> <select id="selectInventoryInfoByOrderCode" resultType="com.ruoyi.sip.domain.InventoryInfo">
<include refid="selectInventoryInfoVo"/> <include refid="selectInventoryInfoVo"/>
where t1.outer_code in ( where t1.outer_code in (

View File

@ -127,6 +127,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
t1.product_code, t1.product_code,
t1.quantity, t1.quantity,
t1.outer_status, t1.outer_status,
t1.delivery_status,
t1.order_code, t1.order_code,
t1.contact_person, t1.contact_person,
t1.contact_phone, t1.contact_phone,

View File

@ -342,4 +342,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
delete from oms_payable_ticket_detail where ticket_bill_code = #{code} delete from oms_payable_ticket_detail where ticket_bill_code = #{code}
</delete> </delete>
<!-- 按应付单主键删除其收票明细(删除应付单时级联清理,避免孤儿数据) -->
<delete id="deleteByPayableBillId" parameterType="Long">
delete from oms_payable_ticket_detail where payable_bill_id = #{payableBillId}
</delete>
</mapper> </mapper>

View File

@ -164,4 +164,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach> </foreach>
</delete> </delete>
<!-- 按应付单主键删除其收票计划(删除应付单时级联清理,避免孤儿数据) -->
<delete id="deleteByPayableBillId" parameterType="Long">
delete from oms_payable_ticket_plan where payable_bill_id = #{payableBillId}
</delete>
</mapper> </mapper>

View File

@ -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="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="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="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="productType != null and productType != ''"> and t1.product_type = #{productType}</if>
<if test="productCode != null and productCode != ''"> and t1.product_code = #{productCode}</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> <if test="createBy != null and createBy != ''"> and t1.create_by = #{createBy}</if>

View File

@ -217,4 +217,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<!-- 按应收单主键删除其开票明细(删除应收单时级联清理,避免孤儿数据) -->
<delete id="deleteByReceivableBillId" parameterType="Long">
delete from oms_receivable_invoice_detail where receivable_bill_id = #{receivableBillId}
</delete>
</mapper> </mapper>

View File

@ -99,4 +99,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<delete id="deleteByReceivableBillId" parameterType="Long">
delete from oms_receivable_invoice_plan where receivable_bill_id = #{receivableBillId}
</delete>
</mapper> </mapper>

View File

@ -219,4 +219,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
from oms_receivable_receipt_detail from oms_receivable_receipt_detail
where receipt_bill_code = #{receiptBillCode} where receipt_bill_code = #{receiptBillCode}
</delete> </delete>
<!-- 按应收单主键删除其收款明细(删除应收单时级联清理,避免孤儿数据) -->
<delete id="deleteByReceivableBillId" parameterType="Long">
delete from oms_receivable_receipt_detail where receivable_bill_id = #{receivableBillId}
</delete>
</mapper> </mapper>

View File

@ -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;