feat(scrap): 报废原因分类 + 角色级审批
报废要回答「这笔损失出在哪个环节」,并让主管审批不再依赖逐个指定人。
- 新增「报废原因分类」字段(scrap_approval + trans_scrap 各一列),
只有两个互斥口径:生产报废(走 Track 的)/ 库存报废(MOM 自身流程的)。
不传即库存报废 —— 这条二分法在写入那一刻就成立,不依赖任何推导。
⚠️ 不能从 source_table 推导:Track 的生产报废与手工的不良品退回共用
同一张 trans_defective_goods 表,推导会把生产损失算成库存损失。
- scrap_approval 加 company_name / source_ref:前者是公司隔离快照,
后者是外部单据的幂等锚点(Redis 未部署,prevent_double_submit 全程
fail-open,唯一索引是唯一防线)。
- 审批从「只认 type=user」放宽到「user 或 role」——主管角色都能审,
谁审就记谁。★ 空名单依然拒绝所有人(Fail-Closed),这是历史
「名单为空则人人可审」漏洞的修复点,不得改回 fail-open。
- 角色级放行必须配公司隔离:6 个主管里 IRIS 5 个、LICA 1 个,
不隔离就是跨公司审批通道。
- trans_return 加 source_ref(幂等锚点)。
迁移:db_migrations/phase12(建列)+ phase13(存量空分类回填为库存报废)。
This commit is contained in:
@ -115,9 +115,17 @@ class ScrapSourceAdapter:
|
||||
# --- 台账公共字段 ---
|
||||
@staticmethod
|
||||
def _ledger_kwargs(req, operator_name):
|
||||
"""写 TransScrap 的公共字段 —— **三种来源适配器全部经过这里**。
|
||||
|
||||
★ 这是报废原因分类落到台账的**唯一出口**,分类只在这里带一次,
|
||||
三个 adapter(库存行 / 在管不良品 / 借出未还)就都通了。
|
||||
「统计生产报废金额」按 trans_scrap.reason_category 分组,不要靠
|
||||
reason 自由文本去匹配。
|
||||
"""
|
||||
from app.models.scrap_approval import ScrapApproval
|
||||
return {
|
||||
'reason': req.remark or '',
|
||||
'reason_category': getattr(req, 'reason_category', None),
|
||||
'operator_name': operator_name,
|
||||
'approver_name': ScrapApproval._user_name(req.actual_approver_id),
|
||||
'approval_status': 'executed',
|
||||
@ -238,9 +246,39 @@ class DefectiveScrapAdapter(ScrapSourceAdapter):
|
||||
def cap(self, row):
|
||||
return float(getattr(row, 'remaining_qty', 0) or 0)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_location(goods):
|
||||
"""
|
||||
取「原库位 / 批次(序列号)」:台账快照优先,回查源库存行兜底。
|
||||
|
||||
★ 口径 = **原库位**(这批货最初在哪),与三张库存表来源一致。
|
||||
★ 原实现此处硬编码空串(注释「台账无库位字段」),导致申请单/审批单
|
||||
明细里凡是不良品来源的行,库位与批次恒显示 "-"。但台账自带
|
||||
source_table + stock_id 溯源指针,源行还在时本来取得到。
|
||||
★ 两级都落空(源行已被入库模块物理删除,且台账也无快照)时返回空串
|
||||
—— 不中断报废:实物已销毁,台账必须先记上,位置缺失是可接受的
|
||||
降级,记录丢失不是。
|
||||
★ 取值口径与 scrap.py 的 _from_stock 一致:成品表无 batch_number 列,
|
||||
回退到 serial_number。
|
||||
"""
|
||||
loc = getattr(goods, 'warehouse_location', '') or ''
|
||||
batch = getattr(goods, 'batch_number', '') or ''
|
||||
if loc and batch:
|
||||
return loc, batch
|
||||
|
||||
model = _stock_model_map().get(getattr(goods, 'source_table', ''))
|
||||
if model is not None and getattr(goods, 'stock_id', None):
|
||||
row = model.query.get(goods.stock_id)
|
||||
if row:
|
||||
loc = loc or getattr(row, 'warehouse_location', '') or ''
|
||||
batch = batch or (getattr(row, 'batch_number', '')
|
||||
or getattr(row, 'serial_number', '') or '')
|
||||
return loc, batch
|
||||
|
||||
def snapshot(self, row, qty, raw):
|
||||
# 物料名/规格取自台账自身的冗余快照,**不联表 MaterialBase** ——
|
||||
# 原库存行可能已被物理删除,联表会取到空值。
|
||||
location, batch_number = self._resolve_location(row)
|
||||
return {
|
||||
'source_table': self.source_table,
|
||||
'stock_id': row.id,
|
||||
@ -248,8 +286,8 @@ class DefectiveScrapAdapter(ScrapSourceAdapter):
|
||||
'sku': getattr(row, 'sku', '') or '',
|
||||
'name': getattr(row, 'material_name', '') or raw.get('name') or '',
|
||||
'spec_model': getattr(row, 'spec_model', '') or raw.get('spec_model') or '',
|
||||
'location': '', # 台账无库位字段
|
||||
'batch_number': '',
|
||||
'location': location,
|
||||
'batch_number': batch_number,
|
||||
'scrap_qty': qty,
|
||||
'available_at_apply': self.cap(row),
|
||||
'scrap_mode': self.scrap_mode,
|
||||
|
||||
Reference in New Issue
Block a user