6.5 KiB
6.5 KiB
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),优先级:- 客户专属价(
CustomerProductPrice) - 价格等级价(
PriceLevel.discount_rate>0时sale_price × rate) - 最近成交价(该客户+商品最近一张已过账销售单行单价)
- 默认售价
返回
{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);受保护 APIPOST /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 check0 issues;- 非批次/非单位/老式开单路径行为与改造前一致(回归保障)。