factory/apps/wpm/scripts/batch_gxerp.py

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 = 300 # 单个大批白料回溯的节点访问上限(防病态网状谱系组合爆炸; 实测正常批1~10次, 最重合法案例77次)
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