front/src/utils/feeTotals.ts
zzh b921badfe6
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m13s
feat: 对账件重尺导入与应收生效阶段合计
运单管理支持导入下游对账重量并锁定渠道重算应付;费用明细按最高计费阶段合计,避免预报与入库叠算。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 16:57:28 +08:00

102 lines
3.2 KiB
TypeScript

export type FeeRowLike = {
feeType?: string
type?: string
currency?: string
amount?: number
costItem?: string
chargingStage?: string
}
export function normalizeFeeType(row: FeeRowLike): 'receivable' | 'payable' | '' {
const raw = String(row?.feeType ?? row?.type ?? '').toLowerCase()
if (raw === 'receivable' || raw === 'payable') return raw
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[]) {
const totals = new Map<string, number>()
for (const row of list) {
const currency = String(row?.currency ?? 'CNY').trim() || 'CNY'
const amount = Number(row?.amount)
if (!Number.isFinite(amount)) continue
totals.set(currency, (totals.get(currency) ?? 0) + amount)
}
return totals
}
export function calcGrossProfitTotals(
receivable: Map<string, number>,
payable: Map<string, number>,
) {
const currencies = new Set([...receivable.keys(), ...payable.keys()])
const result = new Map<string, number>()
for (const currency of currencies) {
result.set(currency, (receivable.get(currency) ?? 0) - (payable.get(currency) ?? 0))
}
return result
}
export function buildFeeTotalsFromSummaryItems(
items: Array<{ feeType?: string; currency?: string; totalAmount?: number }>,
feeType: 'receivable' | 'payable',
) {
const totals = new Map<string, number>()
for (const item of items) {
if (normalizeFeeType({ feeType: item.feeType }) !== feeType) continue
const currency = String(item.currency ?? 'CNY').trim() || 'CNY'
const amount = Number(item.totalAmount)
if (!Number.isFinite(amount)) continue
totals.set(currency, (totals.get(currency) ?? 0) + amount)
}
return totals
}
export function formatFeeTotals(totals: Map<string, number>) {
if (!totals.size) return '--'
return [...totals.entries()]
.map(([currency, amount]) => `${currency} ${amount.toFixed(2)}`)
.join(' + ')
}
export function formatGrossProfitTotals(totals: Map<string, number>) {
if (!totals.size) return '--'
return [...totals.entries()]
.map(([currency, amount]) => {
const sign = amount > 0 ? '+' : ''
return `${sign}${currency} ${amount.toFixed(2)}`
})
.join(' + ')
}