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

@@ -2,6 +2,29 @@
本文档面向前端,描述 `cost` 成本模块的 API。
## 2026-06-30 前端变更摘要
本次支出明细接口兼容新增“倍数型支出”字段。支出类目接口无变化,按类目汇总接口的响应结构无变化,但汇总金额仍来自 `amount`
**新增字段(支出明细列表、详情、创建、修改均涉及):**
| 字段 | 类型 | 位置 | 说明 |
|------|------|------|------|
| `unit_amount` | decimal string / null | request + response | 单价 / 基数金额,如临时工日薪。响应中为字符串,如 `"200.0000"` |
| `quantity` | decimal string / null | request + response | 数量 / 倍数,如 `"3.0000"` |
| `unit_name` | string | request + response | 单位名称,如 `人天``小时``件`,可为空字符串 |
**兼容规则:**
- 普通支出:继续传 `amount`,不传 `unit_amount``quantity` 即可。
- 倍数型支出:传 `unit_amount + quantity``amount` 可不传;后端保存时计算 `amount = unit_amount * quantity`
- 如果同时传 `amount``unit_amount + quantity`,后端以公式计算结果为准,返回的 `amount` 是计算后的最终金额。
- `unit_amount``quantity` 必须同时填写或同时为 `null`/不传;只传一个会返回 `400`
- 将倍数型支出改回普通支出时PUT 需要同时传:`amount``unit_amount: null``quantity: null``unit_name` 可传空字符串。
---
## 基本约定
- Base URL: `/api/v2`
@@ -153,6 +176,9 @@ GET /api/v2/cost-entries/
"category_id": 1,
"category_name": "电费",
"amount": "350.00",
"unit_amount": null,
"quantity": null,
"unit_name": "",
"occurred_at": "2026-06-01",
"operator_id": 12,
"image1": "",
@@ -170,7 +196,10 @@ GET /api/v2/cost-entries/
| 字段 | 类型 | 说明 |
|------|------|------|
| `amount` | string | 金额Decimal 转字符串,前端展示时注意格式化) |
| `amount` | string | 最终支出金额 / 统计金额Decimal 转字符串,前端展示时注意格式化) |
| `unit_amount` | string/null | 单价 / 基数金额;普通支出为 `null` |
| `quantity` | string/null | 数量 / 倍数;普通支出为 `null` |
| `unit_name` | string | 单位名称;普通支出为空字符串 |
| `occurred_at` | string | 发生日期,`YYYY-MM-DD` |
| `operator_id` | int | 经办人 ID自动从当前登录用户获取 |
| `image1` | string | 凭证图片 URL七牛云 CDN无则为空字符串 |
@@ -191,7 +220,10 @@ Content-Type: `multipart/form-data`(支持图片上传)或 `application/json
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `category_id` | int | **是** | 支出类目 ID |
| `amount` | decimal | **是** | 金额,如 `"350.00"` |
| `amount` | decimal | 条件必填 | 普通支出必填;倍数型支出可不传。最终统计金额,如 `"350.00"` |
| `unit_amount` | decimal | 否 | **新增**。单价 / 基数金额;和 `quantity` 必须同时填写或同时为空 |
| `quantity` | decimal | 否 | **新增**。数量 / 倍数;和 `unit_amount` 必须同时填写或同时为空 |
| `unit_name` | string | 否 | **新增**。单位名称,如 `人天``小时``件` |
| `occurred_at` | date | **是** | 发生日期,`YYYY-MM-DD` |
| `image1` | file | 否 | 凭证图片 |
| `image2` | file | 否 | 备用凭证图片 |
@@ -199,7 +231,7 @@ Content-Type: `multipart/form-data`(支持图片上传)或 `application/json
| `source_id` | string | 否 | 来源记录 ID |
| `remarks` | string | 否 | 备注 |
请求示例JSON
请求示例JSON,普通支出
```json
{
@@ -210,6 +242,21 @@ Content-Type: `multipart/form-data`(支持图片上传)或 `application/json
}
```
请求示例JSON**新增:倍数型支出**
```json
{
"category_id": 2,
"unit_amount": "200.00",
"quantity": "3",
"unit_name": "人天",
"occurred_at": "2026-06-01",
"remarks": "临时工 3 人天"
}
```
倍数型支出成功响应中的 `amount` 会是后端计算后的最终金额,例如 `"600.00"`
成功响应 `201`:返回创建的支出明细对象。
错误响应 `400`:校验失败。
@@ -238,7 +285,10 @@ Content-Type: `multipart/form-data` 或 `application/json`
| 字段 | 类型 | 说明 |
|------|------|------|
| `category_id` | int | 切换类目 |
| `amount` | decimal | 金额 |
| `amount` | decimal/null | 普通支出金额;倍数型支出若公式字段存在,会被后端公式结果覆盖 |
| `unit_amount` | decimal/null | **新增**。单价 / 基数金额;传 `null` 可清除公式字段 |
| `quantity` | decimal/null | **新增**。数量 / 倍数;传 `null` 可清除公式字段 |
| `unit_name` | string | **新增**。单位名称;可传空字符串清空 |
| `occurred_at` | date | 发生日期 |
| `image1` | file | 凭证图片 |
| `image2` | file | 备用凭证图片 |
@@ -248,6 +298,27 @@ Content-Type: `multipart/form-data` 或 `application/json`
成功响应 `200`:返回更新后的支出明细对象。
修改倍数型支出数量示例:
```json
{
"quantity": "4"
}
```
后端会按已有 `unit_amount * quantity` 重算并返回新的 `amount`
将倍数型支出改回普通支出示例:
```json
{
"amount": "550.00",
"unit_amount": null,
"quantity": null,
"unit_name": ""
}
```
### 2.5 删除支出明细
```

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 层作为推荐业务入口 |