12 KiB
12 KiB
dealerhub · 批次 A 实施报告(P0 引擎前端配套)
依据:
docs/后续改造实施计划.md批次 A(A1–A5)+ 全局验收标准。 目标:把已完成的 P0 后端六项引擎(批次效期 / 多单位 / 自动取价 / 最低售价+抹零 / 信用额度 / 账龄+对账单)从"后端就绪"变成"前端可见可用"。 结果:全部落地,新增 17 个后端 API 测试(160 passed,原 143 零回归),manage.py check0 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
- DRF 同路径双 action 互相覆盖:
finance/statements/share/原先写了create_share(POST) 与list_shares(GET) 两个url_path="share"的 action——DRF 只保留后者注册,导致 GET 列表永久 405。修复:合并为单 action 按request.method分派(_create_share提为私有方法)。 ?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 冒烟)。
- 踩坑记录(供后续批次参考):
- adrf async 视图里 不能直接触碰惰性 FK /
refresh_from_db(),必须整体塞进sync_to_async(本轮 3 处线上 500 均源于此)。 - DRF 同一
url_path只能有一个 action,同名同路径后者覆盖前者(表现为 405)。 ?format=是 DRF 保留参数(URL_FORMAT_OVERRIDE),自定义输出格式请换参数名。- Granian 在 Windows 上不支持多 worker(自动回落 1),且旧进程 socket 可能残留——
改动后重启务必
netstat确认端口只有一个 PID,否则旧实例会抢答导致"改了没生效"。
- adrf async 视图里 不能直接触碰惰性 FK /
- 下一步(按计划):批次 B(AI 应收风险预警 → AI 开单 → AI 经营问答),与批次 C(计费/演示账套/官网/上线)并行;建议先做 B1 AI 应收风控作为主打卖点。