feat: 对账件重尺导入与应收生效阶段合计
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m13s

运单管理支持导入下游对账重量并锁定渠道重算应付;费用明细按最高计费阶段合计,避免预报与入库叠算。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-09-12 16:57:28 +08:00
parent ffe8679f82
commit b921badfe6
10 changed files with 407 additions and 16 deletions

View File

@ -43,7 +43,9 @@ flowchart TD
wipeP --> refreshM[刷新营销金额] wipeP --> refreshM[刷新营销金额]
paid[客户支付] --> freeze[应收硬冻结] paid[客户支付] --> freeze[应收硬冻结]
outbound[出库] --> shipOnly[仅更新 ship 件重尺/状态] outbound[出库] --> shipOnly[仅更新 ship 件重尺/状态]
shipOnly --> noRecv[不再写应收/应付] shipOnly --> noRecv[不再写应收]
reconcile[导入对账件重尺] --> downW[写 downstream 件重尺]
downW --> payFinal[锁定当前渠道重算应付 outbound]
``` ```
--- ---
@ -56,8 +58,9 @@ flowchart TD
|------|------|------|------| |------|------|------|------|
| **下单** | 预报件重尺 → 计费重 → 写 `forecast` | 同计费重尝试排货,成功则写 `forecast` 应付 | 排货失败不阻断下单,记 `AllocationPendingReason` | | **下单** | 预报件重尺 → 计费重 → 写 `forecast` | 同计费重尝试排货,成功则写 `forecast` 应付 | 排货失败不阻断下单,记 `AllocationPendingReason` |
| **入库** | 同步只写 receive 重尺/收货时间,状态先 `pendingAllocation`;应收在异步任务 `inbound-billing` 中清空并写 `inbound` | 异步任务内 `TryConfirm` 重排货写应付;失败仍待排货 | **不校验服务渠道、不阻断扫码**;有效账单仍只保留一套 | | **入库** | 同步只写 receive 重尺/收货时间,状态先 `pendingAllocation`;应收在异步任务 `inbound-billing` 中清空并写 `inbound` | 异步任务内 `TryConfirm` 重排货写应付;失败仍待排货 | **不校验服务渠道、不阻断扫码**;有效账单仍只保留一套 |
| **出库** | **不写应收** | 不重算 | 只更新 ship 件重尺与状态 | | **出库** | **不写应收** | 不自动重算 | 只更新 ship 件重尺与状态 |
| **已支付** | 禁止重算/加应收/改附加服务触发重算 | 出库不改 | `EnsureReceivableMutable` | | **已支付** | 禁止重算/加应收/改附加服务触发重算;禁止重跑入库计费 | 仍可重算应付 / 导入对账重 | `EnsureReceivableMutable` |
| **导入对账件重尺** | **不改** | 写 `downstream` 重尺 → 锁定当前渠道重算应付(阶段 `outbound`) | 运单管理:`GET/POST /order/weight/downstream-*`;尺寸可留空回落收货/发货 |
### 3.2 重量口径(硬约束) ### 3.2 重量口径(硬约束)
@ -65,6 +68,7 @@ flowchart TD
2. 进位规则 → `ChargeableWeight` 2. 进位规则 → `ChargeableWeight`
3. 应收、应付、排货预览**同一** `ChargeableWeight` 3. 应收、应付、排货预览**同一** `ChargeableWeight`
4. **禁止**用 `ActualWeight` 直接乘单价 4. **禁止**用 `ActualWeight` 直接乘单价
5. **有效应收/应付只保留一套**:入库或重算后不得叠算预报+入库;合计按最高计费阶段(出库 > 入库 > 预报)
### 3.3 预报快照(二期) ### 3.3 预报快照(二期)
@ -223,8 +227,11 @@ flowchart TD
| `POST /order/price-check` | 查价(可带 additionalServices / DeclaredSum) | | `POST /order/price-check` | 查价(可带 additionalServices / DeclaredSum) |
| `POST /operation/inbound/create` | 入库:清空后重算应收/应付 inbound | | `POST /operation/inbound/create` | 入库:清空后重算应收/应付 inbound |
| `POST /operation/outbound/create` | 出库:不写费用 | | `POST /operation/outbound/create` | 出库:不写费用 |
| `GET /order/weight/downstream-import-template` | 对账件重尺导入模板 |
| `POST /order/weight/downstream-import` | 导入 downstream 重尺并锁定渠道重算应付 |
| `POST /order/fee/recalculate-receivable` | 重算应收(已支付拒绝) | | `POST /order/fee/recalculate-receivable` | 重算应收(已支付拒绝) |
| `POST /order/fee/recalculate-payable` | 重算应付 | | `POST /order/fee/recalculate-payable` | 重算应付(已有渠道时锁定渠道,不换渠道) |
| `POST /order/fee/rerun-inbound-billing` | 重跑入库计费(已支付拒绝,应收已锁) |
| `POST /order/fee/add` | 手工加费(应收已支付拒绝) | | `POST /order/fee/add` | 手工加费(应收已支付拒绝) |
| `POST /order/additional-services/update` | 未支付改附加服务并重算 | | `POST /order/additional-services/update` | 未支付改附加服务并重算 |
| `POST /channel/sales-product/billing/simulate` | 用当前库内卖价做计费模拟 | | `POST /channel/sales-product/billing/simulate` | 用当前库内卖价做计费模拟 |
@ -243,6 +250,7 @@ flowchart TD
| 渠道偏远分区 | `/channel/serviceChannels` | | 渠道偏远分区 | `/channel/serviceChannels` |
| 下单勾选附加服务 | PC `placeOrder`、H5 `H5PlaceOrderSheet` | | 下单勾选附加服务 | PC `placeOrder`、H5 `H5PlaceOrderSheet` |
| 毛利 / 预报差额 / 改附加服务 | 运单管理 `OrderDetailDrawer` 费用 Tab | | 毛利 / 预报差额 / 改附加服务 | 运单管理 `OrderDetailDrawer` 费用 Tab |
| 导入对账件重尺 | 运单管理「更多」→「导入对账件重尺」 |
| 预置字典种子 | `system/dict/dict-batch-import.ts`(下载 OMS 预置字典) | | 预置字典种子 | `system/dict/dict-batch-import.ts`(下载 OMS 预置字典) |
--- ---
@ -253,7 +261,8 @@ flowchart TD
|------|------| |------|------|
| 金额引擎 | `Oms.Application/.../ParcelBillingCalculator.cs` | | 金额引擎 | `Oms.Application/.../ParcelBillingCalculator.cs` |
| 下单/入库/出库编排 | `OrderService.ParcelBilling.cs` | | 下单/入库/出库编排 | `OrderService.ParcelBilling.cs` |
| 改服务 / 冻结 | `OrderService.OrderEdit.cs` | | 改服务 / 冻结 / 对账导入 | `OrderService.OrderEdit.cs` |
| 对账件重尺 Excel | `OrderDownstreamWeightImportHelper.cs` |
| 查价 | `OrderService.ParcelQuote.cs` | | 查价 | `OrderService.ParcelQuote.cs` |
| 计费模拟 | `SalesProductService.BillingSimulate.cs` | | 计费模拟 | `SalesProductService.BillingSimulate.cs` |
| 分区快照/混表校验 | `PriceAreaSnapshotHelper.cs` | | 分区快照/混表校验 | `PriceAreaSnapshotHelper.cs` |
@ -295,6 +304,7 @@ flowchart TD
- 卖价项级「计费时机」字段 - 卖价项级「计费时机」字段
- 出库改已付应收 - 出库改已付应收
- 物理 drop `t_sales_product.PriceAreaId` - 物理 drop `t_sales_product.PriceAreaId`
- 导入任意重量类型自动改应收(对账导入只动应付)
**可后续:** **可后续:**
@ -304,6 +314,7 @@ flowchart TD
- 一票多件计费维度 - 一票多件计费维度
- 发布前更完整的毛利模拟(含多候选渠道对比) - 发布前更完整的毛利模拟(含多候选渠道对比)
- 费用争议走调整单而非改原费用行 - 费用争议走调整单而非改原费用行
- 对账导入异步任务(超 2000 行)与应付版本审计
--- ---

View File

@ -1981,6 +1981,57 @@ export async function postOrderWeightUpdate(body: {
return res.data?.data ?? res.data?.Data ?? res.data return res.data?.data ?? res.data?.Data ?? res.data
} }
export type OrderDownstreamWeightImportResult = {
successCount: number
failCount: number
failures: Array<{ rowNumber: number; waybillNo?: string; reason: string }>
}
/** GET /order/weight/downstream-import-template */
export async function downloadOrderDownstreamWeightImportTemplate(fileName?: string) {
const res = await axiosInstance.get(orderUrl('/order/weight/downstream-import-template'), {
responseType: 'blob',
})
const blob = res.data as Blob
const ct = (res.headers['content-type'] || '').toString()
if (ct.includes('application/json')) {
const text = await blob.text()
const json = JSON.parse(text)
throw new Error(json.msg ?? json.message ?? json.Message ?? '下载失败')
}
const name = fileName || '对账件重尺导入模板.xlsx'
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = name.endsWith('.xlsx') ? name : `${name}.xlsx`
a.style.display = 'none'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
/** POST /order/weight/downstream-import multipart */
export async function importOrderDownstreamWeights(
file: File
): Promise<OrderDownstreamWeightImportResult> {
const form = new FormData()
form.append('file', file)
const res = await axiosInstance.post(orderUrl('/order/weight/downstream-import'), form)
assertBusinessOk(res.data)
const d = res.data?.data ?? res.data?.Data ?? res.data ?? {}
const failures = (d.failures ?? d.Failures ?? []).map((item: any) => ({
rowNumber: Number(item.rowNumber ?? item.RowNumber ?? 0),
waybillNo: String(item.waybillNo ?? item.WaybillNo ?? '') || undefined,
reason: String(item.reason ?? item.Reason ?? ''),
}))
return {
successCount: Number(d.successCount ?? d.SuccessCount ?? 0),
failCount: Number(d.failCount ?? d.FailCount ?? 0),
failures,
}
}
/** POST /order/additional-services/update — 未付款订单更新附加服务 */ /** POST /order/additional-services/update — 未付款订单更新附加服务 */
export async function postOrderAdditionalServicesUpdate(body: { export async function postOrderAdditionalServicesUpdate(body: {
id: number | string id: number | string

View File

@ -175,6 +175,7 @@ export const PERMISSION_ACTION_LABELS: Record<string, string> = {
AddFee: '添加费用', AddFee: '添加费用',
RecalculateReceivable: '重算应收', RecalculateReceivable: '重算应收',
RecalculatePayable: '重算应付', RecalculatePayable: '重算应付',
ImportDownstreamWeight: '导入对账件重尺',
AddDeclarationRow: '添加申报行', AddDeclarationRow: '添加申报行',
DownstreamForecastResult: '下游预报结果', DownstreamForecastResult: '下游预报结果',
More: '更多', More: '更多',
@ -564,6 +565,7 @@ const CORE_GROUPS: PageButtonGroup[] = [
{ code: 'AddFee', name: '添加费用' }, { code: 'AddFee', name: '添加费用' },
{ code: 'RecalculateReceivable', name: '重算应收' }, { code: 'RecalculateReceivable', name: '重算应收' },
{ code: 'RecalculatePayable', name: '重算应付' }, { code: 'RecalculatePayable', name: '重算应付' },
{ code: 'ImportDownstreamWeight', name: '导入对账件重尺' },
{ code: 'AddDeclarationRow', name: '添加申报行' }, { code: 'AddDeclarationRow', name: '添加申报行' },
]), ]),
B('Order.BatchJobs', [ B('Order.BatchJobs', [

View File

@ -1422,6 +1422,22 @@ export default {
'order.myOrders.actions.createProblematic': 'Create problematic', 'order.myOrders.actions.createProblematic': 'Create problematic',
'order.myOrders.actions.createClaimed': 'Create claim', 'order.myOrders.actions.createClaimed': 'Create claim',
'order.myOrders.actions.reallocate': 'Reallocate', 'order.myOrders.actions.reallocate': 'Reallocate',
'order.waybillManage.importDownstream.action': 'Import reconcile weight',
'order.waybillManage.importDownstream.modalTitle': 'Import reconcile weight',
'order.waybillManage.importDownstream.tip':
'Writes agent-reconciled weight to downstream dimensions and recalculates payable on the current channel only. Receivable is never changed. Length/width/height are optional (falls back to receive/ship). Actual weight is required. Max 2000 rows per file.',
'order.waybillManage.importDownstream.downloadTemplate': 'Download template',
'order.waybillManage.importDownstream.submit': 'Import',
'order.waybillManage.importDownstream.cancel': 'Cancel',
'order.waybillManage.importDownstream.downloadSuccess': 'Template downloaded',
'order.waybillManage.importDownstream.downloadFailed': 'Template download failed',
'order.waybillManage.importDownstream.needFile': 'Please select an Excel file',
'order.waybillManage.importDownstream.success': 'Import succeeded',
'order.waybillManage.importDownstream.failed': 'Import failed',
'order.waybillManage.importDownstream.partialSuccess': 'Some rows failed; see details below',
'order.waybillManage.importDownstream.allFailed': 'Import failed; check the file',
'order.waybillManage.importDownstream.resultSummary': '{success} succeeded, {fail} failed',
'order.waybillManage.importDownstream.failRow': 'Row {row} ({waybill}): {reason}',
'order.myOrders.actions.markProcessed': 'Mark processed', 'order.myOrders.actions.markProcessed': 'Mark processed',
'order.myOrders.reallocate.modalTitle': 'Reallocate ({count} selected)', 'order.myOrders.reallocate.modalTitle': 'Reallocate ({count} selected)',
'order.myOrders.reallocate.hint': 'Only recalculates payable and updates the service channel. Receivable and dimensions are unchanged. Paid orders are skipped. Changing channel clears provider service no. / tracking no.; re-forward downstream if needed.', 'order.myOrders.reallocate.hint': 'Only recalculates payable and updates the service channel. Receivable and dimensions are unchanged. Paid orders are skipped. Changing channel clears provider service no. / tracking no.; re-forward downstream if needed.',

View File

@ -1308,6 +1308,22 @@ export default {
'order.myOrders.actions.reallocate': '重新排货', 'order.myOrders.actions.reallocate': '重新排货',
'order.myOrders.actions.markProcessed': '标记已处理', 'order.myOrders.actions.markProcessed': '标记已处理',
'order.myOrders.actions.more': '更多', 'order.myOrders.actions.more': '更多',
'order.waybillManage.importDownstream.action': '导入对账件重尺',
'order.waybillManage.importDownstream.modalTitle': '导入对账件重尺',
'order.waybillManage.importDownstream.tip':
'按代理对账重量写入下游件重尺(downstream),锁定当前服务渠道重算应付;不改应收。长/宽/高可留空(沿用收货/发货尺寸);实重必填。单次最多 2000 行。',
'order.waybillManage.importDownstream.downloadTemplate': '下载模板',
'order.waybillManage.importDownstream.submit': '开始导入',
'order.waybillManage.importDownstream.cancel': '取消',
'order.waybillManage.importDownstream.downloadSuccess': '模板下载成功',
'order.waybillManage.importDownstream.downloadFailed': '模板下载失败',
'order.waybillManage.importDownstream.needFile': '请选择要上传的 Excel 文件',
'order.waybillManage.importDownstream.success': '导入成功',
'order.waybillManage.importDownstream.failed': '导入失败',
'order.waybillManage.importDownstream.partialSuccess': '部分行导入失败,请查看失败明细',
'order.waybillManage.importDownstream.allFailed': '导入失败,请检查文件内容',
'order.waybillManage.importDownstream.resultSummary': '成功 {success} 条,失败 {fail} 条',
'order.waybillManage.importDownstream.failRow': '第 {row} 行({waybill}):{reason}',
'order.myOrders.reallocate.modalTitle': '重新排货(已选 {count} 单)', 'order.myOrders.reallocate.modalTitle': '重新排货(已选 {count} 单)',
'order.myOrders.reallocate.hint': '仅重算应付并更新服务渠道,不改应收与件重尺;已付款订单会自动跳过。换渠道会清空服务商单号/跟踪号,需自行重新预报下游。', 'order.myOrders.reallocate.hint': '仅重算应付并更新服务渠道,不改应收与件重尺;已付款订单会自动跳过。换渠道会清空服务商单号/跟踪号,需自行重新预报下游。',
'order.myOrders.reallocate.schemeLabel': '排货方案', 'order.myOrders.reallocate.schemeLabel': '排货方案',

View File

@ -3,6 +3,8 @@ export type FeeRowLike = {
type?: string type?: string
currency?: string currency?: string
amount?: number amount?: number
costItem?: string
chargingStage?: string
} }
export function normalizeFeeType(row: FeeRowLike): 'receivable' | 'payable' | '' { export function normalizeFeeType(row: FeeRowLike): 'receivable' | 'payable' | '' {
@ -11,6 +13,38 @@ export function normalizeFeeType(row: FeeRowLike): 'receivable' | 'payable' | ''
return '' return ''
} }
function getChargingStageRank(stage?: string) {
const s = String(stage ?? '').toLowerCase()
if (s === 'outbound') return 3
if (s === 'inbound') return 2
if (s === 'forecast') return 1
return 0
}
/**
* 每个费用类型 + 费用项只保留最高计费阶段(出库 > 入库 > 预报),
* 避免预报与入库明细叠算进合计。
*/
export function filterEffectiveStageFees<T extends FeeRowLike>(rows: T[]): T[] {
const winners = new Map<string, T>()
for (const row of rows) {
const feeType = normalizeFeeType(row) || 'unknown'
const costItem = String(row?.costItem ?? '').trim() || '_'
const key = `${feeType}|${costItem}`
const current = winners.get(key)
if (!current) {
winners.set(key, row)
continue
}
const rowStage = String((row as any)?.chargingStage ?? (row as any)?.ChargingStage ?? '')
const curStage = String((current as any)?.chargingStage ?? (current as any)?.ChargingStage ?? '')
if (getChargingStageRank(rowStage) > getChargingStageRank(curStage)) {
winners.set(key, row)
}
}
return [...winners.values()]
}
export function sumFeesByCurrency(list: FeeRowLike[]) { export function sumFeesByCurrency(list: FeeRowLike[]) {
const totals = new Map<string, number>() const totals = new Map<string, number>()
for (const row of list) { for (const row of list) {

View File

@ -1476,6 +1476,35 @@ function normalizeFeeType(row: any): 'receivable' | 'payable' | '' {
return ''; return '';
} }
function getChargingStageRank(stage?: string) {
const s = String(stage ?? '').toLowerCase();
if (s === 'outbound') return 3;
if (s === 'inbound') return 2;
if (s === 'forecast') return 1;
return 0;
}
/** 同费用项只保留最高计费阶段,避免预报+入库叠算 */
function filterEffectiveStageFees(list: any[]) {
const winners = new Map<string, any>();
for (const row of list) {
const feeType = normalizeFeeType(row) || 'unknown';
const costItem = String(row?.costItem ?? row?.CostItem ?? '').trim() || '_';
const key = `${feeType}|${costItem}`;
const current = winners.get(key);
if (!current) {
winners.set(key, row);
continue;
}
const rowStage = String(row?.chargingStage ?? row?.ChargingStage ?? '');
const curStage = String(current?.chargingStage ?? current?.ChargingStage ?? '');
if (getChargingStageRank(rowStage) > getChargingStageRank(curStage)) {
winners.set(key, row);
}
}
return [...winners.values()];
}
function sumFeesByCurrency(list: any[]) { function sumFeesByCurrency(list: any[]) {
const totals = new Map<string, number>(); const totals = new Map<string, number>();
for (const row of list) { for (const row of list) {
@ -1488,11 +1517,11 @@ function sumFeesByCurrency(list: any[]) {
} }
const receivableFeeList = computed(() => const receivableFeeList = computed(() =>
feeList.value.filter((row) => normalizeFeeType(row) === 'receivable') filterEffectiveStageFees(feeList.value.filter((row) => normalizeFeeType(row) === 'receivable'))
); );
const payableFeeList = computed(() => const payableFeeList = computed(() =>
feeList.value.filter((row) => normalizeFeeType(row) === 'payable') filterEffectiveStageFees(feeList.value.filter((row) => normalizeFeeType(row) === 'payable'))
); );
const receivableTotals = computed(() => sumFeesByCurrency(receivableFeeList.value)); const receivableTotals = computed(() => sumFeesByCurrency(receivableFeeList.value));

View File

@ -0,0 +1,182 @@
<template>
<a-modal
v-model:visible="visible"
:title="t('order.waybillManage.importDownstream.modalTitle')"
:width="560"
:ok-loading="importing"
:ok-text="t('order.waybillManage.importDownstream.submit')"
:cancel-text="t('order.waybillManage.importDownstream.cancel')"
unmount-on-close
@before-ok="handleBeforeOk"
@cancel="handleCancel"
>
<div class="downstream-weight-import-modal">
<div class="import-tip">{{ t('order.waybillManage.importDownstream.tip') }}</div>
<div class="import-actions">
<a-button type="outline" :loading="downloading" @click="handleDownloadTemplate">
{{ t('order.waybillManage.importDownstream.downloadTemplate') }}
</a-button>
</div>
<a-upload
draggable
:auto-upload="false"
:show-file-list="true"
:limit="1"
accept=".xlsx,.xls"
:file-list="fileList"
@change="handleFileChange"
/>
<div v-if="importResult" class="import-result">
<a-alert type="info" :show-icon="false">
{{
t('order.waybillManage.importDownstream.resultSummary', {
success: importResult.successCount,
fail: importResult.failCount,
})
}}
</a-alert>
<ul v-if="importResult.failures.length" class="import-fail-list">
<li v-for="item in importResult.failures" :key="`${item.rowNumber}-${item.waybillNo || ''}`">
{{
t('order.waybillManage.importDownstream.failRow', {
row: item.rowNumber,
waybill: item.waybillNo || '--',
reason: item.reason,
})
}}
</li>
</ul>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import type { FileItem } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue';
import {
downloadOrderDownstreamWeightImportTemplate,
importOrderDownstreamWeights,
type OrderDownstreamWeightImportResult,
} from '@/api/order';
const emit = defineEmits<{
success: [];
}>();
const { t } = useI18n();
const visible = ref(false);
const fileList = ref<FileItem[]>([]);
const selectedFile = ref<File | null>(null);
const downloading = ref(false);
const importing = ref(false);
const importResult = ref<OrderDownstreamWeightImportResult | null>(null);
function open() {
fileList.value = [];
selectedFile.value = null;
importResult.value = null;
visible.value = true;
}
function handleFileChange(_: FileItem[], fileItem: FileItem) {
fileList.value = fileItem ? [fileItem] : [];
selectedFile.value = (fileItem?.file as File) ?? null;
importResult.value = null;
}
async function handleDownloadTemplate() {
downloading.value = true;
try {
await downloadOrderDownstreamWeightImportTemplate();
Message.success(t('order.waybillManage.importDownstream.downloadSuccess'));
} catch (e: any) {
Message.error(e?.message || t('order.waybillManage.importDownstream.downloadFailed'));
} finally {
downloading.value = false;
}
}
async function handleBeforeOk() {
if (!selectedFile.value) {
Message.warning(t('order.waybillManage.importDownstream.needFile'));
return false;
}
importing.value = true;
try {
const result = await importOrderDownstreamWeights(selectedFile.value);
importResult.value = result;
if (result.successCount > 0) {
emit('success');
}
if (result.failCount === 0) {
Message.success(t('order.waybillManage.importDownstream.success'));
visible.value = false;
return true;
}
if (result.successCount > 0) {
Message.warning(t('order.waybillManage.importDownstream.partialSuccess'));
} else {
Message.error(t('order.waybillManage.importDownstream.allFailed'));
}
return false;
} catch (e: any) {
Message.error(e?.message || t('order.waybillManage.importDownstream.failed'));
return false;
} finally {
importing.value = false;
}
}
function handleCancel() {
visible.value = false;
}
defineExpose({ open });
</script>
<style scoped lang="less">
.downstream-weight-import-modal {
display: flex;
flex-direction: column;
gap: 16px;
}
.import-tip {
color: var(--color-text-3);
font-size: 13px;
line-height: 1.6;
}
.import-actions {
display: flex;
gap: 8px;
}
.import-result {
display: flex;
flex-direction: column;
gap: 8px;
}
.import-fail-list {
margin: 0;
padding: 8px 12px;
max-height: 220px;
overflow: auto;
background: var(--color-fill-2);
border-radius: 4px;
font-size: 12px;
line-height: 1.6;
color: var(--color-text-2);
list-style: none;
li + li {
margin-top: 4px;
}
}
</style>

View File

@ -1618,6 +1618,35 @@ function normalizeFeeType(row: any): 'receivable' | 'payable' | '' {
return ''; return '';
} }
function getChargingStageRank(stage?: string) {
const s = String(stage ?? '').toLowerCase();
if (s === 'outbound') return 3;
if (s === 'inbound') return 2;
if (s === 'forecast') return 1;
return 0;
}
/** 同费用项只保留最高计费阶段,避免预报+入库叠算 */
function filterEffectiveStageFees(list: any[]) {
const winners = new Map<string, any>();
for (const row of list) {
const feeType = normalizeFeeType(row) || 'unknown';
const costItem = String(row?.costItem ?? row?.CostItem ?? '').trim() || '_';
const key = `${feeType}|${costItem}`;
const current = winners.get(key);
if (!current) {
winners.set(key, row);
continue;
}
const rowStage = String(row?.chargingStage ?? row?.ChargingStage ?? '');
const curStage = String(current?.chargingStage ?? current?.ChargingStage ?? '');
if (getChargingStageRank(rowStage) > getChargingStageRank(curStage)) {
winners.set(key, row);
}
}
return [...winners.values()];
}
function sumFeesByCurrency(list: any[]) { function sumFeesByCurrency(list: any[]) {
const totals = new Map<string, number>(); const totals = new Map<string, number>();
for (const row of list) { for (const row of list) {
@ -1630,11 +1659,11 @@ function sumFeesByCurrency(list: any[]) {
} }
const receivableFeeList = computed(() => const receivableFeeList = computed(() =>
feeList.value.filter((row) => normalizeFeeType(row) === 'receivable') filterEffectiveStageFees(feeList.value.filter((row) => normalizeFeeType(row) === 'receivable'))
); );
const payableFeeList = computed(() => const payableFeeList = computed(() =>
feeList.value.filter((row) => normalizeFeeType(row) === 'payable') filterEffectiveStageFees(feeList.value.filter((row) => normalizeFeeType(row) === 'payable'))
); );
const receivableTotals = computed(() => sumFeesByCurrency(receivableFeeList.value)); const receivableTotals = computed(() => sumFeesByCurrency(receivableFeeList.value));

View File

@ -54,6 +54,7 @@
import MarkProblematicModal from './components/MarkProblematicModal.vue'; import MarkProblematicModal from './components/MarkProblematicModal.vue';
import MarkClaimedModal from './components/MarkClaimedModal.vue'; import MarkClaimedModal from './components/MarkClaimedModal.vue';
import ReallocateModal from './components/ReallocateModal.vue'; import ReallocateModal from './components/ReallocateModal.vue';
import DownstreamWeightImportModal from './components/DownstreamWeightImportModal.vue';
import ColumnConfigModal from '../components/ColumnConfigModal.vue'; import ColumnConfigModal from '../components/ColumnConfigModal.vue';
import { import {
buildHotColumns, buildHotColumns,
@ -1372,6 +1373,11 @@
reallocateModalVisible.value = true; reallocateModalVisible.value = true;
}; };
const downstreamWeightImportModalRef = ref<InstanceType<typeof DownstreamWeightImportModal> | null>(null);
const openDownstreamWeightImport = () => {
downstreamWeightImportModalRef.value?.open();
};
function resolveClaimContextFromRow(row: any) { function resolveClaimContextFromRow(row: any) {
let hasInsurance: boolean | null = null; let hasInsurance: boolean | null = null;
const hi = row?.hasInsurance ?? row?.HasInsurance; const hi = row?.hasInsurance ?? row?.HasInsurance;
@ -1472,6 +1478,7 @@
if (key === 'cancelIntercept') return handleCancelIntercept(); if (key === 'cancelIntercept') return handleCancelIntercept();
if (key === 'delete') return handleDelete(); if (key === 'delete') return handleDelete();
if (key === 'export') return handleExport(); if (key === 'export') return handleExport();
if (key === 'importDownstreamWeight') return openDownstreamWeightImport();
if (key === 'modifyOrder') return handleModifyOrder(); if (key === 'modifyOrder') return handleModifyOrder();
}; };
@ -1927,10 +1934,10 @@
:disabled="selectedRowCount === 0 || isActionBusy('batchPrint')" :disabled="selectedRowCount === 0 || isActionBusy('batchPrint')"
@click="handleBatchPrint" @click="handleBatchPrint"
>{{ t('order.myOrders.actions.batchPrint') }}</a-button> >{{ t('order.myOrders.actions.batchPrint') }}</a-button>
<a-dropdown trigger="click" position="bl" :disabled="selectedRowCount === 0 || !!actionLoadingKey"> <a-dropdown trigger="click" position="bl" :disabled="!!actionLoadingKey">
<a-button <a-button
class="app-btn-toolbar btn-more" class="app-btn-toolbar btn-more"
:disabled="selectedRowCount === 0 || !!actionLoadingKey" :disabled="!!actionLoadingKey"
> >
{{ t('order.myOrders.actions.more') }} {{ t('order.myOrders.actions.more') }}
<icon-down class="btn-more-arrow" /> <icon-down class="btn-more-arrow" />
@ -1938,39 +1945,49 @@
<template #content> <template #content>
<a-doption <a-doption
v-if="showExceptionCreateActions" v-if="showExceptionCreateActions"
:disabled="isActionBusy('markProblematic')" :disabled="selectedRowCount === 0 || isActionBusy('markProblematic')"
@click="onMoreActionClick('createProblematic')" @click="onMoreActionClick('createProblematic')"
>{{ t('order.myOrders.actions.createProblematic') }}</a-doption> >{{ t('order.myOrders.actions.createProblematic') }}</a-doption>
<a-doption <a-doption
v-if="showExceptionCreateActions" v-if="showExceptionCreateActions"
:disabled="isActionBusy('markClaimed')" :disabled="selectedRowCount === 0 || isActionBusy('markClaimed')"
@click="onMoreActionClick('createClaimed')" @click="onMoreActionClick('createClaimed')"
>{{ t('order.myOrders.actions.createClaimed') }}</a-doption> >{{ t('order.myOrders.actions.createClaimed') }}</a-doption>
<a-doption <a-doption
v-if="showReallocateAction" v-if="showReallocateAction"
:disabled="isActionBusy('reallocate')" :disabled="selectedRowCount === 0 || isActionBusy('reallocate')"
@click="onMoreActionClick('reallocate')" @click="onMoreActionClick('reallocate')"
>{{ t('order.myOrders.actions.reallocate') }}</a-doption> >{{ t('order.myOrders.actions.reallocate') }}</a-doption>
<a-doption <a-doption
v-if="showCreateTrackAction" v-if="showCreateTrackAction"
:disabled="isActionBusy('track')" :disabled="selectedRowCount === 0 || isActionBusy('track')"
@click="onMoreActionClick('createTrackOrder')" @click="onMoreActionClick('createTrackOrder')"
>{{ t('order.myOrders.actions.createTrackOrder') }}</a-doption> >{{ t('order.myOrders.actions.createTrackOrder') }}</a-doption>
<a-doption <a-doption
v-if="showInterceptActions" v-if="showInterceptActions"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('intercept')" @click="onMoreActionClick('intercept')"
>{{ t('order.myOrders.actions.intercept') }}</a-doption> >{{ t('order.myOrders.actions.intercept') }}</a-doption>
<a-doption <a-doption
v-if="showInterceptActions" v-if="showInterceptActions"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('cancelIntercept')" @click="onMoreActionClick('cancelIntercept')"
>{{ t('order.myOrders.actions.cancelIntercept') }}</a-doption> >{{ t('order.myOrders.actions.cancelIntercept') }}</a-doption>
<a-doption <a-doption
v-if="showDeleteAction" v-if="showDeleteAction"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('delete')" @click="onMoreActionClick('delete')"
>{{ t('order.myOrders.actions.delete') }}</a-doption> >{{ t('order.myOrders.actions.delete') }}</a-doption>
<a-doption @click="onMoreActionClick('export')">{{ t('order.myOrders.actions.export') }}</a-doption> <a-doption
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('export')"
>{{ t('order.myOrders.actions.export') }}</a-doption>
<a-doption @click="onMoreActionClick('importDownstreamWeight')">
{{ t('order.waybillManage.importDownstream.action') }}
</a-doption>
<a-doption <a-doption
v-if="showModifyOrderAction" v-if="showModifyOrderAction"
:disabled="selectedRowCount === 0"
@click="onMoreActionClick('modifyOrder')" @click="onMoreActionClick('modifyOrder')"
>{{ t('order.myOrders.actions.modifyOrder') }}</a-doption> >{{ t('order.myOrders.actions.modifyOrder') }}</a-doption>
</template> </template>
@ -2126,6 +2143,10 @@
:order-ids="exceptionOrderIds" :order-ids="exceptionOrderIds"
@success="handleExceptionActionSuccess" @success="handleExceptionActionSuccess"
/> />
<DownstreamWeightImportModal
ref="downstreamWeightImportModalRef"
@success="handleExceptionActionSuccess"
/>
<OrderPayConfirmModal <OrderPayConfirmModal
v-model:visible="payConfirmVisible" v-model:visible="payConfirmVisible"
:orders="payConfirmOrders" :orders="payConfirmOrders"