forked from erp-dev/erp
feat: big
This commit is contained in:
170
docs/api_v1_plate_order_detail_2026-06-30.md
Normal file
170
docs/api_v1_plate_order_detail_2026-06-30.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# API v1 Plate Order Detail 字段文档
|
||||
|
||||
日期:2026-06-30
|
||||
|
||||
## 接口
|
||||
|
||||
`GET /api/v1/plate-orders/{id}/`
|
||||
|
||||
获取单个开版订单详情。
|
||||
|
||||
## 权限与隔离
|
||||
|
||||
- 需要登录。
|
||||
- 使用 Django model permission:需要具备查看 `PlateOrder` 的权限。
|
||||
- 非 superuser 默认受 merchant 隔离限制,只能访问当前用户员工所属 merchant 下的数据。
|
||||
- 默认还受客户可见性限制;拥有 `printing.view_all_plateorders` 权限可突破客户可见性限制。
|
||||
- 订单不存在或无权限访问时返回 `404`。
|
||||
|
||||
## 响应字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `id` | integer | 开版订单 ID |
|
||||
| `original_id` | integer \| null | 克隆来源订单 ID |
|
||||
| `merchant_id` | integer \| null | 所属商户 ID |
|
||||
| `design_code` | string \| null | 设计编号 |
|
||||
| `plate_type` | string \| null | 起版情况,例如首版、复版 |
|
||||
| `plate_date` | datetime \| null | 下版时间 |
|
||||
| `plate_method` | string \| null | 开版方式 |
|
||||
| `plate_image` | array | 开版图原始数据列表 |
|
||||
| `plate_image_url` | string[] | 开版图 URL 列表,从 `plate_image` 中提取并转换为可访问 URL |
|
||||
| `image_name` | string \| null | 图片名称 |
|
||||
| `plate_notes` | string \| null | 打版注意事项 |
|
||||
| `reprint_reason` | string \| null | 复版原因 |
|
||||
| `urgency_level` | string | 紧急程度 |
|
||||
| `is_invalid` | boolean | 是否作废 |
|
||||
| `customer` | integer | 客户 ID |
|
||||
| `customer_name` | string | 客户名称 |
|
||||
| `customer_phone` | string \| null | 客户手机号 |
|
||||
| `area` | string \| null | 区域 |
|
||||
| `default_address` | string \| null | 默认地址 |
|
||||
| `salesperson` | integer \| null | 销售员员工 ID |
|
||||
| `salesperson_name` | string \| null | 销售员姓名 |
|
||||
| `merchandiser` | integer \| null | 跟单员员工 ID |
|
||||
| `merchandiser_name` | string \| null | 跟单员姓名 |
|
||||
| `designer` | integer \| null | 设计师员工 ID |
|
||||
| `designer_name` | string \| null | 设计师姓名 |
|
||||
| `style_name` | string \| null | 款号名称 |
|
||||
| `fabric` | string \| null | 布料 |
|
||||
| `fabric_source` | string \| null | 布料来源 |
|
||||
| `width` | string \| null | 幅宽 |
|
||||
| `production_method` | string \| null | 做货方式 |
|
||||
| `is_mark_frame` | boolean | 是否套唛架 |
|
||||
| `drawing_rating` | string \| null | 画图评级 |
|
||||
| `color_matching_rating` | string \| null | 调色评级 |
|
||||
| `sample_rating` | string \| null | 套样评级 |
|
||||
| `difficulty_rating` | string \| null | 难度评级 |
|
||||
| `sample_meter` | string \| null | 米样 |
|
||||
| `required_sample_meters` | decimal string \| null | 客户要求米样米数 |
|
||||
| `required_completion_date` | datetime \| null | 要求完成时间 |
|
||||
| `completion_date` | datetime \| null | 完成时间 |
|
||||
| `approval_result` | string \| null | 审批结果 |
|
||||
| `is_ordered` | boolean | 是否已下单 |
|
||||
| `customer_feedback` | string \| null | 客户修改意见 |
|
||||
| `print_count` | integer | 打印次数 |
|
||||
| `process` | integer | 关联流程 ID |
|
||||
| `process_name` | string \| null | 流程名称 |
|
||||
| `status` | string | 当前流程状态名称;无当前节点时通常表示已完成 |
|
||||
| `status_id` | integer \| null | 当前待执行状态 ID |
|
||||
| `is_completed` | boolean | 流程是否完成 |
|
||||
| `has_started` | boolean | 是否已有未撤销的流程记录 |
|
||||
| `progress_percentage` | integer | 流程进度百分比 |
|
||||
| `business_object_id` | integer \| null | 关联流程实例 ID |
|
||||
| `last_completed_state` | string | 最后一个已完成节点名称;没有时为空字符串 |
|
||||
| `created_by` | integer \| null | 创建人用户 ID |
|
||||
| `created_at` | datetime | 创建时间 |
|
||||
| `updated_at` | datetime | 更新时间 |
|
||||
|
||||
## `plate_image` 数据形态
|
||||
|
||||
`plate_image` 是 JSON 数组,常见元素结构如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"file_id": 123,
|
||||
"name": "图片名称",
|
||||
"path": "uploads/example.jpg",
|
||||
"url": "https://example.com/media/uploads/example.jpg",
|
||||
"size": 102400,
|
||||
"content_type": "image/jpeg",
|
||||
"uploaded_at": "2026-06-30T10:00:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
兼容历史数据时,元素也可能只有 `url` 或 `path`。前端展示图片优先使用 `plate_image_url`。
|
||||
|
||||
## 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 18839,
|
||||
"original_id": null,
|
||||
"merchant_id": 1,
|
||||
"design_code": "18839",
|
||||
"plate_type": "首版",
|
||||
"plate_date": "2026-06-30T10:00:00+08:00",
|
||||
"plate_method": "普通开版",
|
||||
"plate_image": [
|
||||
{
|
||||
"file_id": 123,
|
||||
"name": "开版图",
|
||||
"path": "uploads/plate/example.jpg",
|
||||
"url": "https://example.com/media/uploads/plate/example.jpg",
|
||||
"size": 102400,
|
||||
"content_type": "image/jpeg",
|
||||
"uploaded_at": "2026-06-30T09:58:00+08:00"
|
||||
}
|
||||
],
|
||||
"plate_image_url": [
|
||||
"https://example.com/media/uploads/plate/example.jpg"
|
||||
],
|
||||
"image_name": "开版图",
|
||||
"plate_notes": "注意事项",
|
||||
"reprint_reason": null,
|
||||
"urgency_level": "正常",
|
||||
"is_invalid": false,
|
||||
"customer": 1001,
|
||||
"customer_name": "示例客户",
|
||||
"customer_phone": "13800000000",
|
||||
"area": "广州",
|
||||
"default_address": "广州市示例地址",
|
||||
"salesperson": 11,
|
||||
"salesperson_name": "销售员A",
|
||||
"merchandiser": 12,
|
||||
"merchandiser_name": "跟单员B",
|
||||
"designer": 13,
|
||||
"designer_name": "设计师C",
|
||||
"style_name": "款号001",
|
||||
"fabric": "棉布",
|
||||
"fabric_source": "客户来布",
|
||||
"width": "150cm",
|
||||
"production_method": "印花",
|
||||
"is_mark_frame": false,
|
||||
"drawing_rating": null,
|
||||
"color_matching_rating": null,
|
||||
"sample_rating": null,
|
||||
"difficulty_rating": null,
|
||||
"sample_meter": "米样说明",
|
||||
"required_sample_meters": "5.00",
|
||||
"required_completion_date": "2026-07-01T18:00:00+08:00",
|
||||
"completion_date": null,
|
||||
"approval_result": null,
|
||||
"is_ordered": false,
|
||||
"customer_feedback": null,
|
||||
"print_count": 0,
|
||||
"process": 1,
|
||||
"process_name": "开版流程",
|
||||
"status": "画图",
|
||||
"status_id": 2,
|
||||
"is_completed": false,
|
||||
"has_started": true,
|
||||
"progress_percentage": 25,
|
||||
"business_object_id": 5001,
|
||||
"last_completed_state": "接单",
|
||||
"created_by": 7,
|
||||
"created_at": "2026-06-30T09:50:00+08:00",
|
||||
"updated_at": "2026-06-30T10:10:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
"id": 1,
|
||||
"merchant": 10,
|
||||
"name": "通用",
|
||||
"payload_processor": "",
|
||||
"speech_enabled": false,
|
||||
"created_at": "2026-04-10T12:00:00+08:00",
|
||||
"updated_at": "2026-04-10T12:00:00+08:00"
|
||||
}
|
||||
@@ -287,6 +289,7 @@
|
||||
| `merchant` | int | 所属商户 ID |
|
||||
| `name` | string | 分类名称 |
|
||||
| `payload_processor` | string | payload 增强器标识,未启用时为空字符串 |
|
||||
| `speech_enabled` | boolean | 该分类创建任务时是否触发语音播报;默认 `false` |
|
||||
| `created_at` | datetime | 创建时间 |
|
||||
| `updated_at` | datetime | 更新时间 |
|
||||
|
||||
@@ -301,13 +304,15 @@
|
||||
|------|------|------|------|
|
||||
| `name` | string | 是 | 分类名称,同商户下唯一 |
|
||||
| `payload_processor` | string | 否 | payload 增强器标识;当前可选值:`structured_description_v1` |
|
||||
| `speech_enabled` | boolean | 否 | 是否启用该分类的任务创建语音播报;默认 `false` |
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "售后",
|
||||
"payload_processor": "structured_description_v1"
|
||||
"payload_processor": "structured_description_v1",
|
||||
"speech_enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -331,6 +336,7 @@
|
||||
|------|------|------|
|
||||
| `name` | string | 分类名称,同商户下唯一 |
|
||||
| `payload_processor` | string | payload 增强器标识;传空字符串表示清空 |
|
||||
| `speech_enabled` | boolean | 是否启用该分类的任务创建语音播报 |
|
||||
|
||||
成功响应:`MissionCategory`
|
||||
|
||||
|
||||
@@ -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 删除支出明细
|
||||
|
||||
```
|
||||
|
||||
@@ -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 层作为推荐业务入口 |
|
||||
|
||||
118
docs/好布业金额计算报告.md
Normal file
118
docs/好布业金额计算报告.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 好布业 ERP 金额计算规则报告
|
||||
|
||||
**—— 计价引擎舍入规则逆向分析 ——**
|
||||
|
||||
编制日期:2026 年 6 月
|
||||
|
||||
---
|
||||
|
||||
## 一、结论摘要
|
||||
|
||||
经过逐步测试与逻辑反推,好布业 ERP 系统的金额计算规则已被完整还原。核心规则可用一行公式表达:
|
||||
|
||||
> **总价 = ROUND ( 数量 × 单价 , 0 ) —— 逢五进一(round-half-up)**
|
||||
|
||||
**关键特征:** 输入阶段不舍入,按原始小数相乘;只在生成总价时把乘积四舍五入到整数(元),逢 0.5 进位。
|
||||
|
||||
---
|
||||
|
||||
## 二、分析背景
|
||||
|
||||
在日常使用中发现,系统的总价金额始终为整数,单价或数量中携带的小数似乎在某一环节被处理掉了。为弄清系统究竟在哪一步、按什么规则处理小数,本次分析采用受控输入测试法:固定其他变量,逐组输入特定数值,观察输出总价,从而逆向推断计算引擎的真实行为。
|
||||
|
||||
需要回答三个核心问题:
|
||||
|
||||
- 舍入规则是什么——四舍五入、截断(向下取整)还是其他?
|
||||
- 舍入发生在哪一步——输入阶段(先舍单价/数量)还是乘积阶段(先乘后舍)?
|
||||
- 逢五如何处理——逢五进一还是银行家舍入(逢五取偶)?
|
||||
|
||||
---
|
||||
|
||||
## 三、测试数据与观察
|
||||
|
||||
以下为全部实测数据,按测试目的分组列出。
|
||||
|
||||
### 3.1 基础舍入测试
|
||||
|
||||
| 数量 | 单价 | 理论乘积 | 实际总价 | 说明 |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| 1 | 1.4 | 1.4 | **1** | 舍去 |
|
||||
| 1 | 1.49 | 1.49 | **1** | 舍去 |
|
||||
| 1 | 1.5 | 1.5 | **2** | 进位 |
|
||||
|
||||
**观察:** 1.5 → 2 进位,说明并非截断(若为截断/向下取整,1.5 应得 1)。结合 1.49 → 1,规则锁定为四舍五入。
|
||||
|
||||
### 3.2 对称性测试(小数放数量 vs 放单价)
|
||||
|
||||
| 数量 | 单价 | 实际总价 | 说明 |
|
||||
|:---:|:---:|:---:|:---:|
|
||||
| 1.49 | 1 | **1** | 小数在数量 |
|
||||
| 1 | 1.49 | **1** | 小数在单价 |
|
||||
| 1.5 | 1 | **2** | 小数在数量 |
|
||||
| 1 | 1.5 | **2** | 小数在单价 |
|
||||
|
||||
**观察:** 无论小数落在数量还是单价,处理结果一致,舍入对称。但这些组合均有一个因子为 1,乘积等于小数本身,无法区分“先舍输入”与“先乘后舍”。
|
||||
|
||||
### 3.3 关键测试:舍入发生在哪一步
|
||||
|
||||
使两个因子都不为整数且不为 1,让两种引擎给出不同结果:
|
||||
|
||||
| 数量 | 单价 | 先舍输入预期 | 先乘后舍预期 | 实际总价 |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| 1.5 | 1.5 | 2×2 = 4 | round(2.25) = 2 | **2** |
|
||||
|
||||
**结论:** 实际为 2,等于 round(1.5 × 1.5) = round(2.25)。证明系统先按原始小数相乘、再对乘积舍入,输入阶段不舍入。同时否定了“单价/数量在进入计算前即被取整”及“字段为 INT”的早期猜测——内部计算至少保留小数(很可能为 decimal)。
|
||||
|
||||
### 3.4 收尾测试:逢五进一 vs 银行家舍入
|
||||
|
||||
此前的进位样本(1.5→2、2.25→2)恰好两种规则结果相同,无法区分。选取乘积正好为 2.5 的点(2 为偶数,银行家舍入会归 2)进行判定:
|
||||
|
||||
| 数量 | 单价 | 逢五进一预期 | 银行家舍入预期 | 实际总价 |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| 1 | 2.5 | 3 | 2 | **3** |
|
||||
|
||||
**结论:** 实际为 3,确认为逢五进一(round-half-up),排除银行家舍入。
|
||||
|
||||
---
|
||||
|
||||
## 四、最终规则与计算流程
|
||||
|
||||
综合全部测试,系统计价流程如下:
|
||||
|
||||
| 环节 | 行为 |
|
||||
|:---|:---|
|
||||
| **输入(数量、单价)** | 保留原始小数,不做任何舍入 |
|
||||
| **乘法** | 用原始小数相乘,得到带小数的乘积 |
|
||||
| **生成总价** | 对乘积四舍五入到整数;乘积小数部分 ≥ 0.5 进位(逢五进一,非银行家舍入) |
|
||||
| **存储 / 显示** | 以整数(元)为单位呈现 |
|
||||
|
||||
**计算流程示意:**
|
||||
|
||||
```
|
||||
输入(原始小数) → 数量 × 单价(保留小数) → ROUND 到整数(逢五进一) → 整数元存储/显示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、系统性质判定
|
||||
|
||||
- **精度等级:** 对外结算精度为 0 位小数(整数元),但内部计算保留小数参与运算。
|
||||
- **舍入位置:** 位于乘积阶段(先乘后舍),而非输入阶段。
|
||||
- **舍入方式:** 四舍五入、逢五进一(round-half-up)。
|
||||
- **数据本质:** 金额字段对外表现为整数,但计算层非 INT 截断,更接近 decimal 计算后取整。
|
||||
- **适用定性:** 属于“元级整数结算”的简化型计价系统,不处理角、分、厘等小单位。
|
||||
|
||||
---
|
||||
|
||||
## 六、使用建议与注意事项
|
||||
|
||||
- 单条记录的小数部分会被舍入,多条汇总时可能产生“先汇总再取整”与“逐条取整再相加”的差异,对账时需明确以哪种口径为准。
|
||||
- 由于逢五进一,金额在临界值(乘积恰为 X.5)会整体偏高,长期累积对总额有轻微上偏影响,财务核算时可留意。
|
||||
- 若需更高精度(保留角/分),需在系统层调整结算精度,单纯依赖现有规则无法还原小数金额。
|
||||
- 建议在正式启用前,对“数量与单价均带两位以上小数”的组合再抽样复核,确保乘积舍入行为在更复杂数值下依旧一致。
|
||||
|
||||
---
|
||||
|
||||
## 附录:规则一句话总结
|
||||
|
||||
> 该 ERP 系统在金额计算中,按原始小数完成「数量 × 单价」后,对乘积统一四舍五入(逢五进一)为整数元,输入阶段不舍入,最终结算单位为元级整数金额。
|
||||
Reference in New Issue
Block a user