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