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