505 lines
26 KiB
Python
505 lines
26 KiB
Python
from apps.wpm.models import BatchSt, BatchLog, Handoverb
|
|
import logging
|
|
import re
|
|
from apps.qm.models import Defect, FtestWork, FtestworkDefect
|
|
from apps.wpm.models import Mlogb, MlogbDefect, Mlog
|
|
from apps.mtm.models import Mgroup
|
|
from apps.inm.models import MIOItem
|
|
import decimal
|
|
from decimal import Decimal
|
|
from django.db.models import Sum
|
|
from datetime import datetime
|
|
from apps.wpm.services_2 import get_f_l_date
|
|
import json
|
|
from apps.utils.tools import MyJSONEncoder
|
|
myLogger = logging.getLogger("log")
|
|
|
|
# ==================== 大批直通良率统计 ====================
|
|
# 大批 = 拆出检验小批的那个批(由拆批交接关系确定, 不依赖批次号命名/工艺路线)
|
|
# 直通良率 = 大批下所有子批次外观检验直通合格数之和 / 大批白料数
|
|
# 白料数(入料数) = 沿拆合批关系(BatchLog)从大批向上回溯, 找到锚点工段产出批,
|
|
# 取其锚点工段领用数(即进炉前数量); 一条炉料链拆给多个大批时按生产主线流出数量占比分摊
|
|
# (汇入不良集中批的旁路不占份额), 占比截断为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):
|
|
try:
|
|
batchst = BatchSt.objects.get(batch=batch, version=1)
|
|
except BatchSt.DoesNotExist:
|
|
myLogger.error(f"Batch {batch} does not exist")
|
|
return
|
|
|
|
data = {"批次号": batch}
|
|
|
|
mgroup_qs = Mgroup.objects.all().order_by("sort")
|
|
for mgroup in mgroup_qs:
|
|
mgroup_name = mgroup.name
|
|
mlogb1_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,
|
|
material_out__isnull=False, mlog__mgroup=mgroup,
|
|
mlog__is_fix=False, batch=batch, need_inout=True)
|
|
if mlogb1_qs.exists():
|
|
data[f"{mgroup_name}_日期"] = []
|
|
data[f"{mgroup_name}_操作人"] = []
|
|
data[f"{mgroup_name}_班次"] = []
|
|
data[f"{mgroup_name}_count_use"] = 0
|
|
data[f"{mgroup_name}_count_real"] = 0
|
|
data[f"{mgroup_name}_count_ok"] = 0
|
|
data[f"{mgroup_name}_count_notok"] = 0
|
|
data[f"{mgroup_name}_count_ok_full"] = 0
|
|
data[f"{mgroup_name}_count_pn_jgqbl"] = 0
|
|
mlogb_q_ids = []
|
|
for item in mlogb1_qs:
|
|
# 找到对应的输入
|
|
mlogb_from:Mlogb = item.mlogb_from
|
|
if mlogb_from:
|
|
mlogb_q_ids.append(mlogb_from.id)
|
|
data[f"{mgroup_name}_count_use"] += mlogb_from.count_use
|
|
data[f"{mgroup_name}_count_pn_jgqbl"] += mlogb_from.count_pn_jgqbl
|
|
if item.mlog.handle_user:
|
|
data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user)
|
|
if item.mlog.handle_date:
|
|
data[f"{mgroup_name}_日期"].append(item.mlog.handle_date)
|
|
if item.mlog.shift:
|
|
data[f"{mgroup_name}_班次"].append(item.mlog.shift.name)
|
|
data[f"{mgroup_name}_count_real"] += item.count_real
|
|
data[f"{mgroup_name}_count_ok"] += item.count_ok
|
|
data[f"{mgroup_name}_count_ok_full"] += item.count_ok_full if item.count_ok_full else 0
|
|
data[f"{mgroup_name}_count_notok"] += item.count_notok if item.count_notok else 0
|
|
|
|
try:
|
|
data[f"{mgroup_name}_完全合格率"] = round((data[f"{mgroup_name}_count_ok_full"] / data[f"{mgroup_name}_count_real"])*100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data[f"{mgroup_name}_完全合格率"] = 0
|
|
|
|
try:
|
|
data[f"{mgroup_name}_合格率"] = round((data[f"{mgroup_name}_count_ok"] / data[f"{mgroup_name}_count_real"])*100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data[f"{mgroup_name}_合格率"] = 0
|
|
|
|
mlogbd1_qs = MlogbDefect.objects.filter(mlogb__in=mlogb1_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
|
mlogbd1_q_qs = MlogbDefect.objects.filter(mlogb__id__in=mlogb_q_ids, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
|
|
|
for item in mlogbd1_q_qs:
|
|
data[f"{mgroup_name}_加工前_缺陷_{item['defect__name']}"] = item["total"]
|
|
data[f"{mgroup_name}_加工前_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data[f"{mgroup_name}_count_use"])*100, 2)
|
|
|
|
for item in mlogbd1_qs:
|
|
data[f"{mgroup_name}_缺陷_{item['defect__name']}"] = item["total"]
|
|
data[f"{mgroup_name}_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data[f"{mgroup_name}_count_real"])*100, 2)
|
|
|
|
data[f"{mgroup_name}_日期"] = list(set(data[f"{mgroup_name}_日期"]))
|
|
data[f"{mgroup_name}_日期"].sort()
|
|
data[f"{mgroup_name}_小日期"] = min(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
|
data[f"{mgroup_name}_大日期"] = max(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
|
|
data[f"{mgroup_name}_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data[f"{mgroup_name}_日期"]])
|
|
data[f"{mgroup_name}_操作人"] = list(set(data[f"{mgroup_name}_操作人"]))
|
|
data[f"{mgroup_name}_操作人"] = ";".join([item.name for item in data[f"{mgroup_name}_操作人"]])
|
|
data[f"{mgroup_name}_班次"] = list(set(data[f"{mgroup_name}_班次"]))
|
|
data[f"{mgroup_name}_班次"] = ";".join([item for item in data[f"{mgroup_name}_班次"]])
|
|
|
|
|
|
# 按 mlog__submit_time, id 排序,每条 Mlogb 记录独立为一次返修
|
|
# (同一 Mlog 下有多条同批次 Mlogb 时也能正确拆分为多次返修)
|
|
_all_fix_qs = Mlogb.objects.filter(
|
|
mlog__submit_time__isnull=False,
|
|
material_out__isnull=False,
|
|
mlog__mgroup__name="外观检验",
|
|
mlog__is_fix=True,
|
|
batch=batch,
|
|
need_inout=True,
|
|
).order_by("mlog__submit_time", "id")
|
|
_fix_prefixes = []
|
|
for fix_idx, fix_mlogb in enumerate(_all_fix_qs):
|
|
suffix = "" if fix_idx == 0 else str(fix_idx + 1)
|
|
prefix = f"外观检验_返修{suffix}_"
|
|
_fix_prefixes.append(prefix)
|
|
mlog = fix_mlogb.mlog
|
|
handle_date = mlog.handle_date
|
|
data[f"{prefix}count_real"] = fix_mlogb.count_real
|
|
data[f"{prefix}count_ok"] = fix_mlogb.count_ok
|
|
data[f"{prefix}count_ok_full"] = fix_mlogb.count_ok_full or 0
|
|
data[f"{prefix}count_notok"] = fix_mlogb.count_notok or 0
|
|
try:
|
|
data[f"{prefix}合格率"] = round((fix_mlogb.count_ok / fix_mlogb.count_real) * 100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data[f"{prefix}合格率"] = 0
|
|
try:
|
|
data[f"{prefix}完全合格率"] = round(((fix_mlogb.count_ok_full or 0) / fix_mlogb.count_real) * 100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data[f"{prefix}完全合格率"] = 0
|
|
data[f"{prefix}日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else ""
|
|
data[f"{prefix}小日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else ""
|
|
data[f"{prefix}大日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else ""
|
|
data[f"{prefix}操作人"] = mlog.handle_user.name if mlog.handle_user else ""
|
|
data[f"{prefix}班次"] = mlog.shift.name if mlog.shift else ""
|
|
fix_defect_qs = MlogbDefect.objects.filter(mlogb=fix_mlogb, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
|
for item in fix_defect_qs:
|
|
data[f"{prefix}缺陷_{item['defect__name']}"] = item["total"]
|
|
data[f"{prefix}缺陷_{item['defect__name']}_比例"] = round((item["total"] / fix_mlogb.count_real) * 100, 2)
|
|
|
|
# 车间库存抽检
|
|
ft_qs = FtestWork.objects.filter(type2=FtestWork.TYPE2_SOME, wm__mgroup__name="外观检验", batch=batch, submit_time__isnull=False)
|
|
if ft_qs.exists():
|
|
data["外观检验_车间库存抽检_日期"] = []
|
|
data["外观检验_车间库存抽检_操作人"] = []
|
|
data["外观检验_车间库存抽检_count_notok"] = 0
|
|
for item in ft_qs:
|
|
if item.test_user:
|
|
data["外观检验_车间库存抽检_操作人"].append(item.test_user)
|
|
if item.test_date:
|
|
data["外观检验_车间库存抽检_日期"].append(item.test_date)
|
|
data["外观检验_车间库存抽检_count_notok"] += item.count_notok if item.count_notok else 0
|
|
|
|
data["外观检验_车间库存抽检_日期"] = list(set(data["外观检验_车间库存抽检_日期"]))
|
|
data["外观检验_车间库存抽检_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["外观检验_车间库存抽检_日期"]])
|
|
data["外观检验_车间库存抽检_操作人"] = list(set(data["外观检验_车间库存抽检_操作人"]))
|
|
data["外观检验_车间库存抽检_操作人"] = ";".join([item.name for item in data["外观检验_车间库存抽检_操作人"]])
|
|
|
|
# 车间库存抽检缺陷
|
|
ftd_qs = FtestworkDefect.objects.filter(ftestwork__in=ft_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
|
for item in ftd_qs:
|
|
data[f"外观检验_车间库存抽检_缺陷_{item['defect__name']}"] = item["total"]
|
|
|
|
if "外观检验_count_ok" in data:
|
|
data["外观检验_总合格数"] = data["外观检验_count_ok"] + sum(data.get(f"{p}count_ok", 0) for p in _fix_prefixes)
|
|
try:
|
|
data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data["外观检验_总合格率"] = 0
|
|
|
|
data["外观检验_完全总合格数"] = data["外观检验_count_ok_full"] + sum(data.get(f"{p}count_ok_full", 0) for p in _fix_prefixes)
|
|
try:
|
|
data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data["外观检验_完全总合格率"] = 0
|
|
|
|
data["外观检验_直通合格数"] = data["外观检验_总合格数"] - data.get("外观检验_车间库存抽检_count_notok", 0)
|
|
if "尺寸检验_合格率" in data:
|
|
try:
|
|
data["外观检验_直通合格率"] = round((data["外观检验_总合格率"]* data["尺寸检验_合格率"])/100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data["外观检验_直通合格率"] = 0
|
|
|
|
try:
|
|
data["外观检验_直通合格率2"] = round((data["外观检验_直通合格数"]/data["尺寸检验_count_use"])*100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data["外观检验_直通合格率2"] = 0
|
|
|
|
if "尺寸检验_完全合格率" in data:
|
|
try:
|
|
data["外观检验_完全直通合格率"] = round((data["外观检验_完全总合格率"]* data["尺寸检验_完全合格率"])/100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data["外观检验_完全直通合格率"] = 0
|
|
|
|
res = get_f_l_date(data)
|
|
|
|
|
|
batchst.data = json.loads(json.dumps(data, cls=MyJSONEncoder))
|
|
if batchst.first_time is None or (res["first_time"] and res["first_time"] < batchst.first_time):
|
|
batchst.first_time = res["first_time"]
|
|
if batchst.last_time is None or (res["last_time"] and res["last_time"] > batchst.last_time):
|
|
batchst.last_time = res["last_time"]
|
|
batchst.save()
|
|
|
|
try:
|
|
handle_zt(batchst, data)
|
|
except Exception:
|
|
myLogger.exception(f"直通统计-{batch}计算失败")
|
|
|
|
|
|
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", "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
|
|
if big_batch:
|
|
cal_zt_big(big_batch)
|
|
|
|
|
|
def _zt_split_parents(batchst: BatchSt):
|
|
"""经拆批交接产生该批的来源批次号集合"""
|
|
qs = BatchLog.objects.filter(target=batchst, relation_type="split", handover__isnull=False).select_related("source")
|
|
return {e.source.batch for e in qs}
|
|
|
|
|
|
def resolve_zt_big(batchst: BatchSt, data: dict):
|
|
"""确定检验小批所属的大批号, 无法确定返回 None"""
|
|
if batchst.zt_batch:
|
|
return batchst.zt_batch
|
|
if not any(k in data for k in ("尺寸检验_count_real", "外观检验_count_real")):
|
|
return None
|
|
parents = _zt_split_parents(batchst)
|
|
if len(parents) == 1:
|
|
return parents.pop()
|
|
if len(parents) > 1:
|
|
myLogger.error(f"直通统计-{batchst.batch}存在多个拆批来源{parents}, 无法归属大批")
|
|
return None
|
|
# 合批复检批(如 大批-A-1): 所有合批来源须归属同一大批
|
|
merge_srcs = [e.source for e in BatchLog.objects.filter(target=batchst, relation_type="merge").select_related("source")]
|
|
if merge_srcs:
|
|
bigs = set()
|
|
for s in merge_srcs:
|
|
if s.zt_batch:
|
|
bigs.add(s.zt_batch)
|
|
else:
|
|
ps = _zt_split_parents(s)
|
|
bigs.add(ps.pop() if len(ps) == 1 else None)
|
|
if len(bigs) == 1 and None not in bigs:
|
|
return bigs.pop()
|
|
myLogger.error(f"直通统计-{batchst.batch}合批来源大批不一致{bigs}, 无法归属")
|
|
return None
|
|
# 无任何拆合批上游: 自身即大批(未拆批直接检验)
|
|
return batchst.batch
|
|
|
|
|
|
def _zt_edge_qty(edge: BatchLog, target_batch: str):
|
|
"""该条拆合批边流入 target 的数量"""
|
|
if edge.handover_id:
|
|
# 拆批明细行记目标批, 合批明细行记来源批
|
|
hb_batch = target_batch if edge.relation_type == "split" else edge.source.batch
|
|
qty = Handoverb.objects.filter(handover_id=edge.handover_id, batch=hb_batch).aggregate(t=Sum("count"))["t"]
|
|
if qty:
|
|
return qty
|
|
return edge.handover.count # 无明细行时兜底整单数量
|
|
if edge.mlog_id:
|
|
# 报工改号: 目标批产出行对应的领用数
|
|
row = Mlogb.objects.filter(mlog_id=edge.mlog_id, batch=target_batch,
|
|
material_out__isnull=False).select_related("mlogb_from").first()
|
|
if row:
|
|
return row.mlogb_from.count_use if row.mlogb_from else row.count_real
|
|
return None
|
|
|
|
|
|
def _zt_source_out_ratio(edge: BatchLog, qty):
|
|
"""qty 占来源批生产主线流出量的比例(白料分摊系数)
|
|
|
|
生产主线流出 = 报工改号边 / 拆批边 / 汇入大批格式批号的合批边(即当前回溯所走的这类边);
|
|
汇入不良集中批(如 黑检片-00)等旁路的合批边不占白料份额 —— 炉后不良对应的白料
|
|
应计入生产主线大批的分母, 直通良率才能体现炉段损耗。
|
|
合并回自身的整理性合批边(source==target)不算流出。
|
|
只有一条生产主线流出边时全额传递(ratio=1); 多条(如炉料链拆给多个大批)按数量占比分摊。
|
|
"""
|
|
out_edges = [e for e in BatchLog.objects.filter(source=edge.source).select_related("target", "handover")
|
|
if e.target.batch != e.source.batch]
|
|
prod_edges = [e for e in out_edges
|
|
if e.mlog_id or e.relation_type == "split" or ZT_BIG_BATCH_RE.match(e.target.batch) or e.id == edge.id]
|
|
if len(prod_edges) <= 1:
|
|
return Decimal(1)
|
|
total = Decimal(0)
|
|
for e in prod_edges:
|
|
e_qty = _zt_edge_qty(e, e.target.batch)
|
|
if e_qty:
|
|
total += Decimal(e_qty)
|
|
else:
|
|
myLogger.error(f"直通统计-{e.source.batch}->{e.target.batch}流出边数量缺失, 分摊占比可能偏大")
|
|
if not total:
|
|
return Decimal(1)
|
|
return min(Decimal(qty) / total, Decimal(1))
|
|
|
|
|
|
def _zt_white_count(batchst: BatchSt, depth=0, path=frozenset(), memo=None):
|
|
"""回溯计算该批对应的白料数(进炉前数量)
|
|
|
|
返回 (白料数, 口径set); 不可得时白料数为 None。
|
|
分摊: 来源批白料 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, {"超出回溯深度"}, False
|
|
memo["visits"] += 1
|
|
if memo["visits"] > ZT_MAX_VISITS:
|
|
return None, {"回溯规模超限"}, False
|
|
path = path | {batchst.batch}
|
|
|
|
# 锚点: 该批有锚点工段产出报工, 白料数=锚点工段领用数
|
|
anchor_qs = Mlogb.objects.filter(
|
|
batch=batchst.batch, material_out__isnull=False, need_inout=True,
|
|
mlog__mgroup__name__in=ZT_ANCHOR_MGROUPS, mlog__submit_time__isnull=False,
|
|
mlog__is_fix=False).select_related("mlogb_from", "mlog__mgroup")
|
|
white = Decimal(0)
|
|
kinds = set()
|
|
found_anchor = False
|
|
for row in anchor_qs:
|
|
found_anchor = True
|
|
white += row.mlogb_from.count_use if row.mlogb_from else row.count_real
|
|
kinds.add(f"{row.mlog.mgroup.name}领用")
|
|
if found_anchor:
|
|
memo["res"][batchst.batch] = (white, kinds, True)
|
|
return white, kinds, True
|
|
|
|
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:
|
|
# 无上游谱系: 兜底汇总原料性入库数量(采购/其他入库, 排除生产入库)
|
|
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, memo)
|
|
if legacy is not None:
|
|
return legacy
|
|
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}边数量缺失"}, 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, False
|
|
clean = clean and p_clean
|
|
white_total += Decimal(p_white) * _zt_source_out_ratio(edge, qty)
|
|
kinds |= p_kinds
|
|
if clean:
|
|
memo["res"][batchst.batch] = (white_total, kinds, True)
|
|
return white_total, kinds, clean
|
|
|
|
|
|
def _zt_white_legacy(batchst: BatchSt, depth, path, memo):
|
|
"""BatchLog 上线前的老批次没有拆合批边, 根据 BatchSt 记录的创建来源续上回溯
|
|
|
|
可解时返回 (白料数, 口径set, 可缓存标记), 否则返回 None。
|
|
"""
|
|
src_batch = None
|
|
qty = None
|
|
ratio = Decimal(1)
|
|
if batchst.handover_id and batchst.handover.wm_id:
|
|
# 拆批交接创建: 来源为交接的车间库存批, 按同单各行数量占比分摊
|
|
src_batch = batchst.handover.wm.batch
|
|
own = Handoverb.objects.filter(handover_id=batchst.handover_id, batch=batchst.batch).aggregate(t=Sum("count"))["t"]
|
|
total = Handoverb.objects.filter(handover_id=batchst.handover_id).aggregate(t=Sum("count"))["t"]
|
|
qty = own or batchst.handover.count
|
|
if own and total and total > own:
|
|
ratio = Decimal(own) / Decimal(total)
|
|
elif batchst.mlog_id:
|
|
# 报工改号创建: 来源为报工的投入批
|
|
row = Mlogb.objects.filter(mlog_id=batchst.mlog_id, batch=batchst.batch,
|
|
material_out__isnull=False).select_related("mlogb_from").first()
|
|
if row and row.mlogb_from:
|
|
src_batch = row.mlogb_from.batch
|
|
qty = row.mlogb_from.count_use
|
|
if not src_batch or src_batch in path or not qty:
|
|
return None
|
|
try:
|
|
src_bs = BatchSt.objects.get(batch=src_batch, version=1)
|
|
except BatchSt.DoesNotExist:
|
|
return None
|
|
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 | {"谱系兜底"}, p_clean
|
|
|
|
|
|
def cal_zt_big(big_batch: str):
|
|
"""重算大批的直通良率并写入其 BatchSt.data"""
|
|
try:
|
|
big_bs = BatchSt.objects.get(batch=big_batch, version=1)
|
|
except BatchSt.DoesNotExist:
|
|
myLogger.error(f"直通统计-大批{big_batch}不存在")
|
|
return
|
|
if big_bs.zt_batch != big_batch:
|
|
big_bs.zt_batch = big_batch
|
|
big_bs.save(update_fields=["zt_batch", "update_time"])
|
|
|
|
# 从大批侧发现子批: 拆批目标 + 完全由本大批子批合并出的复检批(如 大批-A-1), 支持多级
|
|
known = {big_batch}
|
|
sub_map = {}
|
|
frontier = [big_bs]
|
|
for _ in range(3):
|
|
new_nodes = []
|
|
for node in frontier:
|
|
for e in BatchLog.objects.filter(source=node).select_related("target"):
|
|
t = e.target
|
|
if t.batch in known:
|
|
continue
|
|
if e.relation_type == "split" and e.handover_id:
|
|
ok = True
|
|
elif e.relation_type == "merge":
|
|
m_srcs = BatchLog.objects.filter(target=t, relation_type="merge").select_related("source")
|
|
ok = all(s.source.batch in known or s.source.zt_batch == big_batch for s in m_srcs)
|
|
else:
|
|
ok = False
|
|
if ok:
|
|
known.add(t.batch)
|
|
sub_map[t.batch] = t
|
|
new_nodes.append(t)
|
|
frontier = new_nodes
|
|
if not frontier:
|
|
break
|
|
# 锁定归属
|
|
for b, t in sub_map.items():
|
|
if t.zt_batch and t.zt_batch != big_batch:
|
|
myLogger.error(f"直通统计-{b}已归属{t.zt_batch}, 与{big_batch}冲突, 跳过")
|
|
continue
|
|
if t.zt_batch != big_batch:
|
|
t.zt_batch = big_batch
|
|
t.save(update_fields=["zt_batch", "update_time"])
|
|
|
|
# 分子: 所有归属本大批的批次(检验小批/复检批)外观检验直通合格数之和
|
|
count_zt = 0
|
|
sub_batches = []
|
|
for sub in BatchSt.objects.filter(zt_batch=big_batch, version=1).exclude(batch=big_batch):
|
|
sub_batches.append(sub.batch)
|
|
count_zt += (sub.data or {}).get("外观检验_直通合格数", 0) or 0
|
|
# 大批未拆批直接检验时自身也计入
|
|
count_zt += (big_bs.data or {}).get("外观检验_直通合格数", 0) or 0
|
|
|
|
# 分母: 白料数(大批成型后基本不变, 已算过的直接复用, 含算不出的负缓存;
|
|
# 大批自身重算时 main 会重建 data, 缓存自然失效并重新回溯)
|
|
data = big_bs.data or {}
|
|
if "直通_白料数" in data:
|
|
white = data.get("直通_白料数")
|
|
white = Decimal(str(white)) if white else None
|
|
kinds = set((data.get("直通_口径") or "").split(";")) - {"", "谱系异常(合格数大于白料数)"}
|
|
else:
|
|
white, kinds = _zt_white_count(big_bs)
|
|
if not white:
|
|
myLogger.error(f"直通统计-{big_batch}白料数不可得: {kinds}")
|
|
|
|
data["直通_子批次"] = ";".join(sorted(sub_batches))
|
|
data["直通_总合格数"] = count_zt
|
|
data["直通_口径"] = ";".join(sorted(kinds))
|
|
if white and Decimal(str(count_zt)) > white:
|
|
# 分子超过分母说明谱系/分摊异常(如白料池循环整理导致回溯不全), 隔离不出良率
|
|
myLogger.error(f"直通统计-{big_batch}总合格数{count_zt}大于白料数{white}, 谱系或分摊异常")
|
|
data["直通_白料数"] = round(white, 1)
|
|
data["直通_良率"] = None
|
|
data["直通_口径"] = ";".join(sorted(kinds | {"谱系异常(合格数大于白料数)"}))
|
|
elif white:
|
|
data["直通_白料数"] = round(white, 1)
|
|
try:
|
|
data["直通_良率"] = round((Decimal(str(count_zt)) / white) * 100, 2)
|
|
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
data["直通_良率"] = 0
|
|
else:
|
|
data["直通_白料数"] = None
|
|
data["直通_良率"] = None
|
|
big_bs.data = json.loads(json.dumps(data, cls=MyJSONEncoder))
|
|
big_bs.save(update_fields=["data", "update_time"])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
pass |