front/docs/应收应付
zzh 50b3eac99c
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m40s
feat: 运单费用版本切换、服务商名称与列配置中文,我的运单隐藏操作日志
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-09 18:30:48 +08:00

327 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: OMS应收应付计费逻辑
overview: OMS 小包订单应收(卖价)与应付(渠道买价)的完整计费逻辑说明,含生命周期、计费重、附加服务、计费条件、公式、偏远分区、发布校验与二期能力;供后续开发/运营查阅。
isProject: false
---
# OMS 应收 / 应付计费逻辑(完整说明)
> 状态:已落地(2026-08)。仓库:前端 `E:\oms0414`,后端 `oms0414-back`。
> SQL:`sql/20260801_billing_additional_services.sql`、`sql/20260801_billing_phase2.sql`。
---
## 1. 概念与命名
| 术语 | 含义 |
|------|------|
| **应收** | 向客户收取的费用,`OrderFee.Type = receivable`,来自**销售产品卖价**发布快照 |
| **应付** | 付给下游渠道的成本,`OrderFee.Type = payable`,来自**服务渠道买价**发布快照 |
| **计费重** | `OrderWeight.ChargeableWeight`:计重规则(实重/体积重)+ 进位后的重量;**金额一律按计费重,不用实重** |
| **计费条件** | 卖价/买价项字段 `ChargingCondition`(中文公式);空或「无」= 恒真 |
| **附加服务** | 产品可选、客户下单勾选;字典 `additional_service`;写入订单 `AdditionalServices` JSON |
| **价格分区** | KPI `t_kpi_area`(price):地址 → 分区键;定价因素 `zone`/`价格分区`(旧名「分区」仍兼容)。产品表不再配置 `PriceAreaId`,由卖价分区反推进快照 |
| **偏远分区** | KPI remote:① 产品/渠道 `RemoteAreaId` → 布尔「是否偏远」;② 定价因素 `remoteZone`/`偏远分区` 可按 RemoteAreaId 配偏远费明细行 |
**命名陷阱**:`Order.PayableAmount` = 客户应付(营销后应收净额),**不是**渠道应付合计。
---
## 2. 总览流水线
```mermaid
flowchart TD
placeOrder[下单勾选附加服务] --> weightF[预报件重尺算计费重]
weightF --> remoteP[产品偏远分区写 IsRemote]
remoteP --> recvF[卖价:条件+匹配+公式 → 应收 forecast]
recvF --> payF[排货:渠道偏远上下文 → 应付 forecast]
payF --> snap[固化 ForecastReceivable/Payable 快照]
snap --> marketing[营销折算 PayableAmount]
inbound[入库] --> weightI[收货件重尺算计费重]
weightI --> wipeR[清空全部应收后写 inbound 应收]
wipeR --> wipeP[清空全部应付后重排货写 inbound 应付]
wipeP --> refreshM[刷新营销金额]
paid[客户支付] --> freeze[应收硬冻结]
outbound[出库] --> shipOnly[仅更新 ship 件重尺/状态]
shipOnly --> noRecv[不再写应收]
reconcile[导入对账件重尺] --> downW[写 downstream 件重尺]
downW --> payFinal[锁定当前渠道重算应付 outbound]
```
---
## 3. 生命周期与落库规则
### 3.1 各节点行为
| 节点 | 应收 | 应付 | 备注 |
|------|------|------|------|
| **下单** | 预报件重尺 → 计费重 → 写 `forecast` | 同计费重尝试排货,成功则写 `forecast` 应付 | 排货失败不阻断下单,记 `AllocationPendingReason` |
| **入库** | 同步只写 receive 重尺/收货时间,状态先 `pendingAllocation`;应收在异步任务 `inbound-billing` 中清空并写 `inbound` | 异步任务内 `TryConfirm` 重排货写应付;失败仍待排货 | **不校验服务渠道、不阻断扫码**;有效账单仍只保留一套 |
| **出库** | **不写应收** | 不自动重算 | 只更新 ship 件重尺与状态 |
| **已支付** | 禁止重算/加应收/改附加服务触发重算;禁止重跑入库计费 | 仍可重算应付 / 导入对账重 | `EnsureReceivableMutable` |
| **导入对账件重尺** | **不改** | 写 `downstream` 重尺 → 锁定当前渠道重算应付(阶段 `outbound`) | 运单管理:`GET/POST /order/weight/downstream-*`;尺寸可留空回落收货/发货 |
### 3.2 重量口径(硬约束)
1. 用件重尺 + 计重规则 → 体积重 / 原始计费重
2. 进位规则 → `ChargeableWeight`
3. 应收、应付、排货预览**同一** `ChargeableWeight`
4. **禁止**用 `ActualWeight` 直接乘单价
5. **有效应收/应付只保留一套**:入库或重算后不得叠算预报+入库;合计按最高计费阶段(出库 > 入库 > 预报)
### 3.3 预报快照(二期)
下单完成后固化,入库不覆盖:
- `Order.ForecastReceivableAmount`
- `Order.ForecastPayableAmount`
运单详情费用 Tab 展示「预报 → 当前」差额与毛利(应收 − 应付)。
---
## 4. 单条价格项怎么算出来
引擎:`ParcelBillingCalculator`。
对每个**启用**卖价/买价项:
```
1. 计费条件求值(ChineseFormulaEvaluator)→ 假则跳过
2. 分区 / 货物属性 / 仓库 / 重量区间匹配明细
3. 按 calculation_formula 算金额
4. 同阶段同 CostItem 多命中 → 保留 SerialNumber 最小一条
```
### 4.1 计费条件
- 字段:`ChargingCondition`(UI 文案「计费条件」)
- 空 / 「无」→ 恒真
- 语法:与自动触发相同(中文字段 + `包含` / `&&` `||` / 比较)
- AI Skill:`oms-billing-condition-formula`(scene `billingConditionFormula`)
- 常用示例:
- `附加服务包含('insurance')`
- `是否偏远()==1`
- `申报总金额()>1000`
**应收上下文**:订单字段 + 产品偏远命中后的 `IsRemote` + `AdditionalServices`。
**应付上下文**:同一引擎,但 **`是否偏远` 按渠道 `RemoteAreaId` 命中**注入;不用客户附加服务驱动应付。
### 4.2 价格分区 / 偏远分区匹配
字典 `pricing_factor`:
| code | 展示名 | 因素值来源 |
|------|--------|------------|
| `zone` | 价格分区 | KPI `areaType=price` |
| `remoteZone` | 偏远分区 | KPI `areaType=remote` |
1. 发布卖价时 `PriceAreaSnapshotHelper` 从卖价明细**价格分区**键反推 AreaId,固化 `PriceAreaInfos`
2. 校验:可解析的价格分区键必须同属一个 AreaId,否则拒绝发布
3. 下单/查价:地址匹配规则 → `zoneKeys` → 对明细 `分区`/`价格分区`/`zone` 因子
4. **偏远分区因素**:明细含 `偏远分区`/`remoteZone` 时,须偏远命中且因素值 = 生效 `RemoteAreaId`(或同名);非偏远或键不匹配则该明细不命中;明细无该因素则不筛偏远
产品配置**不再**维护 `PriceAreaId`(保存时置 null)。
### 4.2.1 客户组 / 全部客户
- 字典 `customer_group` 含 `all`(全部客户);与其它客户组**互斥**(前端选中 all 后其它置灰)
- 价格表只生成一列「全部客户」时,`CustomerGroupValues` JSON 键为「全部客户」
- 取价优先级:`全部客户`/`all` → 客户 `PriceTierKey` 精确键 → 「普通客户」回落
### 4.3 计算公式(字典 `calculation_formula`)
| code | 标签 | 金额 |
|------|------|------|
| `total` | 总价 | 明细单价(可含客户分组价) |
| `unit` | 单价 | 单价 × 计费重 |
| `continue_unit` → `continue` | 续单价 | 首重总价 + 续重 |
| `percent_declared` | 货值比例 | `round(DeclaredSum × rate / 100, 2)` |
| `percent_freight` | 运费比例 | 两趟计算:先算非运费比例项,再 `round(同币种运费合计 × rate / 100, 2)` |
约定:
- `rate` 存在 `calculatedValue`(百分数,如 `0.5` = 0.5%)
- `percent_declared`:`DeclaredSum<=0` 或与卖价币种不一致(有申报币种时)→ 跳过
- 无计费重时:仅允许不依赖重量的公式路径(如 total / percent_declared;percent_freight 依赖同批次运费)
### 4.4 费用项字典(`fee_item`)
常用:`freight` 运费、`handling` 操作费、`insurance` 保险费、`fuelSurcharge` 燃油附加费、`remoteFee` 偏远费。
---
## 5. 附加服务
### 5.1 配置
- 字典类型:`additional_service`
- `insurance` 保险
- `repackage` 更换包装
- 产品:`SalesProduct.AdditionalServices` = JSON 数组,如 `["insurance","repackage"]`
- 订单:`Order.AdditionalServices` = 客户勾选结果
### 5.2 下单 / 改服务
- PC / H5 下单:按产品可选服务渲染勾选 → `additionalServices: string[]`
- 非法 code(不在产品配置内)→ 拒绝
- **未支付**可改:`POST /order/additional-services/update` → 校验 → 重写服务 → 按当前阶段重算应收 → 刷新营销 → 尽力重算应付
- **已支付**禁止改(走应收冻结)
### 5.3 与收费关系
附加服务**不直接**挂费用;通过卖价计费条件触发,例如保险费项条件:
```text
附加服务包含('insurance')
```
条件留空的运费等基础项:始终尝试收取(再走分区/重量匹配)。
---
## 6. 偏远费
| 侧 | 配置 | 运行时 |
|----|------|--------|
| 应收 | 销售产品 `RemoteAreaId` + 卖价因素「偏远分区」可选 | 命中 → `Order.IsRemote=1`;条件 `是否偏远()==1`;明细偏远分区键 = 产品 RemoteAreaId |
| 应付 | 服务渠道 `RemoteAreaId` + 买价因素「偏远分区」可选 | 排货时按**渠道**偏远注入条件上下文;明细偏远分区键 = 渠道 RemoteAreaId |
两套分区可不同。偏远费项发布/保存时强制条件含「是否偏远」。推荐:运费项只挂价格分区;偏远费项挂偏远分区 + `是否偏远()==1`。
部署后请重新导入 OMS 预置字典(`pricing_factor` / `customer_group` 有变更)。
---
## 7. 发布与保存校验
卖价保存/发布:
- `costItem=insurance` → 计费条件不能为空/「无」
- `remoteFee`(或名称含「偏远」)→ 条件必须含「是否偏远」
- 分区因子不得混用多个 AreaId
买价:偏远费条件校验同上。
---
## 8. 理赔与保险(闭环)
`POST /order/mark-claimed`:
1. `ClaimAmount > 0`
2. 若 `DeclaredSum > 0` 且索赔额 > 申报额 → 拒绝
3. 订单含 `insurance` 附加服务:`HasInsurance=true`,索赔币种默认可取申报币种
4. 未购保险:允许理赔,备注侧可打 `[未购保险]`
前端理赔弹窗展示保险提示与申报上限。
---
## 9. 关键 API
| API | 作用 |
|-----|------|
| `POST /order/parcel/create` | 下单:应收 forecast + 尝试应付 + 预报快照 |
| `POST /order/price-check` | 查价(可带 additionalServices / DeclaredSum) |
| `POST /operation/inbound/create` | 入库:清空后重算应收/应付 inbound |
| `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-payable` | 重算应付(已有渠道时锁定渠道;允许渠道不在旧规则排货池,只重算买价) |
| `POST /order/fee/bind-snapshot-versions` | 显式切换订单绑定的规则/价格版本(可选随后重算应收/应付) |
| `POST /order/fee/reallocate` | 重新排货(取当前有效规则;成功后写回订单规则版本) |
| `POST /order/fee/rerun-inbound-billing` | 重跑入库计费(已支付拒绝,应收已锁) |
| `POST /order/fee/add` | 手工加费(应收已支付拒绝) |
| `POST /order/additional-services/update` | 未支付改附加服务并重算 |
| `POST /channel/sales-product/billing/simulate` | 用当前库内卖价做计费模拟 |
| `POST /order/mark-claimed` | 理赔(含保险规则) |
---
## 10. 前端落点
| 能力 | 位置 |
|------|------|
| 产品附加服务、去掉价格分区 | `/channel/salesProducts` 产品配置 |
| 计费条件(紧凑:输入 + AI + 插入字段) | 卖价/买价基础信息 |
| 计费模拟 | 卖价 Tab 工具栏 |
| 货值比例 / 运费比例 | 价格表明细计算公式字典 |
| 渠道偏远分区 | `/channel/serviceChannels` |
| 下单勾选附加服务 | PC `placeOrder`、H5 `H5PlaceOrderSheet` |
| 毛利 / 预报差额 / 改附加服务 | 运单管理 `OrderDetailDrawer` 费用 Tab |
| 导入对账件重尺 | 运单管理「更多」→「导入对账件重尺」 |
| 预置字典种子 | `system/dict/dict-batch-import.ts`(下载 OMS 预置字典) |
---
## 11. 核心代码索引
| 模块 | 路径 |
|------|------|
| 金额引擎 | `Oms.Application/.../ParcelBillingCalculator.cs` |
| 下单/入库/出库编排 | `OrderService.ParcelBilling.cs` |
| 改服务 / 冻结 / 对账导入 | `OrderService.OrderEdit.cs` |
| 对账件重尺 Excel | `OrderDownstreamWeightImportHelper.cs` |
| 查价 | `OrderService.ParcelQuote.cs` |
| 计费模拟 | `SalesProductService.BillingSimulate.cs` |
| 分区快照/混表校验 | `PriceAreaSnapshotHelper.cs` |
| 公式字段目录 | `AutoTriggerFormulaMeta.cs` / `formulaMeta.ts` |
| 计费条件编辑器 | `salesProducts/components/BillingConditionEditor.vue` |
| 单测 | `Oms.Tests/ParcelBillingCalculatorTests.cs` |
---
## 12. 运营配置速查
### 保险费(客户勾选才收)
1. 产品勾选附加服务 `insurance`
2. 卖价项:费用项=保险费,公式=货值比例,费率如 `0.5`
3. 计费条件:`附加服务包含('insurance')`
4. 客户下单勾选保险且填写申报金额 → 收取
### 偏远费(应收)
1. 产品绑偏远分区
2. 卖价项:`remoteFee` + 条件 `是否偏远()==1` + 总价/单价
### 偏远费(应付)
1. 渠道绑偏远分区
2. 买价项:同上条件与公式
### 燃油等按运费比例
卖价项:公式=运费比例,`calculatedValue`=百分数;一般条件留空或按国家等过滤。
---
## 13. 明确不做 / 后续可做
**已不做(或刻意不做):**
- 卖价项级「计费时机」字段
- 出库改已付应收
- 物理 drop `t_sales_product.PriceAreaId`
- 导入任意重量类型自动改应收(对账导入只动应付)
**可后续:**
- 查价页批量勾选附加服务
- 运费比例之外的多币种兑换
- DDP/关税代垫调整单
- 一票多件计费维度
- 发布前更完整的毛利模拟(含多候选渠道对比)
- 费用争议走调整单而非改原费用行
- 对账导入异步任务(超 2000 行)与应付版本审计
---
## 14. 相关计划
- 实施过程稿:[`附加服务与计费条件_b5d360e1.plan.md`](附加服务与计费条件_b5d360e1.plan.md)
- 本文件为**已落地逻辑**的权威查阅版;业务规则变更时优先更新本文件。