From ae01e066ca5a428bfc336fe5af9b9d58ec14bcf6 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 17:58:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + src/modules/inbounds/index.ts | 4 +- .../pages/UnifiedOrdersView.test.ts | 10 +- .../pages/UnifiedOrdersView.vue | 103 +++++++++++++++++- 4 files changed, 112 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9f97338c..673c74ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,6 +67,8 @@ 全量验收按业务从 `../../docs/订单与客户/`、`入库测量与计费/`、`国内货柜履约/`、`财务管理/` 选择最小契约;海外仓业务开发先读 `../../docs/海外仓业务/README.md`。进度、排期或验收状态问题再读取进度表和报告。 +散货托盘码打印:`modules/loading-lists/components/PalletLabelPrintDialog.vue`、`modules/loading-lists/api/platformLoadingLists.ts`,由货柜详情与装柜清单详情进入;先调用 `/pallet-labels/prepare` 获取服务端按 FBA 仓分组的冻结清单,再通过 `/pallet-labels/print` 下载 PDF。支持全部打印或按仓指定张数补打,一件代发托盘不在该清单,前端不生成或重新编号托盘码。 + ## 海外模块内部快速索引(海外仓全流程) | 关键词 | 页面/路由入口 | API 与联查 | diff --git a/src/modules/inbounds/index.ts b/src/modules/inbounds/index.ts index 0e89cc3a..ee497e10 100644 --- a/src/modules/inbounds/index.ts +++ b/src/modules/inbounds/index.ts @@ -5,13 +5,15 @@ export { default as InboundMeasurementDialog } from './measurement/InboundMeasur export { default as MeasurementEvidenceThumbnail } from './measurement/components/MeasurementEvidenceThumbnail.vue' export { default as InboundFeePanel } from './fee/InboundFeePanel.vue' export { + adjustPublishedInboundFee, calculateInboundFee, confirmInboundFee, + createFeeAdjustmentIdempotencyKey, createFeeActionIdempotencyKey, getLatestInboundFee, sendInboundFee, } from './fee/api' -export type { InboundFeeSnapshot } from './fee/types' +export type { AdjustPublishedInboundFeeRequest, InboundFeeSnapshot } from './fee/types' export { downloadWeightImage } from './weight/api/weightOcr' export type { InboundMeasurementDetail } from './measurement/types' export type { InboundHoldCommand, InboundHoldDetail, InboundHoldEvidenceUpload, InboundHoldHistoryEntry } from './core/types' diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index c37c6617..d2dbf48d 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -7,10 +7,14 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/p const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/routes.ts'), 'utf8').replace(/\r\n/g, '\n') describe('UnifiedOrdersView contract', () => { - it('shows the server confirmation lock without reading a stale fee snapshot', () => { + it('loads the reference unit price while rechecking the confirmation lock before sending', () => { const openDialogSource = source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()')) const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange')) - expect(openDialogSource).not.toContain('getLatestInboundFee') + expect(openDialogSource).toContain('getLatestInboundFee') + expect(sendFeeSource).toContain('getLatestInboundFee') + expect(sendFeeSource).toContain('adjustPublishedInboundFee') + expect(source).toContain('价格表参考单价') + expect(source).toContain('本次对客单价') expect(sendFeeSource).toContain("ElMessage.error(getApiErrorDetails(error, '确认费用发送失败').message)") }) @@ -443,7 +447,7 @@ describe('UnifiedOrdersView contract', () => { const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange')) expect(sendFeeSource).not.toContain('saveInboundMeasurementDraft') expect(sendFeeSource).not.toContain('updateMeasurementCalculationMode') - expect(source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()'))).not.toContain('getLatestInboundFee') + expect(source.slice(source.indexOf('async function openFeeConfirmDialog()'), source.indexOf('async function executeConfirmAndSendFee()'))).toContain('getLatestInboundFee') }) it('clears an invalidated measurement loading state before refreshing order details', () => { diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index b45ba476..d0eda136 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -59,9 +59,11 @@ import { unifiedOrderStatusLabel, } from '@/modules/unified-orders/status' import { + adjustPublishedInboundFee, downloadWeightImage, calculateInboundFee, confirmInboundFee, + createFeeAdjustmentIdempotencyKey, createFeeActionIdempotencyKey, getLatestInboundFee, getInboundMeasurement, @@ -75,6 +77,7 @@ import { InboundMeasurementDialog, MeasurementEvidenceThumbnail, sendInboundFee, + type InboundFeeSnapshot, type InboundMeasurementDetail, type InboundHoldDetail, } from '@/modules/inbounds' @@ -146,6 +149,12 @@ const customerFeeTotal = computed(() => ( function feeMoney(value: number) { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: otherFeeSummary.value.currency || 'CNY' }).format(value) } +function feeConfirmMoney(value: number, currency = 'CNY') { + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency, + }).format(value) +} type OtherFeeSummary = { currency: string; approvedReceivable: number; approvedPayable: number } type AutomaticFeeSummary = { feeStatus: string; totalAmount: number } @@ -222,6 +231,25 @@ const confirmFeeSending = ref(false) const feeConfirmOpen = ref(false) const feeConfirmWeight = ref(null) const feeConfirmVolume = ref(null) +const feeConfirmReferenceLoading = ref(false) +const feeConfirmReference = ref(null) +const feeConfirmUnitPrice = ref(null) +const feeConfirmPriceReason = ref('') +const feeConfirmPriceChanged = computed(() => { + const reference = Number(feeConfirmReference.value?.unitPrice) + const current = Number(feeConfirmUnitPrice.value) + return reference >= 0 && current >= 0 && Math.abs(reference - current) > 0.000001 +}) +const feeConfirmPriceUnit = computed(() => { + const snapshot = feeConfirmReference.value + return snapshot ? `${snapshot.currency} / ${snapshot.chargeUnit}` : '—' +}) +const feeConfirmEstimatedTotal = computed(() => { + const snapshot = feeConfirmReference.value + const unitPrice = Number(feeConfirmUnitPrice.value) + if (!snapshot || !(unitPrice >= 0)) return null + return Number(snapshot.chargeableQuantity || 0) * unitPrice + Number(snapshot.additionalAmount || 0) +}) const feeConfirmRows = computed(() => { const summary = detail.value?.summary const measuredGrossWeight = Number(measurementDetail.value?.actualGrossWeightKg) @@ -1631,7 +1659,40 @@ async function openFeeConfirmDialog() { ? Number(actualTotals.gross) : measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null feeConfirmVolume.value = Number(actualTotals.volume) > 0 ? Number(actualTotals.volume) : null + feeConfirmReference.value = null + feeConfirmUnitPrice.value = null + feeConfirmPriceReason.value = '' feeConfirmOpen.value = true + feeConfirmReferenceLoading.value = true + try { + const tenantId = String(summary.tenantId) + const inboundId = String(summary.inboundId) + let snapshot: InboundFeeSnapshot + try { + snapshot = await getLatestInboundFee(tenantId, inboundId) + } catch (error) { + const details = getApiErrorDetails(error, '价格表参考单价加载失败') + if (details.code !== 'A0404') throw error + snapshot = await calculateInboundFee(tenantId, inboundId, { + selectedWeightKg: Number(feeConfirmWeight.value), + selectedVolumeCbm: Number(feeConfirmVolume.value), + weightSource: 'ACTUAL', + volumeSource: 'ACTUAL', + }, createFeeActionIdempotencyKey('calculate')) + } + if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) { + ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送') + feeConfirmOpen.value = false + return + } + feeConfirmReference.value = snapshot + feeConfirmUnitPrice.value = Number(snapshot.unitPrice) + } catch (error) { + ElMessage.error(getApiErrorDetails(error, '价格表参考单价加载失败,暂不能推送费用').message) + feeConfirmOpen.value = false + } finally { + feeConfirmReferenceLoading.value = false + } } async function executeConfirmAndSendFee() { @@ -1639,10 +1700,19 @@ async function executeConfirmAndSendFee() { if (!summary?.inboundId) return const selectedWeightKg = Number(feeConfirmWeight.value) const selectedVolumeCbm = Number(feeConfirmVolume.value) + const selectedUnitPrice = Number(feeConfirmUnitPrice.value) if (!(selectedWeightKg > 0) || !(selectedVolumeCbm > 0)) { ElMessage.warning('计费毛重和计费体积必须大于 0') return } + if (!(selectedUnitPrice > 0)) { + ElMessage.warning('对客单价必须大于 0') + return + } + if (feeConfirmPriceChanged.value && feeConfirmPriceReason.value.trim().length < 2) { + ElMessage.warning('人工修改参考单价后,请填写至少 2 个字符的调整原因') + return + } confirmFeeSending.value = true try { const tenantId = String(summary.tenantId) @@ -1667,12 +1737,23 @@ async function executeConfirmAndSendFee() { weightSource: 'ACTUAL', volumeSource: 'ACTUAL', }, createFeeActionIdempotencyKey('calculate')) + const calculatedUnitPrice = Number(snapshot.unitPrice) + if (Math.abs(selectedUnitPrice - calculatedUnitPrice) > 0.000001) { + snapshot = await adjustPublishedInboundFee(tenantId, inboundId, { + expectedFeeVersion: snapshot.feeVersion, + chargeableQuantity: Number(snapshot.chargeableQuantity), + unitPrice: selectedUnitPrice, + additionalAmount: Number(snapshot.additionalAmount || 0), + reason: feeConfirmPriceReason.value.trim() || '推送前按已展示参考单价人工确认', + toleranceConfirmed: true, + }, createFeeAdjustmentIdempotencyKey()) + } snapshot = await confirmInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('confirm')) if (snapshot.feeStatus === 'CONFIRMED') { const sent = await sendInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('send')) automaticFee.value = { feeStatus: sent.feeStatus, totalAmount: Number(sent.totalAmount || 0) } feePanelRevision.value += 1 - ElMessage.success('计费重已确认并发送给客户') + ElMessage.success('计费量和单价已确认并发送给客户') feeConfirmOpen.value = false await loadDetail(true) return @@ -2127,13 +2208,22 @@ onBeforeUnmount(() => {