All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m13s
运单管理支持导入下游对账重量并锁定渠道重算应付;费用明细按最高计费阶段合计,避免预报与入库叠算。 Co-authored-by: Cursor <cursoragent@cursor.com>
102 lines
3.2 KiB
TypeScript
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(' + ')
|
|
}
|