Files
dealerhub/PROGRESS_BATCH_A.md

164 lines
12 KiB
Markdown
Raw Permalink 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.
# dealerhub · 批次 A 实施报告(P0 引擎前端配套)
> **依据**:`docs/后续改造实施计划.md` 批次 A(A1–A5)+ 全局验收标准。
> **目标**:把已完成的 P0 后端六项引擎(批次效期 / 多单位 / 自动取价 / 最低售价+抹零 /
> 信用额度 / 账龄+对账单)从"后端就绪"变成"前端可见可用"。
> **结果**:全部落地,新增 17 个后端 API 测试(**160 passed**,原 143 零回归),
> `manage.py check` 0 issues,`npm run build` 通过,Playwright 真实浏览器全链路验证通过。
> **顺带修掉 2 个真 bug**(详见 §6)。
---
## 一、后端补口(A 批次唯一需要的 API 侧改动)
| 新增接口 | 用途 | 落点 |
|---|---|---|
| `GET /catalog/products/<id>/units/` | 商品可用录入单位 + 换算率 + 折算售价(含基本单位),供开单页"选择录入单位" | `apps/catalog/views.py` |
| `POST /sales/bills/create-bill/` | 按行解析单位/取价/抹零后建草稿单;余额不足返回 400 `insufficient_stock` | `apps/sales/views.py` |
| `POST /sales/bills/<id>/confirm/` | 过账;**402 + `code=credit_limit_exceeded`** 可前端弹"强制过账",`force=true` 放行 | 同上 |
| `POST /purchase/bills/create-bill/` | 建草稿(支持批次号/生产日/到期日/录入单位) | `apps/purchase/views.py` |
| `POST /purchase/bills/<id>/confirm/` | 过账(写库存 + 批次账 + 应付) | 同上 |
| `POST /finance/statements/share/<token>/revoke/` | 吊销对账单分享链接 | `apps/finance/views.py` |
| `GET /open/statements/<token>/`(增强) | 浏览器/`?view=html` → 客户侧**可打印 HTML 页面**;默认仍 JSON | `apps/finance/views.py` + `statement_html.py`[新] |
**业务异常 → HTTP 语义映射**(前端可判别,不再吞成 500):
| 服务层异常 | HTTP | 响应体 |
|---|---|---|
| `CreditLimitExceeded` | 402 | `{code, detail, customer_code, outstanding, limit, this_bill}` |
| `BelowMinPrice` | 400 | `{code: "below_min_price", product_code, unit_price, min_price}` |
| `InsufficientStock` | 400 | `{code: "insufficient_stock", detail}` |
| 状态非法 | 400 | `{code: "invalid_state", detail}` |
**序列化器补字段**:`ProductSerializer` 增 `base_unit/base_unit_name/min_sale_price/is_batch_managed/shelf_life_days`;
`SalesBillSerializer` 增 `customer_name/round_off`,行增 `source_unit/source_quantity/unit_name`;
`PurchaseBillSerializer` 增 `supplier_name`,行增批次三字段 + `unit_name/is_batch_managed`。
## 二、A1 销售开单页(`SalesBills.vue`,重写)
- **自动取价预填**:选客户 → 逐行调 `price-quote`,行内显示来源标签(客户专属价/等级价/上次成交价/默认售价)+ `上次 ¥x` + `历史区间 min~max`;单价可改。
- **多单位录入**:行内单位下拉列 `瓶/箱×24/提×6`;切换单位按 rate 重算单价,数量按录入单位填,提交带 `source_unit`。
- **抹零**:单据级下拉(不抹/分/角/元),底部实时"行合计 → 抹零 -x → 应收合计"三行预览。
- **最低售价防呆**:成交价折算低于 `min_sale_price` 时单价框描橙,保存前弹"审批放行"确认,确认即行带 `allow_below_min=true`;点"返回修改"则中止。
- **信用额度**:选客户即显进度条(已用/总额度/可用),≥80% 橙、≥100% 红;过账遇 402 弹"信用额度超限 → 强制过账?"确认框走 `force=true`。
- **列表**:新增"新建销售单"入口;草稿行带"过账"按钮;金额列显示 `(抹 x.xx)`;已过账可打印。
## 三、A2 客户档案页(`Customers.vue`[新])
- 表格列:编码/名称/联系人/电话/信用额度/价格等级/状态。
- **行展开**:左半 = 额度占用进度条(<80% 绿 / 80–99% 橙 / ≥100% 红 + 文案"已超限,赊销将被拦截(可强制放行并记录预警)");右半 = 账龄六桶表 + 占比 + "合计未结"。
- 路由 `/customers`,侧栏"进销存业务 → 客户档案"。
## 四、A3 财务页"账龄与对账单"标签(`FinanceReports.vue` 扩展)
- **应收账龄**:六桶柱状图(0-30 绿 / 61-180 橙 / 181+ 红)+ 明细表(金额、占比),支持 `as_of` 与单客户过滤,合计未结高亮。
- **客户对账单**:选客户 + 日期区间 → 期初/期末描述 + 逐行滚动余额明细(应收红、收款绿)→"生成分享链接"按钮(自动复制到剪贴板,7 天有效)。
- **分享记录表**:客户/区间/有效期/状态(有效/已吊销)+ 复制链接 / 预览(新标签打开客户侧页面)/ 吊销。
## 五、A4 + A5
**A4 批次效期(`Batches.vue`[新],路由 `/batches`)**
- 过滤:仓库 / 商品 / 只看有量 / **只看近效期(≤30 天)**;汇总"批次 N 个|在库合计 X"。
- 列表:批次号、生产/到期日、**距到期天数着色**(≤7 红、≤30 橙、>30 绿、已过期红并显示"已过期 N 天")、结存/可用/成本/货值。
- `Notify.vue`:通知类别**图标分类**(🏷️ 批次效期 / 💳 信用 / 📦 库存 / 💰 逾期 / ⚠️ 其他预警)。
- 打印:销售/进货默认模板**条件列 "批次号"**(`{{#if has_batch_lines}}`),销售单批次号来自出库流水的 FEFO 分摊明细(`batch_detail`),多批分摊以 `/` 连接。
**A5 采购开单(`PurchaseBills.vue` 重写)**
- 新建入口 + 行级单位/数量/单价;**批次信息区**(仅批次商品行出现):批次号(必填)/生产日期/到期日/保质期。
- 生产日期选后**按 `shelf_life_days` 自动推算到期日**并提示;批次号缺失时前端拦截(与后端校验一致)。
- 列表草稿行可"过账";金额合计实时。
## 六、顺带修掉的两个真 bug
1. **DRF 同路径双 action 互相覆盖**:`finance/statements/share/` 原先写了 `create_share`(POST) 与 `list_shares`(GET) 两个 `url_path="share"` 的 action——DRF 只保留后者注册,导致 **GET 列表永久 405**。修复:合并为单 action 按 `request.method` 分派(`_create_share` 提为私有方法)。
2. **`?format=` 与 DRF 内容协商冲突**:对账单公开开关最初用 `?format=html`,但 DRF 默认 `URL_FORMAT_OVERRIDE="format"`,找不到 html renderer 会直接 **404**。修复:改用 `?view=html`,并在 docstring 里写明这个坑。
## 七、验证证据
### 1. 后端回归(零破坏)
```
$ python -m pytest tests/ -p no:cacheprovider
160 passed in 3.50s # 原 143 + 新增 17(tests/test_batch_a_api.py)
$ python manage.py check
System check identified no issues (0 silenced).
```
### 2. `npm run build`
```
✓ built in 9.55s
dist/assets/Customers-Cge4xKIc.js 4.57 kB ← 新页面已进产物
dist/assets/Batches-BNgU4pW7.js 3.93 kB ← 新页面已进产物
```
### 3. Playwright 真实浏览器(Granian ASGI,127.0.0.1:8010)
| 场景 | 结果 |
|---|---|
| 登录 → 销售开单 → 选客户 | 信用额度条显示 `已用 ¥0 / 共 ¥800 / 可用 ¥800` |
| 选商品 P001 | 单价自动预填 2.00,来源标签"默认售价",金额联动 |
| 单位切"箱×24" | 单价 48.00、金额 ¥48.00(2×24 正确换算) |
| 单价改 48.66 + 抹元 | 底部实时"行合计 48.66 → 抹零 -0.66 → 应收 48.00" |
| 保存并过账 | 单号 XS202609100001,库存 500→476 |
| 信用超限(额度 800,下单 900) | HTTP **402** + `code=credit_limit_exceeded`,提示"已用 48 + 本单 900 > 额度 800";`force=true` → 200 过账 + 写超限预警 |
| 批次页 | B20260915 剩 14、距到期 **21 天**;勾"只看近效期"过滤正确 |
| 客户档案展开 | 李四超市:`已用 ¥948 / 总额度 ¥800 / 可用 ¥-148` + 100% 红条 + "已超限" + 账龄六桶合计 948 |
| 财务页账龄标签 | 六桶柱状图 + 明细表,合计未结 1723 |
| 对账单生成 | 期初 0 → 明细 2 行 → 期末 948,逐行滚动余额正确 |
| 分享链接 → 匿名访问 | 新标签打开**客户侧对账单页面**(卡片式期初/本期应收/收款/期末 + 明细表 + 打印按钮) |
| 吊销后访问 | HTTP **404** |
| 批次打印 | 销售单 HTML 含"批次号"列与 `B20260901/B20260915`(FEFO 跨批分摊) |
| 非批次打印 | 无"批次号"列(条件块正确) |
| 通知页扫描 | 🏷️ 近效期(B20260915 距到期 21 天)、💳 信用超限(李四超市,已放行)两条新预警 |
### 4. 关键接口手测(curl)
```
GET /api/v1/catalog/products/1/units/ → 瓶(×1, ¥2) / 箱(×24, ¥48) / 提(×6, ¥12)
POST /api/v1/sales/bills/create-bill/ → total 48.0000, round_off 0.6600, quantity 24, source_quantity 1
POST /api/v1/sales/bills/1/confirm/ → state=confirmed;库存 500→476
POST /api/v1/sales/bills/2/confirm/ → 402 credit_limit_exceeded → force → 200
GET /api/v1/finance/statements/receivable-aging/ → 0-30: 948.0000, total 948.0000
POST /api/v1/finance/statements/share/ → 201 + token
GET /api/v1/open/statements/<tok>/ → 200 text/html(对 账 单 / 李四超市 / 948.00 / window.print)
POST /api/v1/finance/statements/share/<tok>/revoke/ → 200;随后匿名访问 404
```
## 八、变更文件清单
```
backend/
├── apps/catalog/views.py [改] +products/<id>/units/ 动作接口
├── apps/catalog/serializers.py [改] 商品增单位/最低售价/批次字段
├── apps/sales/views.py [改] +create_bill / +confirm_bill(402/400 语义映射)
├── apps/sales/serializers.py [改] 行增单位字段、单增客户名/抹零额
├── apps/purchase/views.py [改] +create_bill / +confirm_bill
├── apps/purchase/serializers.py [改] 行增批次三字段/单位名/批次标记
├── apps/finance/views.py [改] share 合并按方法分派、+revoke、+view=html 输出
├── apps/finance/statement_html.py [新] 客户侧对账单 HTML 渲染(全字段转义)
├── apps/printing/renderer.py [改] +{{#if}}/{{else}}(支持嵌套)
├── apps/printing/services.py [改] 行上下文增批次号/FEFO 分摊、默认模板条件批次列
└── tests/test_batch_a_api.py [新] 17 例
frontend/src/
├── pages/SalesBills.vue [重写] 开单:取价/单位/抹零/最低价/信用
├── pages/PurchaseBills.vue [重写] 开单 + 批次信息区 + 自动推算到期日
├── pages/Customers.vue [新] 客户档案:额度进度条 + 账龄
├── pages/Batches.vue [新] 批次效期页(距到期着色 + 近效期过滤)
├── pages/FinanceReports.vue [改] +"账龄与对账单"标签(柱状图/对账单/分享管理)
├── pages/Notify.vue [改] 通知类别图标分类
├── router/index.js [改] +/customers /batches
└── layouts/MainLayout.vue [改] 侧栏 +客户档案 +批次效期
frontend/dist/ [重建]
```
## 九、遗留 / 下一步
- **A 批次范围内无遗留**:计划中 A1–A5 五个子项验收点全部覆盖(含 Playwright 冒烟)。
- **踩坑记录**(供后续批次参考):
1. adrf async 视图里 **不能直接触碰惰性 FK / `refresh_from_db()`**,必须整体塞进 `sync_to_async`(本轮 3 处线上 500 均源于此)。
2. DRF **同一 `url_path` 只能有一个 action**,同名同路径后者覆盖前者(表现为 405)。
3. `?format=` 是 DRF 保留参数(`URL_FORMAT_OVERRIDE`),自定义输出格式请换参数名。
4. Granian 在 Windows 上**不支持多 worker**(自动回落 1),且旧进程 socket 可能残留——
改动后重启务必 `netstat` 确认端口只有一个 PID,否则旧实例会抢答导致"改了没生效"。
- **下一步(按计划)**:批次 B(AI 应收风险预警 → AI 开单 → AI 经营问答),与批次 C(计费/演示账套/官网/上线)并行;建议先做 **B1 AI 应收风控**作为主打卖点。