# 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/`(AllowAny,过期 404,租户从 share 记录解析,不依赖请求头)。 - **测试**:账龄分桶正确、对账单期初/期末平衡、分享链接可匿名访问且过期失效。 ## 验收标准 - 每项附专项测试文件 `tests/test_p0_*.py`;全量 `pytest` 通过且不破坏既有 103 例; - `manage.py check` 0 issues; - 非批次/非单位/老式开单路径行为与改造前一致(回归保障)。