---
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/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)  
- 本文件为**已落地逻辑**的权威查阅版；业务规则变更时优先更新本文件。
