1
0
forked from erp-dev/erp

feat: big

This commit is contained in:
2026-07-01 11:51:13 +08:00
parent 7c9b77afb0
commit 5170700234
28 changed files with 933 additions and 37 deletions

View File

@@ -58,7 +58,10 @@ api_v2/views/cost.py # API View不放在 cost 内部)
| `id` | BigAutoField (PK) | |
| `merchant` | FK → Merchant | 所属商户 |
| `category` | FK → CostCategory | 支出类目 |
| `amount` | DecimalField(15, 2) | 金额 |
| `amount` | DecimalField(15, 2) | 最终支出金额 / 统计金额。普通支出手工填写;倍数型支出由 `unit_amount * quantity` 计算写入 |
| `unit_amount` | DecimalField(15, 4, null) | 单价 / 基数金额,如临时工日薪 |
| `quantity` | DecimalField(12, 4, null) | 数量 / 倍数,如人天、小时、件数 |
| `unit_name` | CharField(max_length=20, null) | 单位名称,如 `人天``小时``件` |
| `occurred_at` | DateField | 发生日期 |
| `operator` | FK → Employee | 经办人 |
| `image1` | ImageField (null) | 凭证图片 |
@@ -80,10 +83,13 @@ api_v2/views/cost.py # API View不放在 cost 内部)
class CostEntryInput:
category_key: str # 类目标识键,用于匹配 CostCategory.unique_key
category_name: str # 类目显示名(匹配不到时用此名自动创建)
amount: Decimal
amount: Decimal | None # 最终金额;倍数型支出可传 None由 unit_amount * quantity 计算
occurred_at: date
source_module: str
source_id: str
unit_amount: Decimal | None = None
quantity: Decimal | None = None
unit_name: str = ''
remarks: str = ''
```
@@ -143,10 +149,24 @@ class PrintingCostProvider:
| 函数 | 说明 |
|------|------|
| `ensure_category(*, merchant, category_key, category_name)` | 按 key 查找或创建支出类目 |
| `create_cost_entry(*, merchant, category, amount, occurred_at, operator, image1, image2, source_module, source_id, remarks)` | 创建支出记录 |
| `create_cost_entry(*, merchant, category, occurred_at, amount, unit_amount, quantity, unit_name, operator, image1, image2, source_module, source_id, remarks)` | 创建支出记录;普通支出使用 `amount`,倍数型支出使用 `unit_amount + quantity` 自动计算最终 `amount` |
| `collect_from_provider(provider, *, merchant, start_date, end_date)` | 从 Provider 采集成本数据 |
| `aggregate_by_category(*, merchant, start_date, end_date)` | 按类别汇总group by category |
### 5.1 金额公式与写入约束
`CostEntry.amount` 永远表示最终支出金额,也是所有统计、排序、报表的唯一金额口径。倍数型支出使用 `unit_amount * quantity` 推导最终金额,保存时写回 `amount`;普通支出不填写公式字段,直接保存手工 `amount`
规则:
- `unit_amount``quantity` 必须同时填写或同时为空。
-`unit_amount``quantity` 同时存在时,`amount` 以公式计算结果为准,手工传入的 `amount` 会被覆盖。
- 当公式字段为空时,`amount` 必须填写。
- `unit_name` 只用于展示单位,不参与金额计算。
> **WARNING: 禁止使用 `QuerySet.update()`、`bulk_update()` 或 SQL 直接更新 `CostEntry.amount`、`unit_amount`、`quantity`。这些写法不会触发 `CostEntry.save()`,会绕过金额公式重算,可能造成统计金额错误。更新支出明细必须使用 service 入口或实例 `save()`;如果确实需要批量修正,必须编写专门的数据迁移/管理命令,并在命令内逐条调用 `save()`。**
---
## 6. API 设计 (api_v2)
@@ -177,4 +197,6 @@ class PrintingCostProvider:
| 4 | `CostCategory.unique_key` 用于 Port 匹配 | 比按 `name` 匹配更稳定,避免重名/改名问题 |
| 5 | `CostEntry` 带两个 `ImageField` | 用户要求:一个用于凭证图片,一个预留备用 |
| 6 | 汇总 API 按 `start/end` 时间段 + `group by category` | 用户指定的统计方式 |
| 7 | 第一版不做 Provider 注册表 | Provider 暂时只有一个调用入口 `collect_from_provider()`,后续可扩展为注册表模式 |
| 7 | 第一版不做 Provider 注册表 | Provider 暂时只有一个调用入口 `collect_from_provider()`,后续可扩展为注册表模式 |
| 8 | `amount` 固定为最终统计金额 | 兼容普通金额支出与倍数型支出,避免统计层判断 `amount` 的双重语义 |
| 9 | 金额公式在 `CostEntry.save()` 兜底计算 | 保证 create/update 经实例保存时都能重算 `amount`service 层作为推荐业务入口 |