Files
dealerhub/docs/P0改造实施计划.md

6.5 KiB
Raw Permalink Blame History

P0 改造实施计划(逐项方案)

依据《差距分析与改造清单.md》的 P0 六项,逐项给出落地方案。实现顺序即优先顺序;每项完成后跑对应测试,最后全量回归。

全局前置:一次模型变更批次

以下新字段/新模型一次 makemigrations 生成(避免反复迁移):

App 变更
catalog Product.is_batch_managed、Product.shelf_life_days、Product.min_sale_price;新模型 UnitConversion(tenant, product, unit, rate)
inventory 新模型 StockBatch(tenant, warehouse, product, batch_no, production_date, expiry_date, on_hand, locked, unit_cost),唯一键 (tenant, warehouse, product, batch_no);StockMovement.batch_detail(JSON,记录 FEFO 分摊明细)
partner 新模型 CustomerProductPrice(tenant, customer, product, price)
sales SalesBill.round_off(抹零额);SalesOrderLine/SalesBillLine.source_unit(FK Unit 可空)+source_quantity(录入单位与数量,quantity 恒为基本单位)
purchase PurchaseBillLine.batch_no/production_date/expiry_date(批次入库穿透);行加 source_unit/source_quantity
finance 新模型 StatementShare(tenant, customer, token UUID, date_from, date_to, expires_at)
notify AlertRule.RULE_TYPE_CHOICES 增加 batch_expiry

单位语义约定(#2):source_quantity × rate = quantity(基本单位);有录入单位时 unit_price 按录入单位计价,amount = source_quantity × unit_price;无录入单位时一切按基本单位(现状不变)。


#1 批次/效期管理

  • 模型:见上表。批次账 StockBatch 与总账 Stock 并行维护,总量恒等。
  • 服务(inventory/services.py):
    • inbound(..., batch_no=None, production_date=None, expiry_date=None):商品 is_batch_managed=True 时必须带 batch_no,写 StockBatch(upsert)+ 总账;否则行为不变。
    • outbound(...):批次商品按 FEFO(expiry_date 升序、空值最后、再按 id)自动分摊到多批次;总账一次扣减;分摊明细写入该次 StockMovement.batch_detail。任一批次分摊失败→整单回滚。
    • 非批次商品路径完全不变(现有测试零影响)。
  • 采购穿透:purchase/services.create_purchase_bill 行支持 batch_no/production_date/expiry_date 落行并传给 inbound。
  • 近效期预警:notify 新增规则 batch_expiry(threshold=天数,默认 30);check_batch_expiry_alerts 扫描 on_hand>0 且 expiry_date ≤ today+N 的批次,按"批次+当日"去重发通知;挂入 run_all_alert_checks。
  • API:GET /api/v1/inventory/batches/(只读,支持按 warehouse/product 过滤)。
  • 测试:批次入库聚合、FEFO 跨批次分摊、不足拒绝、近效期预警去重、采购带批次入库、非批次商品回归不受影响。

#2 多单位换算

  • 模型:UnitConversion(product, unit, rate),rate=1 录入单位等于多少基本单位;唯一键 (tenant, product, unit)。
  • 服务:catalog/services.py::to_base(product, unit, qty)——unit=base_unit 直接返回;否则查换算表,缺失报错。
  • 单据:sales/purchase 的 order/bill 行(4 个模型)支持 source_unit(id 或 code) + source_quantity;服务内换算写 quantity。字段为可空,老调用完全兼容。
  • 测试:换算开单(2 箱×24=48 基本单位出库)、无换算率报错、老式行(无单位)回归。

#3 开单自动取价

  • 模型:CustomerProductPrice。
  • 服务:partner/services.py::quote_price(tenant, customer, product),优先级:
    1. 客户专属价(CustomerProductPrice)
    2. 价格等级价(PriceLevel.discount_rate>0 时 sale_price × rate)
    3. 最近成交价(该客户+商品最近一张已过账销售单行单价)
    4. 默认售价 返回 {price, source, last_price, min_price, max_price}(min/max 来自该客户历史成交)。
  • 接入开单:create_sales_bill 行未传 unit_price 时自动取价;换算单位时默认价 = 基本单位价 × rate。
  • API:GET /api/v1/partner/price-quote/?customer_id=&product_ids=(批量报价,供前端开单页预填)。
  • 测试:四级优先级、last/min/max 正确、开单不传价自动填充。

#4 最低售价 + 抹零

  • 校验:Product.min_sale_price>0 且成交价(按基本单位折算)低于它时抛错;行传 allow_below_min=True 视为审批放行(记入 remark)。
  • 抹零:SalesBill.round_off;create_sales_bill(round_to=0.01|0.1|1) 按向下取整计算抹零额(如 100.56 元抹到元 → round_off=0.56),total_amount 为净额;round_off > round_to 拒绝;应收金额=净额。
  • 测试:低于最低价拒绝/放行、三种 round_to 的金额计算、应收与 total 一致。

#5 信用额度落地

  • 服务:partner/services.py::check_credit(tenant, customer, extra_amount)——outstanding = 该客户 open/partial 应收 balance 之和;credit_limit>0 且 outstanding+extra > limit 时超限。
  • 接入过账:confirm_sales_bill 过账前校验,超限抛 CreditLimitExceeded(force=True 放行并写 warning 通知);占用达 90% 发预警通知。
  • 测试:限额内通过、超限拒绝、force 放行+通知、无限额(0)不校验。

#6 账龄分析 + 对账单 + 公开分享

  • 账龄:finance/services.receivable_aging(tenant, as_of=None, customer=None)——按 bill_date 距 today 天数分桶 0-30/31-60/61-90/91-180/181-365/365+,余额=balance;支持按客户汇总。
  • 对账单:customer_statement(tenant, customer, date_from, date_to)——期初=open 应收中 bill_date<from 的余额合计;明细=区间内应收单(借)+收款核销(贷,按 receipt.bill_date);逐行滚动余额,期末=期初+借-贷。
  • 公开分享:StatementShare(token=uuid4, expires_at);受保护 API POST /api/v1/finance/statements/share/(生成)与 GET /api/v1/finance/statements/share/(列表/吊销);公开 GET /api/v1/open/statements/<token>/(AllowAny,过期 404,租户从 share 记录解析,不依赖请求头)。
  • 测试:账龄分桶正确、对账单期初/期末平衡、分享链接可匿名访问且过期失效。

验收标准

  • 每项附专项测试文件 tests/test_p0_*.py;全量 pytest 通过且不破坏既有 103 例;
  • manage.py check 0 issues;
  • 非批次/非单位/老式开单路径行为与改造前一致(回归保障)。