Files
dealerhub/PROGRESS_BATCH_A.md
T

12 KiB
Raw Blame History

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 应收风控作为主打卖点。