fix:白料回溯记忆化+访问上限, 入库兜底汇总原料性入库
- 回溯按批次号记忆化, 修复遗留无损批网状谱系导致的指数爆炸卡死 - 单次回溯超3000节点放弃并记口径"回溯规模超限" - 入库兜底改为汇总pur_in/other_in全部入库明细, 修复多次入库低估 - save(update_fields)补update_time使auto_now生效 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fb5f1d072d
commit
d1c7ffb598
|
|
@ -241,8 +241,9 @@ FtestWork where type2 = TYPE2_SOME
|
|||
- **锚点**:批次存在锚点工段(`ZT_ANCHOR_MGROUPS=["黑化"]`)产出报工时停止,白料数 = 该批锚点工段 `mlogb_from.count_use`(进炉前领用数)。多次黑化的链取离大批最近的一次。
|
||||
- **分摊**(`_zt_source_out_ratio`):来源批有多条**生产主线**流出边(报工改号/拆批/汇入大批格式 `ZT_BIG_BATCH_RE` 的合批)时,按各去向数量占比分摊;汇入不良集中批(如 `黑检片-00`)的旁路不占白料份额,使炉后不良计入生产主线分母。单一去向全额传递。
|
||||
- **边数量**:拆批取 `Handoverb(batch=目标批)`,合批取 `Handoverb(batch=来源批)`,报工改号取目标产出行的 `mlogb_from.count_use`;无明细时兜底整单数量。
|
||||
- **外购兜底**:无上游谱系且由入库创建 → 白料数 = 入库数量(`MIOItem.count`),口径记 `入库数量`。
|
||||
- **老数据兜底**(`_zt_white_legacy`):BatchLog 上线前的批次无边,沿 `BatchSt.handover/mlog` 创建来源续上回溯,口径记 `谱系兜底`。
|
||||
- **外购/遗留兜底**:无上游谱系 → 白料数 = 该批原料性入库数量之和(`MIOItem`,仅 `pur_in`/`other_in`,排除生产入库),口径记 `入库数量`。
|
||||
- **老数据兜底**(`_zt_white_legacy`):BatchLog 上线前的批次无边且无入库,沿 `BatchSt.handover/mlog` 创建来源续上回溯,口径记 `谱系兜底`。
|
||||
- **性能护栏**:回溯按批次号记忆化(网状/菱形谱系线性复杂度,防环截断过的节点不入缓存);单次回溯节点访问超 `ZT_MAX_VISITS=3000` 时放弃,口径记 `回溯规模超限`(负缓存,不重复付代价)。
|
||||
|
||||
### 缓存与隔离
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ myLogger = logging.getLogger("log")
|
|||
# (汇入不良集中批的旁路不占份额), 占比截断为1; 无锚点谱系(如外购半成品)时兜底用入库数量。
|
||||
ZT_ANCHOR_MGROUPS = ["黑化"] # 入料数锚点工段
|
||||
ZT_MAX_DEPTH = 12 # 白料回溯最大层数
|
||||
ZT_MAX_VISITS = 3000 # 单个大批白料回溯的节点访问上限(防病态网状谱系组合爆炸)
|
||||
ZT_BIG_BATCH_RE = re.compile(r"^\d{4}-[0-9A-Za-z.]+-\d+") # 大批批号格式(合批改名产生), 用于区分生产分流与不良汇集
|
||||
|
||||
def main(batch: str, mgroup_obj):
|
||||
|
|
@ -214,7 +215,7 @@ def handle_zt(batchst: BatchSt, data: dict):
|
|||
big_batch = resolve_zt_big(batchst, data)
|
||||
if big_batch and batchst.zt_batch != big_batch:
|
||||
batchst.zt_batch = big_batch
|
||||
batchst.save(update_fields=["zt_batch"])
|
||||
batchst.save(update_fields=["zt_batch", "update_time"])
|
||||
if big_batch is None and BatchSt.objects.filter(zt_batch=batchst.batch, version=1).exclude(id=batchst.id).exists():
|
||||
# 当前批自身是大批(已有子批归属于它), 其数据变化(如白料数)也需重算
|
||||
big_batch = batchst.batch
|
||||
|
|
@ -303,14 +304,28 @@ def _zt_source_out_ratio(edge: BatchLog, qty):
|
|||
return min(Decimal(qty) / total, Decimal(1))
|
||||
|
||||
|
||||
def _zt_white_count(batchst: BatchSt, depth=0, path=frozenset()):
|
||||
def _zt_white_count(batchst: BatchSt, depth=0, path=frozenset(), memo=None):
|
||||
"""回溯计算该批对应的白料数(进炉前数量)
|
||||
|
||||
返回 (白料数, 口径set); 不可得时白料数为 None。
|
||||
分摊: 来源批白料 p_white 按 qty 占其全部流出量的比例分给当前批。
|
||||
分摊: 来源批白料 p_white 按 qty 占其生产主线流出量的比例分给当前批。
|
||||
memo 按批次号缓存本次回溯的中间结果, 网状/菱形谱系保持线性复杂度;
|
||||
经防环截断的节点结果与路径相关, 不入缓存。visits 超限直接放弃(隔离病态谱系)。
|
||||
"""
|
||||
white, kinds, _clean = _zt_white_walk(batchst, depth, path,
|
||||
memo if memo is not None else {"visits": 0, "res": {}})
|
||||
return white, kinds
|
||||
|
||||
|
||||
def _zt_white_walk(batchst: BatchSt, depth, path, memo):
|
||||
"""返回 (白料数, 口径set, 结果是否未经防环截断可缓存)"""
|
||||
if batchst.batch in memo["res"]:
|
||||
return memo["res"][batchst.batch]
|
||||
if depth > ZT_MAX_DEPTH:
|
||||
return None, {"超出回溯深度"}
|
||||
return None, {"超出回溯深度"}, False
|
||||
memo["visits"] += 1
|
||||
if memo["visits"] > ZT_MAX_VISITS:
|
||||
return None, {"回溯规模超限"}, False
|
||||
path = path | {batchst.batch}
|
||||
|
||||
# 锚点: 该批有锚点工段产出报工, 白料数=锚点工段领用数
|
||||
|
|
@ -326,40 +341,47 @@ def _zt_white_count(batchst: BatchSt, depth=0, path=frozenset()):
|
|||
white += row.mlogb_from.count_use if row.mlogb_from else row.count_real
|
||||
kinds.add(f"{row.mlog.mgroup.name}领用")
|
||||
if found_anchor:
|
||||
return white, kinds
|
||||
memo["res"][batchst.batch] = (white, kinds, True)
|
||||
return white, kinds, True
|
||||
|
||||
edges = [e for e in BatchLog.objects.filter(target=batchst).select_related("source", "handover")
|
||||
if e.source.batch not in path] # 跳过自环/已访问节点(同名重复改号、合回上游等)
|
||||
all_edges = list(BatchLog.objects.filter(target=batchst).select_related("source", "handover"))
|
||||
edges = [e for e in all_edges if e.source.batch not in path] # 跳过自环/已访问节点(同名重复改号、合回上游等)
|
||||
clean = len(edges) == len(all_edges)
|
||||
if not edges:
|
||||
# 无上游谱系: 入库创建的批(如外购半成品)兜底用入库数量
|
||||
mioitem = batchst.mioitem
|
||||
if mioitem is None and batchst.mio_id:
|
||||
mioitem = MIOItem.objects.filter(mio_id=batchst.mio_id, batch=batchst.batch).first()
|
||||
if mioitem and mioitem.count:
|
||||
return mioitem.count, {"入库数量"}
|
||||
# 无上游谱系: 兜底汇总原料性入库数量(采购/其他入库, 排除生产入库)
|
||||
mio_total = MIOItem.objects.filter(
|
||||
batch=batchst.batch, mio__submit_time__isnull=False,
|
||||
mio__type__in=["pur_in", "other_in"]).aggregate(t=Sum("count"))["t"]
|
||||
if mio_total:
|
||||
if clean:
|
||||
memo["res"][batchst.batch] = (mio_total, {"入库数量"}, True)
|
||||
return mio_total, {"入库数量"}, clean
|
||||
# BatchLog 上线前的老批次没有边, 但 BatchSt 记录了创建它的交接/报工, 由此续上回溯
|
||||
legacy = _zt_white_legacy(batchst, depth, path)
|
||||
legacy = _zt_white_legacy(batchst, depth, path, memo)
|
||||
if legacy is not None:
|
||||
return legacy
|
||||
return None, {f"{batchst.batch}无上游谱系且无入库数量"}
|
||||
return None, {f"{batchst.batch}无上游谱系且无入库数量"}, False
|
||||
|
||||
white_total = Decimal(0)
|
||||
for edge in edges:
|
||||
qty = _zt_edge_qty(edge, batchst.batch)
|
||||
if not qty:
|
||||
return None, {f"{edge.source.batch}->{batchst.batch}边数量缺失"}
|
||||
p_white, p_kinds = _zt_white_count(edge.source, depth + 1, path)
|
||||
return None, {f"{edge.source.batch}->{batchst.batch}边数量缺失"}, False
|
||||
p_white, p_kinds, p_clean = _zt_white_walk(edge.source, depth + 1, path, memo)
|
||||
if p_white is None:
|
||||
return None, p_kinds
|
||||
return None, p_kinds, False
|
||||
clean = clean and p_clean
|
||||
white_total += Decimal(p_white) * _zt_source_out_ratio(edge, qty)
|
||||
kinds |= p_kinds
|
||||
return white_total, kinds
|
||||
if clean:
|
||||
memo["res"][batchst.batch] = (white_total, kinds, True)
|
||||
return white_total, kinds, clean
|
||||
|
||||
|
||||
def _zt_white_legacy(batchst: BatchSt, depth, path):
|
||||
def _zt_white_legacy(batchst: BatchSt, depth, path, memo):
|
||||
"""BatchLog 上线前的老批次没有拆合批边, 根据 BatchSt 记录的创建来源续上回溯
|
||||
|
||||
可解时返回 (白料数, 口径set), 否则返回 None。
|
||||
可解时返回 (白料数, 口径set, 可缓存标记), 否则返回 None。
|
||||
"""
|
||||
src_batch = None
|
||||
qty = None
|
||||
|
|
@ -385,10 +407,10 @@ def _zt_white_legacy(batchst: BatchSt, depth, path):
|
|||
src_bs = BatchSt.objects.get(batch=src_batch, version=1)
|
||||
except BatchSt.DoesNotExist:
|
||||
return None
|
||||
p_white, p_kinds = _zt_white_count(src_bs, depth + 1, path)
|
||||
p_white, p_kinds, p_clean = _zt_white_walk(src_bs, depth + 1, path, memo)
|
||||
if p_white is None:
|
||||
return None
|
||||
return p_white * ratio, p_kinds | {"谱系兜底"}
|
||||
return p_white * ratio, p_kinds | {"谱系兜底"}, p_clean
|
||||
|
||||
|
||||
def cal_zt_big(big_batch: str):
|
||||
|
|
@ -400,7 +422,7 @@ def cal_zt_big(big_batch: str):
|
|||
return
|
||||
if big_bs.zt_batch != big_batch:
|
||||
big_bs.zt_batch = big_batch
|
||||
big_bs.save(update_fields=["zt_batch"])
|
||||
big_bs.save(update_fields=["zt_batch", "update_time"])
|
||||
|
||||
# 从大批侧发现子批: 拆批目标 + 完全由本大批子批合并出的复检批(如 大批-A-1), 支持多级
|
||||
known = {big_batch}
|
||||
|
|
@ -434,7 +456,7 @@ def cal_zt_big(big_batch: str):
|
|||
continue
|
||||
if t.zt_batch != big_batch:
|
||||
t.zt_batch = big_batch
|
||||
t.save(update_fields=["zt_batch"])
|
||||
t.save(update_fields=["zt_batch", "update_time"])
|
||||
|
||||
# 分子: 所有归属本大批的批次(检验小批/复检批)外观检验直通合格数之和
|
||||
count_zt = 0
|
||||
|
|
@ -476,7 +498,7 @@ def cal_zt_big(big_batch: str):
|
|||
data["直通_白料数"] = None
|
||||
data["直通_良率"] = None
|
||||
big_bs.data = json.loads(json.dumps(data, cls=MyJSONEncoder))
|
||||
big_bs.save(update_fields=["data"])
|
||||
big_bs.save(update_fields=["data", "update_time"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def inherit_zt_batch(source: BatchSt, target: BatchSt):
|
|||
myLogger.error(f"直通统计-{target.batch}已归属{target.zt_batch}, 与来源{source.batch}的{source.zt_batch}冲突, 不覆盖")
|
||||
return
|
||||
target.zt_batch = source.zt_batch
|
||||
target.save(update_fields=["zt_batch"])
|
||||
target.save(update_fields=["zt_batch", "update_time"])
|
||||
|
||||
def check_wpr_number(number: str):
|
||||
return (len(number) >= 5 and
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import json
|
|||
from apps.utils.tools import MyJSONEncoder
|
||||
|
||||
|
||||
def main(since: str = "2025-06-17", fresh: bool = False):
|
||||
def main(since: str = "2026-06-01", fresh: bool = False):
|
||||
qs = BatchSt.objects.filter(
|
||||
create_time__gte=datetime.strptime(since, "%Y-%m-%d"), version=1
|
||||
).filter(
|
||||
|
|
@ -42,7 +42,7 @@ def main(since: str = "2025-06-17", fresh: bool = False):
|
|||
big = resolve_zt_big(bs, bs.data or {})
|
||||
if big and bs.zt_batch != big:
|
||||
bs.zt_batch = big
|
||||
bs.save(update_fields=["zt_batch"])
|
||||
bs.save(update_fields=["zt_batch", "update_time"])
|
||||
except Exception as e:
|
||||
print(f"[{ind + 1}/{total}] {bs.batch} 归属解析失败: {e}")
|
||||
continue
|
||||
|
|
@ -61,7 +61,7 @@ def main(since: str = "2025-06-17", fresh: bool = False):
|
|||
data = big_bs.data
|
||||
data.pop("直通_白料数", None)
|
||||
big_bs.data = json.loads(json.dumps(data, cls=MyJSONEncoder))
|
||||
big_bs.save(update_fields=["data"])
|
||||
big_bs.save(update_fields=["data", "update_time"])
|
||||
cal_zt_big(big)
|
||||
except Exception as e:
|
||||
print(f"{big} 计算失败: {e}")
|
||||
|
|
|
|||
Loading…
Reference in New Issue