From 3bac9b647218ea583481ee847543b0ec1e5ab6f8 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Sat, 5 Sep 2026 00:04:46 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=BB=93=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/payment/paymentApplication.js | 2 + .../components/billing-plan-editor.vue | 2 +- .../components/master-order-dispatch.vue | 123 +++- .../components/master-order-editor.vue | 27 +- src/views/business/contract-manage-change.vue | 2 +- src/views/business/loading-manage.vue | 60 +- src/views/business/master-order.vue | 2 +- src/views/payment/payment-application.vue | 2 +- .../components/formal-settlement-editor.vue | 674 ++++++++++++------ .../components/pre-settlement-editor.vue | 56 +- .../settlement/receivable-payable-detail.vue | 90 +-- 11 files changed, 688 insertions(+), 352 deletions(-) diff --git a/src/api/payment/paymentApplication.js b/src/api/payment/paymentApplication.js index c51a9de..7c48a0d 100644 --- a/src/api/payment/paymentApplication.js +++ b/src/api/payment/paymentApplication.js @@ -18,6 +18,8 @@ export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'p export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data }); export const syncKingdee = id => request({ url: `${baseUrl}/sync-kingdee`, method: 'post', params: { id } }); +export const syncKingdeeBatch = ids => + request({ url: `${baseUrl}/sync-kingdee-batch`, method: 'post', data: ids }); export const paymentTypeOptions = [ { label: '项目预付', value: 'project_advance' }, diff --git a/src/views/business/components/billing-plan-editor.vue b/src/views/business/components/billing-plan-editor.vue index 7e1f6a4..691d522 100644 --- a/src/views/business/components/billing-plan-editor.vue +++ b/src/views/business/components/billing-plan-editor.vue @@ -691,7 +691,7 @@ export default { return value !== undefined && value !== null && String(value).trim() !== ''; }, taxRateInput(row, value) { - const text = String(value || '').replace(/[^\d.]/g, ''); + const text = String(value ?? '').replace(/[^\d.]/g, ''); const parts = text.split('.'); row.taxRate = parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; diff --git a/src/views/business/components/master-order-dispatch.vue b/src/views/business/components/master-order-dispatch.vue index 52c1656..f03f611 100644 --- a/src/views/business/components/master-order-dispatch.vue +++ b/src/views/business/components/master-order-dispatch.vue @@ -71,12 +71,12 @@ - + - + @@ -84,7 +84,7 @@ - + @@ -109,6 +109,7 @@ @@ -171,7 +172,7 @@
- 待提交 {{ pending.length }}({{ pendingSegmentSummary }}) + 待提交 {{ pendingBatchCount }}({{ pendingSegmentSummary }}) {{ pendingExpanded ? '收起待调度列表' : '展开待调度列表' }}
取消确认调度
@@ -179,7 +180,7 @@
@@ -216,13 +217,28 @@ export default { goodsTypes() { return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))].join('、') || '-'; }, masterGoods() { return (this.master?.goods || []).map((item, sourceIndex) => ({ ...item, sourceIndex, label: [item.cargoName, item.cargoType].filter(Boolean).join(' / ') })); }, dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; }, - pendingGroups() { return this.pending.reduce((groups, item) => { (groups[item.segmentNo] ||= []).push(item); return groups; }, {}); }, + pendingGroups() { + return this.pending.reduce((groups, item, index) => { + const segmentRows = (groups[item.segmentNo] ||= []); + const batchKey = item.batchNo || item.id || `${item.segmentNo}-${index}`; + let batch = segmentRows.find(row => row.batchKey === batchKey); + if (!batch) { + batch = { ...item, batchKey, items: [] }; + segmentRows.push(batch); + } + batch.items.push(item); + return groups; + }, {}); + }, + pendingBatchCount() { + return Object.values(this.pendingGroups).reduce((count, rows) => count + rows.length, 0); + }, pendingSegmentSummary() { return this.routes.map(route => { - const items = this.pending.filter(item => item.segmentNo === route.segmentNo); - if (!items.length) return `${route.segmentNo}:无`; - const documentSummary = items.reduce((result, item) => { - result[item.documentType] = (result[item.documentType] || 0) + 1; + const batches = this.pendingGroups[route.segmentNo] || []; + if (!batches.length) return `${route.segmentNo}:无`; + const documentSummary = batches.reduce((result, batch) => { + result[batch.documentType] = (result[batch.documentType] || 0) + 1; return result; }, {}); return `${route.segmentNo}:${Object.entries(documentSummary).map(([type, count]) => `${type} x ${count}`).join('、')}`; @@ -506,6 +522,26 @@ export default { return sum + this.quantityNumber(item.quantity); }, 0); }, + validatePreviousSegmentCompletedQuantity(route, goods) { + const routeIndex = this.routes.indexOf(route); + if (routeIndex <= 0) return true; + const previousCompletedQuantity = this.quantityNumber( + this.routes[routeIndex - 1]?.arrivedQuantity + ); + const currentDispatchQuantity = (goods || []).reduce( + (sum, item) => sum + this.quantityNumber(item.dispatchQuantity), + 0 + ); + const dispatchTotal = + this.quantityNumber(route.dispatchedQuantity) + + this.pendingSegmentQuantity(route) + + currentDispatchQuantity; + if (dispatchTotal <= previousCompletedQuantity) return true; + this.$message.warning( + `${route.segmentNo}调度总量不能大于上一段已完成数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})` + ); + return false; + }, pendingGoodsQuantity(route, row) { const goodsIndex = this.masterGoodsIndex(row); return this.pending.reduce((sum, item) => { @@ -798,6 +834,7 @@ export default { if (!goods.length) return this.$message.warning('请填写本次数量'); if (!this.validateDispatchGoods(route, goods)) return; if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量'); + if (!this.validatePreviousSegmentCompletedQuantity(route, goods)) return; this.syncFreightItems(route); if (!this.validateFreightItems(route)) return; if (!this.validateRoutePhones(route)) return; @@ -805,7 +842,7 @@ export default { if (this.isRoad(route)) { if (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName || !route.vehicleNo)) return this.$message.warning('请选择承运商并填写车牌号'); if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo)) return this.$message.warning('请补全自运或网货平台的车辆与人员信息'); - } else if (!route.vehicleNo || !route.driverPhone || (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName))) { + } else if (!route.vehicleNo || (route.carrierType === '承运商' && (!route.carrierContractId || !route.carrierName))) { return this.$message.warning('请补全非公路运输的承运信息'); } } @@ -829,35 +866,59 @@ export default { } this.$message.success('已加入调度清单'); }, - editPending(item) { - const route = this.routes.find(routeItem => routeItem.segmentNo === item.segmentNo); + pendingGoodsText(batch = {}) { + return (batch.items || [batch]) + .map(item => [item.cargoName, item.cargoType].filter(Boolean).join(',')) + .join(' | '); + }, + pendingQuantityText(batch = {}) { + return (batch.items || [batch]) + .map(item => `${this.formatQuantity(item.quantity)} ${item.quantityUnit || ''}`.trim()) + .join(' | '); + }, + editPending(batch) { + const items = batch.items || [batch]; + const first = items[0]; + const route = this.routes.find(routeItem => routeItem.segmentNo === first.segmentNo); if (!route) return; - const itemSourceIndex = Number(item.sourceIndex); - const sourceIndex = Number.isInteger(itemSourceIndex) && itemSourceIndex >= 0 - ? itemSourceIndex - : (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType); - let goods = route.goods.find(goodsItem => String(goodsItem.sourceIndex) === String(sourceIndex)); - if (!goods && sourceIndex >= 0) { - goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex); - route.goods.push(goods); - } - if (goods) goods.dispatchQuantity = item.quantity; - Object.assign(route, item); + Object.assign(route, first); + const editedGoods = items.map(item => { + const itemSourceIndex = Number(item.sourceIndex); + const sourceIndex = Number.isInteger(itemSourceIndex) && itemSourceIndex >= 0 + ? itemSourceIndex + : (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType); + let goods = route.goods.find(goodsItem => String(goodsItem.sourceIndex) === String(sourceIndex)); + if (!goods && sourceIndex >= 0) { + goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex); + route.goods.push(goods); + } + if (goods) goods.dispatchQuantity = item.quantity; + return { goods, item }; + }); route.currency = this.normalizeCurrencyValue( this.master?.settlementCurrency || route.currency || 'RMB' ); this.syncFreightItems(route); - const freightItem = this.freightItemsForGoods(route, goods); - if (freightItem) { freightItem.unitPrice = item.unitPrice || ''; freightItem.priceUnit = item.priceUnit || freightItem.priceUnit; } - this.removePending(item.id); - this.editingId = item.id; + editedGoods.forEach(({ goods, item }) => { + const freightItem = this.freightItemsForGoods(route, goods); + if (freightItem) { + freightItem.unitPrice = item.unitPrice || ''; + freightItem.priceUnit = item.priceUnit || freightItem.priceUnit; + } + }); + this.removePendingBatch(batch); + this.editingId = batch.batchKey; route.selected = true; - this.$nextTick(() => document.querySelector(`[data-segment="${item.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })); + this.$nextTick(() => document.querySelector(`[data-segment="${first.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })); }, freightItemsForGoods(route, goods) { return (route.freightItems || []).find(item => String(item.sourceIndex) === String(goods?.sourceIndex)) || {}; }, - removePending(id) { this.pending = this.pending.filter(item => item.id !== id); if (this.editingId === id) this.editingId = null; }, + removePendingBatch(batch = {}) { + const ids = new Set((batch.items || [batch]).map(item => item.id)); + this.pending = this.pending.filter(item => !ids.has(item.id)); + if (this.editingId === batch.batchKey) this.editingId = null; + }, validateRoutePhones(route = {}) { const invalidPhone = [ [route.departurePhone, '发货联系方式'], diff --git a/src/views/business/components/master-order-editor.vue b/src/views/business/components/master-order-editor.vue index 0b153fe..440d1ab 100644 --- a/src/views/business/components/master-order-editor.vue +++ b/src/views/business/components/master-order-editor.vue @@ -228,9 +228,9 @@ >
-

附件

-
-
+
+

附件

+
+
+
+ + + @@ -1006,6 +1019,7 @@ export default { const first = this.contracts[0]; this.form.contractId = first?.id; this.form.contractName = first?.contractName || ''; + this.form.customerName = first?.partyA || ''; } }, async projectChange(projectId) { @@ -1019,6 +1033,7 @@ export default { contractChange(contractId) { const contract = this.contracts.find(item => String(item.id) === String(contractId)); this.form.contractName = contract?.contractName || ''; + this.form.customerName = contract?.partyA || ''; }, addressModel(target) { if (target.startsWith('route-')) { @@ -1939,12 +1954,6 @@ export default { .master-editor__attachment { width: 100%; } -.master-editor__attachment-head { - display: flex; - align-items: center; - justify-content: flex-end; - margin-bottom: 16px; -} .master-editor__attachment-upload { display: flex; justify-content: flex-start; diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue index 4d38472..556229d 100644 --- a/src/views/business/contract-manage-change.vue +++ b/src/views/business/contract-manage-change.vue @@ -161,7 +161,7 @@ export default { handleContractFileBatchDownload() { (this.selectedContractFiles.length ? this.selectedContractFiles : this.contractFileRows).forEach(row => { if (row.url) window.open(row.url, '_blank'); }); }, formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; }, addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `第${this.paymentRatioRows.length + 1}笔`, ratioLimit: '', remark: '' }); }, - async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); this.$message.success('变更已提交'); this.$router.back(); }, + async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); this.$message.success('变更已提交'); this.$router.back(); }, }, }; diff --git a/src/views/business/loading-manage.vue b/src/views/business/loading-manage.vue index a42bcf5..0203d79 100644 --- a/src/views/business/loading-manage.vue +++ b/src/views/business/loading-manage.vue @@ -252,7 +252,7 @@ > - + -
+ @@ -677,98 +677,157 @@ - - - {{ formatMoney(detailAmountAdjust.originalAmount) }} - - - - - - {{ formatMoney(detailAmountAdjustSettlementAmount) }} - - - - - - - - + + - + + + + + + + - - - - + + + + + + + + + + + @@ -786,7 +845,10 @@ import { getReceiptClaims, save, } from '@/api/settlement/formalSettlement'; -import { getDetail as getPreSettlementDetail } from '@/api/settlement/preSettlement'; +import { + getDetail as getPreSettlementDetail, + getDetailFees as getPreSettlementDetailFees, +} from '@/api/settlement/preSettlement'; import { createFormalSettlementForm, formalSettlementFormFields, @@ -800,12 +862,14 @@ import { } from '@/option/settlement/formalSettlementTable'; import { getDictionary } from '@/api/system/dictbiz'; import { h } from 'vue'; +import { InfoFilled } from '@element-plus/icons-vue'; import { mapGetters } from 'vuex'; import { downloadFileByUrl } from '@/utils/util'; import * as XLSX from 'xlsx'; export default { name: 'FormalSettlementEditor', + components: { InfoFilled }, props: { modelValue: Boolean, recordId: [String, Number], @@ -893,12 +957,11 @@ export default { detailId: null, reason: '', rows: [], + targetRow: null, }, - detailAmountAdjust: { + billingRuleDialog: { visible: false, - row: null, - originalAmount: 0, - adjustAmount: 0, + rule: null, }, detailCollapsed: false, detailQuery: { @@ -950,14 +1013,6 @@ export default { summaryTotal() { return this.summaryFees.reduce((total, row) => total + Number(row.settlementAmount || 0), 0); }, - detailAmountAdjustSettlementAmount() { - return Number( - ( - Number(this.detailAmountAdjust.originalAmount || 0) + - Number(this.detailAmountAdjust.adjustAmount || 0) - ).toFixed(2) - ); - }, filteredDetails() { return this.details.filter(row => Object.entries(this.appliedDetailQuery).every(([field, keyword]) => { @@ -972,6 +1027,13 @@ export default { const missing = this.getMissingAttachmentTypes(); return missing.length ? `未上传:${missing.join('、')}` : ''; }, + adjustFeeItemNames() { + const names = new Set(); + this.adjust.rows.forEach(row => { + Object.keys(row.feeItems || {}).forEach(name => names.add(name)); + }); + return Array.from(names); + }, }, watch: { modelValue: { @@ -993,6 +1055,34 @@ export default { }, }, methods: { + async loadFormalDetail(id) { + const response = await getDetail(id); + const data = this.unwrapData(response); + this.form = { + ...createFormalSettlementForm(), + ...data, + sourcePreSettlementIds: (data.sources || []).map(item => item.preSettlementId), + sourceDetailIds: (data.details || []) + .filter(item => !item.sourcePreSettlementId) + .map(item => item.sourceDetailId), + }; + this.sources = data.sources || []; + this.details = data.details || []; + this.summaryFees = data.summaryFees || []; + this.paymentApplications = data.paymentApplications || []; + if (this.readonly && this.form.settlementType === 'receivable') { + this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || []; + } + this.adjustments = data.adjustments || []; + this.changeRecords = data.changeRecords || []; + this.attachments = this.parseAttachments(data.attachmentsJson); + this.selectedAttachmentRows = []; + this.invoices = data.invoices || []; + this.sortAttachments(); + this.contracts = this.allContracts.filter( + item => String(item.projectId) === String(this.form.projectId) + ); + }, async initialize() { const newRecordAudit = this.recordId ? null @@ -1014,11 +1104,9 @@ export default { this.invoices = []; this.invoiceClaimNo = ''; this.attachmentUploadFiles = []; - this.detailAmountAdjust = { + this.billingRuleDialog = { visible: false, - row: null, - originalAmount: 0, - adjustAmount: 0, + rule: null, }; this.detailCollapsed = false; this.resetDetailQuery(false); @@ -1037,32 +1125,7 @@ export default { } this.loading = true; try { - const response = await getDetail(this.recordId); - const data = this.unwrapData(response); - this.form = { - ...createFormalSettlementForm(), - ...data, - sourcePreSettlementIds: (data.sources || []).map(item => item.preSettlementId), - sourceDetailIds: (data.details || []) - .filter(item => !item.sourcePreSettlementId) - .map(item => item.sourceDetailId), - }; - this.sources = data.sources || []; - this.details = data.details || []; - this.summaryFees = data.summaryFees || []; - this.paymentApplications = data.paymentApplications || []; - if (this.readonly && this.form.settlementType === 'receivable') { - this.receiptClaims = this.unwrapData(await getReceiptClaims(this.recordId)) || []; - } - this.adjustments = data.adjustments || []; - this.changeRecords = data.changeRecords || []; - this.attachments = this.parseAttachments(data.attachmentsJson); - this.selectedAttachmentRows = []; - this.invoices = data.invoices || []; - this.sortAttachments(); - this.contracts = this.allContracts.filter( - item => String(item.projectId) === String(this.form.projectId) - ); + await this.loadFormalDetail(this.recordId); } finally { this.loading = false; } @@ -1339,7 +1402,6 @@ export default { const range = this.detailCandidate.query.createTimeRange || []; const params = { contractId: this.form.contractId, - settlementType: this.form.settlementType, batchNo: this.detailCandidate.query.batchNo, createStartDate: range[0], createEndDate: range[1], @@ -1529,66 +1591,228 @@ export default { )}.xlsx` ); }, + billingRuleName(row) { + return this.normalizeBillingRule(row).name; + }, + openBillingRuleInfo(row) { + this.billingRuleDialog.rule = this.normalizeBillingRule(row); + this.billingRuleDialog.visible = true; + }, + normalizeBillingRule(row) { + const source = row || {}; + const nested = [ + source.billingRule, + source.billingRuleInfo, + source.billingRuleJson, + source.billingRulesJson, + source.billingRules, + source.billingPlanRule, + source.feeRule, + source.rule, + ].find(value => value !== undefined && value !== null && value !== ''); + const parsedNested = this.parseJsonValue(nested); + const matchedRules = Array.isArray(parsedNested) + ? parsedNested.filter(item => item && typeof item === 'object') + : []; + const rule = this.parseObject(matchedRules[0] || parsedNested || nested); + const matchCondition = this.parseObject( + source.matchCondition ?? source.billingMatchCondition ?? rule.matchCondition + ); + const ruleNames = matchedRules + .map(item => item.ruleName || item.name || item.billingRuleName || item.feeItem) + .filter(Boolean); + return { + ...rule, + name: + source.billingRuleName || + source.feeRuleName || + source.ruleName || + source.billingPlanRuleName || + source.billingPlanName || + rule.name || + rule.ruleName || + rule.billingRuleName || + rule.planName || + rule.billingPlanName || + ruleNames.join('、') || + rule.feeItem || + '', + feeType: source.billingFeeType || source.feeType || rule.feeType, + feeItem: source.billingFeeItem || source.feeItem || rule.feeItem, + billingElement: + source.billingElement || + source.billingFactor || + rule.billingElement || + rule.billingFactor, + billingType: source.billingType || rule.billingType, + billingUnit: source.billingUnit || source.priceUnit || rule.billingUnit || rule.priceUnit, + unitPrice: source.billingUnitPrice ?? source.unitPrice ?? rule.unitPrice, + minimumBillingWeight: + source.minimumBillingWeight ?? rule.minimumBillingWeight ?? rule.lowerLimit, + remark: source.billingRuleRemark || source.ruleRemark || rule.remark, + rules: matchedRules, + matchCondition: { + ...matchCondition, + origin: matchCondition.origin || matchCondition.originName, + destination: matchCondition.destination || matchCondition.destinationName, + transportMode: matchCondition.transportMode || matchCondition.transportType, + cargoType: matchCondition.cargoType || matchCondition.cargoTypeName, + }, + }; + }, + billingRuleRanges(rule) { + const ranges = Array.isArray(rule?.limitRanges) ? rule.limitRanges : []; + if (ranges.length) { + return ranges + .map( + item => `${item.lowerLimit ?? '-'}~${item.upperLimit ?? '-'}:${item.unitPrice ?? '-'}` + ) + .join(';'); + } + if (rule?.lowerLimit !== undefined || rule?.upperLimit !== undefined) { + return `${rule.lowerLimit ?? '-'}~${rule.upperLimit ?? '-'}`; + } + return '-'; + }, + recalculateAdjustRow(row, changedField) { + if (changedField && this.isFreightFeeItem(changedField)) { + row.freightAmount = Number(row.feeItems[changedField] || 0); + } + const feeItemTotal = Object.values(row.feeItems || {}).reduce( + (total, value) => total + Number(value || 0), + 0 + ); + const hasFreightItem = Object.keys(row.feeItems || {}).some(this.isFreightFeeItem); + row.settlementAmountTax = Number( + (hasFreightItem ? feeItemTotal : Number(row.freightAmount || 0) + feeItemTotal).toFixed(2) + ); + }, async openAdjustDialog(row) { if (!row.formalSettlementId) { - const originalAmount = Number( - row.originalAmount ?? - row.totalAmount ?? - Number(row.settlementAmountTax || 0) - Number(row.adjustAmount || 0) - ); - this.detailAmountAdjust = { + this.adjust = { visible: true, - row, - originalAmount: Number(originalAmount.toFixed(2)), - adjustAmount: Number(row.adjustAmount || 0), + loading: true, + saving: false, + detailId: null, + reason: '', + rows: [], + targetRow: row, }; + try { + let feeRows = []; + if (row.sourcePreSettlementId && row.id) { + const response = await getPreSettlementDetailFees(row.id); + feeRows = this.unwrapData(response) || []; + } + this.adjust.rows = (feeRows.length ? feeRows : [row]).map(item => ({ + ...item, + feeItems: this.parseFeeItems(item.feeItemsJson || item.feeItems), + billingRule: this.normalizeBillingRule(item), + settlementAmountTax: + item.settlementAmountTax ?? + item.totalAmount ?? + item.afterAmount ?? + item.settlementAmount ?? + 0, + })); + } finally { + this.adjust.loading = false; + } return; } + const detailRow = row; this.adjust = { visible: true, loading: true, saving: false, - detailId: row.id, + detailId: detailRow.id, reason: '', rows: [], + targetRow: null, }; try { - const response = await getDetailFees(row.id); + const response = await getDetailFees(detailRow.id); const data = this.unwrapData(response); this.adjust.rows = (data || []).map(item => ({ ...item, feeItems: this.parseFeeItems(item.feeItemsJson), + billingRule: this.normalizeBillingRule(item), })); } finally { this.adjust.loading = false; } }, - confirmDetailAmountAdjustment() { - const settlementAmount = this.detailAmountAdjustSettlementAmount; - if (!Number.isFinite(settlementAmount) || settlementAmount < 0) { - return this.$message.warning('调整后的结算金额不能小于0'); - } - const row = this.detailAmountAdjust.row; - if (!row) return; - row.originalAmount = this.detailAmountAdjust.originalAmount; - row.adjustAmount = Number(this.detailAmountAdjust.adjustAmount || 0); - row.settlementAmountTax = settlementAmount; - row.pendingDetailAdjustment = true; - this.buildSummaryFeesFromDetails(); - this.detailAmountAdjust.visible = false; - }, async saveAdjustment() { if (!this.adjust.reason.trim()) return this.$message.warning('请输入调整原因'); + if (!this.adjust.detailId) { + const target = this.adjust.targetRow; + const edited = this.adjust.rows[0]; + if (!target || !edited) return; + const originalAmount = Number( + target.originalAmount ?? + target.totalAmount ?? + target.afterAmount ?? + target.settlementAmountTax ?? + target.settlementAmount ?? + 0 + ); + const settlementAmount = Number( + this.adjust.rows + .reduce((total, item) => total + Number(item.settlementAmountTax || 0), 0) + .toFixed(2) + ); + const feeItems = this.adjust.rows.reduce((result, item) => { + Object.entries(item.feeItems || {}).forEach(([name, amount]) => { + result[name] = Number((Number(result[name] || 0) + Number(amount || 0)).toFixed(2)); + }); + return result; + }, {}); + if (!Number.isFinite(settlementAmount) || settlementAmount < 0) { + return this.$message.warning('调整后的结算金额不能小于0'); + } + Object.assign(target, { + originalAmount, + adjustAmount: Number((settlementAmount - originalAmount).toFixed(2)), + settlementAmountTax: settlementAmount, + feeItemsJson: JSON.stringify(feeItems), + freightAmount: this.adjust.rows.reduce( + (total, item) => total + Number(item.freightAmount || 0), + 0 + ), + transportQuantity: this.adjust.rows.reduce( + (total, item) => total + Number(item.transportQuantity || 0), + 0 + ), + mileage: this.adjust.rows.reduce((total, item) => total + Number(item.mileage || 0), 0), + unitPrice: edited.unitPrice, + remark: edited.remark, + pendingDetailAdjustment: true, + }); + this.buildSummaryFeesFromDetails(); + this.adjust.visible = false; + this.$message.success('调整已暂存,请保存正式结算单提交'); + return; + } this.adjust.saving = true; try { await adjustDetail({ detailId: this.adjust.detailId, changeReason: this.adjust.reason, - rows: this.adjust.rows, + rows: this.adjust.rows.map(row => ({ + id: row.id, + transportQuantity: row.transportQuantity, + mileage: row.mileage, + unitPrice: row.unitPrice, + freightAmount: row.freightAmount, + feeItems: row.feeItems, + settlementAmountTax: row.settlementAmountTax, + settlementAmountNoTax: row.settlementAmountNoTax, + remark: row.remark, + })), }); this.$message.success('调整保存成功'); this.adjust.visible = false; - await this.initialize(); + await this.loadFormalDetail(this.form.id); } finally { this.adjust.saving = false; } @@ -1715,6 +1939,53 @@ export default { if (!url) return this.$message.warning('当前Mock发票暂无可下载附件'); downloadFileByUrl(url, this.invoiceAttachmentName(row)); }, + buildSavePayload() { + return { + id: this.form.id, + contractId: this.form.contractId, + settlementType: this.form.settlementType, + sourcePreSettlementIds: this.form.sourcePreSettlementIds, + sourceDetailIds: this.form.sourceDetailIds, + detailAdjustments: this.details + .filter(row => row.pendingDetailAdjustment) + .map(row => ({ + sourcePreSettlementDetailId: + row.sourcePreSettlementDetailId || (row.sourcePreSettlementId ? row.id : undefined), + sourceDetailId: row.sourcePreSettlementId ? undefined : row.sourceDetailId || row.id, + adjustAmount: Number(row.adjustAmount || 0), + })), + exchangeRateDate: this.form.exchangeRateDate, + exchangeRate: this.form.exchangeRate, + summaryFees: this.summaryFees + .filter( + row => + row.manualFlag === 1 || + row.id || + Number(row.adjustAmount || 0) !== 0 || + String(row.remark || '').trim() + ) + .map(row => ({ + id: row.id || undefined, + feeType: row.feeType, + feeItem: row.feeItem, + adjustAmount: Number(row.adjustAmount || 0), + remark: row.remark, + manualFlag: row.manualFlag || 0, + })), + attachmentsJson: this.stringifyAttachments(this.attachments), + invoices: this.invoices.map(row => ({ + invoiceNo: String(row.invoiceNo || '').trim(), + invoiceDate: row.invoiceDate || null, + invoiceType: row.invoiceType || '', + taxRate: Number(row.taxRate || 0), + invoiceAmount: Number(row.invoiceAmount || 0), + availableInvoiceAmount: Number(row.availableInvoiceAmount || 0), + matchedAmount: Number(row.matchedAmount || 0), + attachmentJson: row.attachmentJson || '', + })), + remark: this.form.remark, + }; + }, async handleSave() { await this.$refs.formRef.validate(); if (!this.form.sourcePreSettlementIds.length && !this.form.sourceDetailIds.length) { @@ -1732,51 +2003,7 @@ export default { if (!this.validateAttachments()) return; this.saving = true; try { - await save({ - id: this.form.id, - contractId: this.form.contractId, - settlementType: this.form.settlementType, - sourcePreSettlementIds: this.form.sourcePreSettlementIds, - sourceDetailIds: this.form.sourceDetailIds, - detailAdjustments: this.details - .filter(row => row.pendingDetailAdjustment) - .map(row => ({ - sourcePreSettlementDetailId: - row.sourcePreSettlementDetailId || (row.sourcePreSettlementId ? row.id : undefined), - sourceDetailId: row.sourcePreSettlementId ? undefined : row.sourceDetailId || row.id, - adjustAmount: Number(row.adjustAmount || 0), - })), - exchangeRateDate: this.form.exchangeRateDate, - exchangeRate: this.form.exchangeRate, - summaryFees: this.summaryFees - .filter( - row => - row.manualFlag === 1 || - row.id || - Number(row.adjustAmount || 0) !== 0 || - String(row.remark || '').trim() - ) - .map(row => ({ - id: row.id || undefined, - feeType: row.feeType, - feeItem: row.feeItem, - adjustAmount: Number(row.adjustAmount || 0), - remark: row.remark, - manualFlag: row.manualFlag || 0, - })), - attachmentsJson: this.stringifyAttachments(this.attachments), - invoices: this.invoices.map(row => ({ - invoiceNo: String(row.invoiceNo || '').trim(), - invoiceDate: row.invoiceDate || null, - invoiceType: row.invoiceType || '', - taxRate: Number(row.taxRate || 0), - invoiceAmount: Number(row.invoiceAmount || 0), - availableInvoiceAmount: Number(row.availableInvoiceAmount || 0), - matchedAmount: Number(row.matchedAmount || 0), - attachmentJson: row.attachmentJson || '', - })), - remark: this.form.remark, - }); + await save(this.buildSavePayload()); this.$message.success('保存成功'); this.visible = false; this.$emit('success'); @@ -1994,6 +2221,19 @@ export default { return {}; } }, + parseJsonValue(value) { + if (value === undefined || value === null || value === '') return null; + if (typeof value === 'object') return value; + try { + return JSON.parse(value); + } catch { + return value; + } + }, + parseObject(value) { + const parsed = this.parseJsonValue(value); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + }, parseAttachments(value) { if (!value) return []; let attachments = value; @@ -2102,8 +2342,37 @@ export default { .formal-editor__adjust-reason { margin-top: 16px; } -.formal-editor__amount-adjust :deep(.el-input-number) { - width: 100%; +.formal-editor__billing-rule-cell { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + max-width: 100%; +} +.formal-editor__billing-rule-icon { + flex: 0 0 auto; + color: var(--el-color-primary); + cursor: pointer; +} +.formal-editor__billing-rule-detail { + margin-top: 12px; +} +.formal-editor__billing-rule-match-title { + margin-top: 20px; +} +.dialog-section-title { + display: flex; + align-items: center; + min-height: 22px; + font-size: 16px; + font-weight: 600; +} +.dialog-section-title::before { + width: 4px; + height: 18px; + margin-right: 8px; + background: #409eff; + content: ''; } .formal-editor__attachment-missing { margin-left: 12px; @@ -2111,11 +2380,6 @@ export default { font-size: 13px; font-weight: 400; } -.formal-editor__attachment-actions { - display: flex; - justify-content: flex-end; - margin-bottom: 12px; -} .formal-editor__attachment-upload { margin-top: 12px; } diff --git a/src/views/settlement/components/pre-settlement-editor.vue b/src/views/settlement/components/pre-settlement-editor.vue index dda20db..8d1b40a 100644 --- a/src/views/settlement/components/pre-settlement-editor.vue +++ b/src/views/settlement/components/pre-settlement-editor.vue @@ -82,7 +82,12 @@ 添加费用 - + -
- 合计:{{ formatMoney(summaryTotal, form.currency) }} -
@@ -298,12 +300,12 @@ +
-
- - 批量下载 - -
@@ -846,6 +848,7 @@