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

79 lines
6.5 KiB
Markdown
Raw 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.
# 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;
- 非批次/非单位/老式开单路径行为与改造前一致(回归保障)。