import datetime

from django.core.cache import cache
from django.db.models import Sum
from django.utils import timezone
from typing import Union

from rest_framework.exceptions import ParseError

from apps.system.models import User
from apps.pm.models import Mtask
from apps.mtm.models import Mgroup, Shift, Material, Route, RoutePack, Team, Srule

from .models import SfLog, WMaterial, Mlog, Mlogb, Mlogbw, Handover, Handoverb, Handoverbw, MlogbDefect, BatchLog, BatchSt, MlogUser
from apps.wf.models import Ticket
from apps.wf.services import WfService
import logging
from apps.wpm.services_2 import ana_batch_thread, ana_wpr_thread
from datetime import timedelta
from apps.wpmw.models import Wpr, WprDefect
from ..qm.models import Defect, Ftest
from django.db.models import Count, Q
from apps.utils.tasks import ctask_run
from apps.mtm.models import Process, WmScope
from django.db.models import F

myLogger = logging.getLogger('log')

def inherit_zt_batch(source: BatchSt, target: BatchSt):
    """拆批/报工改号时目标批继承来源批的直通统计大批归属(纯继承, 不做判定)

    仅用于来源料子集式的流转(拆批/改号); 合批不继承——不良集中批等共享池
    会汇入多个大批的料, 继承会造成错误归属。归属的判定与合批复检批(如 大批-A-1)
    的归属由批次统计脚本(如 batch_gxerp)负责。
    """
    if source.zt_batch and target.zt_batch != source.zt_batch:
        if target.zt_batch:
            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", "update_time"])

def check_wpr_number(number: str):
    return (len(number) >= 5 and 
            (number[-4:].isdigit() or number[-2] == "-" or number[-3] == "-") and 
            not any(char.islower() for char in number))
    
def generate_new_batch(old_batch: str, mlog: Mlog):
    new_batch = old_batch
    supplier = mlog.supplier
    mgroup = mlog.mgroup
    process = mgroup.process
    if mgroup.batch_append_code:
        if mgroup.code:
            new_batch = f'{new_batch}>{mgroup.code}'
    if mlog.mtype == Mlog.MTYPE_OUT:
        supplier_number = supplier.number if supplier else ''
        if supplier_number:
            new_batch = f'{new_batch}>{supplier_number}'
    elif process.batch_append_equip:
        number = mlog.equipment.number if mlog.equipment else ''
        if number:
            new_batch = f'{new_batch}>{number}'
        if mlog.index:
            new_batch = f'{new_batch}>{mlog.index}'
    return new_batch

def find_material_can_change(material: Material, mgroup_to: Mgroup):
    """
    找到可转变为的物料(返工交接用)
    """
    routepackIds = Route.objects.filter(material_out=material, routepack__state=RoutePack.RP_S_CONFIRM).values_list('routepack', flat=True)
    matIds = list(Route.objects.filter(routepack__id__in=routepackIds, process=mgroup_to.process, material_in__type__in=[Material.MA_TYPE_HALFGOOD, Material.MA_TYPE_GOOD]).values_list('material_in', flat=True).distinct())
    if matIds and len(matIds) == 1:
        return Material.objects.get(id=matIds[0])
    raise ParseError("无法返工到该工段")

def get_sflog(mgroup: Mgroup, happen_time: datetime):
    sflog = SfLog.objects.filter(
        start_time__lte=happen_time, end_time__gt=happen_time, mgroup=mgroup).order_by('-start_time').first()
    if sflog is None:  # 需要创建值班记录
        make_sflogs(mgroup=mgroup, start_date=(
            happen_time-datetime.timedelta(days=2)).date(), end_date=(happen_time+datetime.timedelta(days=1)).date())
        sflog = SfLog.objects.filter(
            start_time__lte=happen_time, end_time__gt=happen_time, mgroup=mgroup).order_by('-start_time').first()
    return sflog

def get_team_x(sflog: SfLog):
    srule = Srule.objects.filter(belong_dept=sflog.mgroup.belong_dept).first()
    if srule and srule.rule:
        rule = srule.rule
        rule_compare = rule + rule
        teamId_list = (SfLog.objects.filter(mgroup=sflog.mgroup, start_time__lt=sflog.start_time, start_time__gte=sflog.start_time - timedelta(days=5)).order_by('-start_time').values_list('team_id', flat=True)[:3])[::-1]
        if None in teamId_list:
            pass
        else:
            teamId_list_len = len(teamId_list)
            for i in range(len(rule_compare)-teamId_list_len+1):
                if rule_compare[i:i+teamId_list_len] == teamId_list:
                    teamId = rule_compare[i+teamId_list_len]
                    team = Team.objects.filter(id=teamId).first()
                    return team
    return None


def make_sflogs(mgroup: Mgroup, start_date: datetime.date, end_date: datetime.date, create_by=None):
    shift_rule = mgroup.shift_rule
    shifts = Shift.objects.filter(rule=shift_rule).order_by('sort')  # 根据排班规则制定排班记录
    current_date = start_date
    while current_date <= end_date:
        for shift in shifts:
            start_time_o = shift.start_time_o
            end_time_o = shift.end_time_o
            start_time = datetime.datetime.combine(current_date, start_time_o)
            end_time = datetime.datetime.combine(current_date, end_time_o)
            # 以下代码是解决跨天排班时生成当天班次缺少的bug
            if start_time >= end_time:
                end_time += datetime.timedelta(days=1)
            total_sec = (end_time - start_time).total_seconds()

            # 创建SfLog记录
            sflog, is_created = SfLog.objects.get_or_create(mgroup=mgroup, shift=shift, start_time=start_time, defaults={
                "mgroup": mgroup,
                "shift": shift,
                "work_date": start_time.date(),
                "start_time": start_time,
                "end_time": end_time,
                "total_sec_now": total_sec,
                "total_sec": total_sec,
                "create_by": create_by
            })
            if is_created:
                team = get_team_x(sflog)
                if team:
                    sflog.team = team
                    sflog.leader = team.leader
                    sflog.save()
        current_date = current_date + datetime.timedelta(days=1)


def get_pcoal_heat(year_s: int, month_s: int, day_s: int):
    """
    获取煤粉热值
    只有回转窑需要录入煤粉热值
    """
    key = f'pcoal_heat_{year_s}_{month_s}_{day_s}'
    pcoal_heat = cache.get(key)
    if pcoal_heat is not None and pcoal_heat > 0:
        return pcoal_heat
    else:
        try:
            qs = SfLog.objects.get(work_date__year=year_s, work_date__month=month_s, work_date__day=day_s,
                                   mgroup__name='回转窑', shift__name__in=['白班', '早班'])  # hardcode
            if qs.pcoal_heat is None or qs.pcoal_heat <=0:
                s = SfLog.objects.filter(pcoal_heat__isnull=False).order_by('-start_time').first()
                if s and s.pcoal_heat:
                    qs.pcoal_heat = s.pcoal_heat
                else:
                    qs.pcoal_heat = 25000
                qs.save(update_fields=['pcoal_heat'])
            cache.set(key, qs.pcoal_heat, timeout=60 * 60 * 8)
            return qs.pcoal_heat
        except Exception as e:
            myLogger.error(f'获取煤粉热值失败，{e}, {year_s}, {month_s}, {day_s}', exc_info=True)
            return 25000


def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
    """
    生产日志提交后需要执行的操作
    """
    mlog = Mlog.objects.select_for_update().get(id=mlog.id)
    if mlog.work_start_time and mlog.work_start_time > timezone.now():
        raise ParseError('操作开始时间不能晚于当前时间')
    if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time:
        raise ParseError('操作结束时间不能早于操作开始时间')
    if mlog.count_real == 0:
        raise ParseError('产出数量不能为0')
    # if mlog.submit_time is not None:
    #     return
    if now is None:
        now = timezone.now()
    if mlog.handle_date is None:
        raise ParseError('请选择操作结束时间/操作日期')
    # if now.date() < mlog.handle_date:
    #     raise ParseError('不可提交未来的日志')
    
    mgroup = mlog.mgroup
    process = mgroup.process
    stored_scope = process.into_wm_scope
    stored_notok = process.store_notok
    belong_dept = mgroup.belong_dept
    material_out: Material = mlog.material_out
    material_in: Material = mlog.material_in
    supplier = mlog.supplier  # 外协
    is_fix = mlog.is_fix
    if is_fix: # 如果是返工，直接放到工段下
        stored_scope = WmScope.MGROUP
    stored_location = WmScope.resolve_location(stored_scope, mgroup)
    m_ins_list = []
    m_ins_bl_list = []

    if is_fix:
        can_matoutIds = process.get_canout_mat_ids()
    # 建立关系链
    m_outs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
    exclude_batchst_ids = []
    for item in m_outs:
        if item.mlogb_from and item.batch != item.mlogb_from.batch:
            source, _ = BatchSt.g_create(batch=item.mlogb_from.batch, exclude_batchst_ids=exclude_batchst_ids)
            target, _ = BatchSt.g_create(batch=item.batch, mlog=mlog, material_start=item.material_out)
            exclude_batchst_ids.append(target.id)
            BatchLog.g_create(source= source, target=target, mlog=mlog)
            inherit_zt_batch(source, target)
        if item.mlogbw_from and item.batch != item.mlogbw_from.mlogb.batch:
            source, _ = BatchSt.g_create(batch=item.mlogbw_from.mlogb.batch, exclude_batchst_ids=exclude_batchst_ids)
            target, _ = BatchSt.g_create(batch=item.batch, mlog=mlog, material_start=item.material_out)
            exclude_batchst_ids.append(target.id)
            BatchLog.g_create(source=source, target=target, mlog=mlog)
            inherit_zt_batch(source, target)

    # 消耗物料处理
    m_ins_list = []
    m_ins = Mlogb.objects.filter(mlog=mlog, material_in__isnull=False)
    if m_ins.exists():
        m_ins = m_ins.filter(need_inout=True)
        m_ins_list = [(mi.material_in, mi.batch if mi.batch else mi.batch, mi.count_use, None, mi) for mi in m_ins.all()]
        for item in m_ins:
            mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
            for itemx in mbd_qs:
                if itemx.defect and itemx.count > 0:
                    m_ins_bl_list.append((item.material_in, item.batch, itemx.count, itemx.defect, item))
    else:
        m_ins_list = [(material_in, mlog.batch, mlog.count_use, None, mlog)]
    for mi in m_ins_list:
        mi_ma, mi_batch, mi_count, defect, mlog_or_b = mi
        if mi_ma is None:
            continue
        if mi_count <= 0:
            raise ParseError('存在非正数!')
        # 优先使用日志已选库存；未选择时按工段、部门、全局依次匹配并记录实际来源
        wm_in = getattr(mlog_or_b, 'wm_in', None)
        if wm_in:
            wm = WMaterial.objects.select_for_update().get(id=wm_in.id)
        else:
            wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, mgroup=mgroup, state=WMaterial.WM_OK)
            if not wm_qs.exists():
                wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma,
                                                    belong_dept=belong_dept, mgroup=None, state=WMaterial.WM_OK)
            if not wm_qs.exists():
                wm_qs = WMaterial.objects.filter(
                    batch=mi_batch,
                    material=mi_ma,
                    belong_dept=None,
                    mgroup=None,
                    state=WMaterial.WM_OK,
                )
            count_x = wm_qs.count()
            if count_x == 1:
                wm = WMaterial.objects.select_for_update().get(id=wm_qs.first().id)
            elif count_x == 0:
                raise ParseError(
                    f'{str(mi_ma)}-{mi_batch}-批次库存不存在!')
            else:
                raise ParseError(
                    f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!')
            mlog_or_b.wm_in = wm
            mlog_or_b.save(update_fields=['wm_in'])
        
        if mi_count > wm.count:
            raise ParseError(
                f'{str(mi_ma)}-{mi_batch}-该批次车间库存不足!')
        else:
            wm.count = wm.count - mi_count
            wm.update_by = user
            wm.save()
            if mi_ma.tracking == Material.MA_TRACKING_SINGLE:
                mlogbws = Mlogbw.objects.filter(Q(ftest=None) | Q(ftest__is_ok=True), mlogb=mlog_or_b)
                if mlogbws.count() != mi_count:
                    raise ParseError("日志与实际使用明细数量不一致,操作失败")
                for item in mlogbws:
                    Wpr.change_or_new(wpr=item.wpr, old_wm=wm, ftest=item.ftest)

        # 针对加工前不良的暂时额外处理 
        # 合格B类的需要当合格品
        if stored_notok:
            for item in m_ins_bl_list:
                material, batch, count, defect, mi_ = item
                if count <= 0:
                    raise ParseError('存在非正数!')
                c_state = WMaterial.WM_NOTOK
                if isinstance(defect, Defect) and defect.okcate == Defect.DEFECT_OK_B:
                    c_state = WMaterial.WM_OK
                lookup = {
                    'batch': batch,
                    'material': material,
                    'defect': defect,
                    'state': c_state,
                    **stored_location,
                }
                wm, is_create = WMaterial.locked_get_or_create_inventory(
                    **lookup, defaults={})
                wm.count = wm.count + count
                if is_create:
                    wm.create_by = user
                    wm.batch_ofrom = mi_.batch_ofrom
                    wm.material_ofrom = mi_.material_ofrom
                wm.update_by = user
                wm.save()
                if material.tracking == Material.MA_TRACKING_SINGLE:
                    mlogbws = Mlogbw.objects.filter(mlogb=mlog_or_b, ftest__defect_main=defect)
                    if mlogbws.count() != count:
                        raise ParseError("日志与加工前不良明细数量不一致,操作失败")
                    for item in mlogbws:
                        Wpr.change_or_new(wpr=item.wpr, wm=wm, old_wm=wm, ftest=item.ftest)
                

    mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
    clear_defect = bool(mlog.clear_defect)
    if mlogb_out_qs.exists():
        mlogb_out_qs = mlogb_out_qs.filter(need_inout=True)
        if clear_defect:
            m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()]
        else:
            m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()]
        if stored_notok:
            for item in mlogb_out_qs:
                mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
                if item.qct is not None or mbd_qs.exists():
                    # if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
                    #     Mlogbw.cal_count_notok(item)
                    for itemx in mbd_qs:
                        if itemx.count > 0:
                            if clear_defect and itemx.defect and itemx.defect.okcate == Defect.DEFECT_OK_B:
                                continue
                            m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
                    # # 获取所有主要的不合格项/先暂时保留
                    # bw_qs = Mlogbw.objects.filter(mlogb=item)
                    # defectIds= Ftest.objects.filter(mlogbw_ftest__in=bw_qs).exclude(defect_main=None).values_list("defect_main__id", flat=True).distinct()
                    # defects_map = {d.id: d for d in Defect.objects.filter(id__in=defectIds)}
                    # # 过滤并统计相关数据
                    # filtered_bw_qs = bw_qs.filter(
                    #     ftest__defect_main__id__in=defects_map.keys()
                    # ).values('ftest__defect_main__id').annotate(xcount=Count('id'))
                    # # 整理结果
                    # for defect_data in filtered_bw_qs:
                    #     defect_id = defect_data['ftest__defect_main__id']
                    #     xcount = defect_data['xcount']
                    #     if xcount > 0:
                    #         defect = defects_map[defect_id]
                    #         m_outs_list.append((item.material_out, item.batch, xcount, 0, defect, item))
                else:
                    for f in Mlogb._meta.fields:
                        if 'count_n_' in f.name and getattr(item, f.name) > 0:
                            notok_sign = f.name.replace('count_n_', '')
                            m_outs_list.append( (item.material_out, item.batch if item.batch else mlog.batch, getattr(item, f.name), mlog.count_real_eweight, notok_sign, item))
        # 这里有一个漏洞，在产出物为兄弟件的情况下，不合格品的数量是记录在mlog上的，
        # 而不是mlogb上，以上的额外处理就没有效果了, 不过光子不记录不合格品
    else:
        m_outs_list = [(material_out, mlog.batch, mlog.count_ok, mlog.count_real_eweight, None, mlog)]
        # 一次填写的暂时不处理不合格品
    for mo in m_outs_list:
        mo_ma, mo_batch, mo_count, mo_count_eweight, notok_sign_or_defect, mlog_or_b = mo
        if mo_ma is None:
            continue
        if mo_count < 0:
            raise ParseError('存在负数!')
        elif mo_count == 0:
            continue
        if is_fix:
            wm_state = WMaterial.WM_REPAIRED
            if mo_ma.id in can_matoutIds:
                wm_state = WMaterial.WM_OK if notok_sign_or_defect is None or (
                    isinstance(notok_sign_or_defect, Defect) and notok_sign_or_defect.okcate in [Defect.DEFECT_OK, Defect.DEFECT_OK_B]
                ) else WMaterial.WM_NOTOK
        else:
            wm_state = WMaterial.WM_OK if notok_sign_or_defect is None or (
                    isinstance(notok_sign_or_defect, Defect) and notok_sign_or_defect.okcate in [Defect.DEFECT_OK, Defect.DEFECT_OK_B]
                ) else WMaterial.WM_NOTOK
        lookup = {
            'batch': mo_batch,
            'material': mo_ma,
            'notok_sign': None,
            'defect': None,
            'state': wm_state,
            **stored_location,
        }
        if isinstance(notok_sign_or_defect, Defect):
            lookup['defect'] = notok_sign_or_defect
        elif notok_sign_or_defect is not None:
            lookup['notok_sign'] = notok_sign_or_defect
        wm, is_create2 = WMaterial.locked_get_or_create_inventory(
            **lookup, defaults={})
        wm.count = wm.count + mo_count
        wm.count_eweight = mo_count_eweight
        wm.update_by = user
        if supplier is not None:
            wm.supplier = supplier
        if is_create2:
            wm.create_by = user
            if isinstance(mlog_or_b, Mlog) and mlog_or_b.wm_in:
                wm.batch_ofrom = mlog_or_b.wm_in.batch_ofrom
                wm.material_ofrom = mlog_or_b.wm_in.material_ofrom
            elif isinstance(mlog_or_b, Mlogb):
                wm.batch_ofrom = mlog_or_b.batch_ofrom
                wm.material_ofrom = mlog_or_b.material_ofrom
        if isinstance(mlog_or_b, Mlogb):
            if mlog_or_b.number_from and wm.number_from is not None and wm.number_from != mlog_or_b.number_from:
                raise ParseError(f'{wm.batch}-该批号现有库存来源于个号{wm.number_from}')
            wm.number_from = mlog_or_b.number_from
        wm.save()
        if mo_ma.tracking == Material.MA_TRACKING_SINGLE:
            if notok_sign_or_defect:
                mlogbws = Mlogbw.objects.filter(mlogb=mlog_or_b, ftest__is_ok=False, ftest__defect_main=notok_sign_or_defect)
            else:
                mlogbws = Mlogbw.objects.filter(Q(ftest=None) | Q(ftest__is_ok=True), mlogb=mlog_or_b)
            mlogbws_count = mlogbws.count()
            if mlogbws_count != mo_count:
                raise ParseError("日志与明细数量不一致,操作失败")
            for item in mlogbws:
                if item.wpr:
                    Wpr.change_or_new(wpr=item.wpr, wm=wm, ftest=item.ftest)
                else:
                    if not check_wpr_number(item.number):
                        raise ParseError(f'{item.number} 该编号格式有误')
                    wpr_from = None
                    if item.mlogbw_from:
                        wpr_from = item.mlogbw_from.wpr
                    wpr = Wpr.change_or_new(number=item.number, 
                                            wm=wm, ftest=item.ftest, 
                                            wpr_from=wpr_from)
                    item.wpr = wpr
                    item.save()
        
    mlog.submit_time = now
    mlog.submit_user = user
    mlog.stored_notok = stored_notok
    mlog.stored_scope = stored_scope
    if mlog.work_end_time is None and mlog.work_start_time is not None:
        mlog.work_end_time = now
    mlog.save()

    # 更新任务进度
    cal_mtask_progress_from_mlog(mlog)

    # 触发批次统计分析
    xbatches = list(Mlogb.objects.filter(mlog=mlog).values_list('batch', flat=True))
    ana_batch_thread(xbatchs=xbatches, mgroup=mlog.mgroup)

    # 触发单个统计
    wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, wpr__isnull=False).values_list('wpr__id', flat=True))
    if wprIds:
        ana_wpr_thread(wprIds, mlog.mgroup)


def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
    """日志撤回
    """
    # if mlog.submit_time is None:
    #     return
    mlog = Mlog.objects.select_for_update().get(id=mlog.id)
    if now is None:
        now = timezone.now()

    mgroup = mlog.mgroup
    process = mgroup.process
    belong_dept = mgroup.belong_dept
    material_out:Material = mlog.material_out
    material_in:Material = mlog.material_in
    stored_notok = mlog.stored_notok
    stored_scope = mlog.stored_scope
    stored_location = WmScope.resolve_location(stored_scope, mgroup)
    is_fix = mlog.is_fix
    
    if is_fix:
        can_matoutIds = process.get_canout_mat_ids()
    # 先回退产物
    # 有多个产物的情况
    # 需要考虑不合格品退回的情况
    mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
    clear_defect = bool(mlog.clear_defect)
    if mlogb_out_qs.exists():
        mlogb_out_qs = mlogb_out_qs.filter(need_inout=True)
        if clear_defect:
            m_outs_list = [
                (mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok, mlog.count_real_eweight, None, mo)
                for mo in mlogb_out_qs.all()]
        else:
            m_outs_list = [
                (mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo)
                for mo in mlogb_out_qs.all()]
        if stored_notok:
                for item in mlogb_out_qs:
                    mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
                    if item.qct is not None or mbd_qs.exists():
                        # if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
                        #     Mlogbw.cal_count_notok(item)
                        for itemx in mbd_qs:
                            if itemx.count > 0:
                                if clear_defect and itemx.defect and itemx.defect.okcate == Defect.DEFECT_OK_B:
                                    continue
                                m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
                    # if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
                    #     # 获取所有主要的不合格项
                    #     bw_qs = Mlogbw.objects.filter(mlogb=item)
                    #     defectIds= Ftest.objects.filter(mlogbw_ftest__in=bw_qs).exclude(defect_main=None).values_list("defect_main__id", flat=True).distinct()
                    #     defects_map = {d.id: d for d in Defect.objects.filter(id__in=defectIds)}
                    #     # 过滤并统计相关数据
                    #     filtered_bw_qs = bw_qs.filter(
                    #         ftest__defect_main__id__in=defects_map.keys()
                    #     ).values('ftest__defect_main__id').annotate(xcount=Count('id'))
                    #     # 整理结果
                    #     for defect_data in filtered_bw_qs:
                    #         defect_id = defect_data['ftest__defect_main__id']
                    #         xcount = defect_data['xcount']
                    #         if xcount > 0:
                    #             defect = defects_map[defect_id]
                    #             m_outs_list.append((item.material_out, item.batch, xcount, 0, defect, item))
                    else:
                        for f in Mlogb._meta.fields:
                            if 'count_n_' in f.name and getattr(item, f.name) > 0:
                                notok_sign = f.name.replace('count_n_', '')
                                m_outs_list.append((item.material_out, item.batch if item.batch else mlog.batch,
                                                    getattr(item, f.name), mlog.count_real_eweight, notok_sign, item))
    else:
        m_outs_list = [(material_out, mlog.batch, mlog.count_ok, mlog.count_real_eweight, None, mlog)]
        # 一次填写的暂时不处理不合格品

    for mo in m_outs_list:
        mo_ma, mo_batch, mo_count, _, notok_sign_or_defect, mlog_or_b = mo
        if mo_ma is None:
            continue
        if mo_count < 0:
            raise ParseError('存在负数!')
        elif mo_count == 0:
            continue
        if is_fix:
            wm_state = WMaterial.WM_REPAIRED
            if mo_ma.id in can_matoutIds:
                wm_state = WMaterial.WM_OK if notok_sign_or_defect is None or (
                    isinstance(notok_sign_or_defect, Defect) and notok_sign_or_defect.okcate in [Defect.DEFECT_OK, Defect.DEFECT_OK_B]
                ) else WMaterial.WM_NOTOK
        else:
            wm_state = WMaterial.WM_OK if notok_sign_or_defect is None or (
                    isinstance(notok_sign_or_defect, Defect) and notok_sign_or_defect.okcate in [Defect.DEFECT_OK, Defect.DEFECT_OK_B]
                ) else WMaterial.WM_NOTOK
        lookup = {
            'batch': mo_batch,
            'material': mo_ma,
            'notok_sign': None,
            'defect': None,
            'state': wm_state,
            **stored_location,
        }
        if isinstance(notok_sign_or_defect, Defect):
            lookup['defect'] = notok_sign_or_defect
        else:
            lookup['notok_sign'] = notok_sign_or_defect
        wm_qs = WMaterial.objects.filter(**lookup)
        count_x = wm_qs.count()
        if count_x == 1:
            wm = wm_qs.first()
        elif count_x == 0:
            raise ParseError(
                f'{str(mo_ma)}-{mo_batch}-批次库存不存在!')
        else:
            raise ParseError(
                f'{str(mo_ma)}-{mo_batch}-存在多个相同批次!')
        
        wm = WMaterial.objects.select_for_update().get(id=wm.id)
        wm.count = wm.count - mo_count
        if wm.count < 0:
            raise ParseError(f'{wm.batch} 车间库存不足, 产物无法回退')
        elif wm.count >= 0:
            if isinstance(mlog_or_b, Mlogb):
                if mlog_or_b.number_from == wm.number_from:
                    wm.number_from = None
            wm.update_by = user
            wm.save()
            if mo_ma.tracking == Material.MA_TRACKING_SINGLE:
                if notok_sign_or_defect:
                    mlogbws = Mlogbw.objects.filter(mlogb=mlog_or_b, ftest__is_ok=False, ftest__defect_main=notok_sign_or_defect)
                else:
                    mlogbws = Mlogbw.objects.filter(Q(ftest=None) | Q(ftest__is_ok=True), mlogb=mlog_or_b)
                if mlogbws.count() != mo_count:
                    raise ParseError("日志与明细数量不一致,操作失败")
                for item in mlogbws:
                    Wpr.change_or_new(wpr=item.wpr, old_wm=wm)

    # 再生成消耗
    m_ins_list = []
    m_ins_bl_list = []
    m_ins = Mlogb.objects.filter(mlog=mlog, material_in__isnull=False)
    if m_ins.exists():
        m_ins = m_ins.filter(need_inout=True)
        for mi in m_ins.all():
            m_ins_list.append((mi.material_in, mi.batch, mi.count_use, None, mi))
            for item in m_ins:
                mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
                for itemx in mbd_qs:
                    if itemx.defect and itemx.count > 0:
                        m_ins_bl_list.append((item.material_in, item.batch, itemx.count, itemx.defect, item))
    else:
        m_ins_list = [(material_in, mlog.batch, mlog.count_use, mlog.wm_in, mlog)]
    for mi in m_ins_list:
        mi_ma, mi_batch, mi_count, defect_or, mlog_or_b = mi
        if mi_ma is None:
            continue
        if mi_count <= 0:
            raise ParseError('存在非正数!')
        wm_in = getattr(mlog_or_b, 'wm_in', None)
        if wm_in:
            wm = WMaterial.objects.select_for_update().get(id=wm_in.id)
        else:
            # 针对光子的情况，实际上必须需要wm_in
            lookup = {
                'batch': mi_batch,
                'material': mi_ma,
                'state': WMaterial.WM_OK,
                **stored_location,
            }
            wm, _ = WMaterial.locked_get_or_create_inventory(
                **lookup, defaults={})
        wm.count = wm.count + mi_count
        wm.update_by = user
        wm.save()
        if mi_ma.tracking == Material.MA_TRACKING_SINGLE:
            mlogbws = Mlogbw.objects.filter(Q(ftest=None) | Q(ftest__is_ok=True), mlogb=mlog_or_b)
            if mlogbws.count() != mi_count:
                raise ParseError("日志与实际使用明细数量不一致,操作失败")
            for item in mlogbws:
                Wpr.change_or_new(wpr=item.wpr, wm=wm)

    # 针对加工前不良的暂时额外处理 
    if stored_notok:
        for item in m_ins_bl_list:
            material, batch, count, defect, mi_ = item
            if count <= 0:
                raise ParseError('存在非正数!')
            c_state = WMaterial.WM_NOTOK
            if isinstance(defect, Defect) and defect.okcate == Defect.DEFECT_OK_B:
                c_state = WMaterial.WM_OK
            lookup = {
                'batch': batch,
                'material': material,
                'defect': defect,
                'state': c_state,
                **stored_location,
            }
            wm, is_create = WMaterial.locked_get_or_create_inventory(
                **lookup, defaults={})
            wm.count = wm.count - count
            if wm.count < 0:
                raise ParseError('加工前不良数量大于库存量')
            if is_create:
                wm.create_by = user
            else:
                wm.update_by = user
            wm.save()
            if material.tracking == Material.MA_TRACKING_SINGLE:
                mlogbws = Mlogbw.objects.filter(mlogb=mlog_or_b, ftest__defect_main=defect)
                if mlogbws.count() != count:
                    raise ParseError("日志与加工前不良明细数量不一致,操作失败")
                for item in mlogbws:
                    Wpr.change_or_new(wpr=item.wpr, wm=wm)

    mlog.submit_time = None
    mlog.submit_user = None
    mlog.save()

    # 兜底清理历史 is_fix 错误带入的继承缺陷
    if is_fix:
        legacy_mlogbouts = list(Mlogb.objects.filter(
            mlog=mlog,
            material_out__isnull=False,
            mlogbdefect__is_inherited=True,
        ).distinct())
        if legacy_mlogbouts:
            for mlogbout in legacy_mlogbouts:
                mlogbout.sync_inherited_defect(cal_count=True)
            mlog.cal_mlog_count_from_mlogb()

    # mtask变更状态
    update_mtaskIds = []
    if mlog.mtask:
        update_mtaskIds.append(mlog.mtask.id)
    list_m = Mlogb.objects.filter(mlog=mlog).values_list('mtask__id', flat=True).distinct()
    update_mtaskIds += list(list_m)
    if update_mtaskIds:
        Mtask.objects.filter(id__in=update_mtaskIds, state=Mtask.MTASK_SUBMIT).update(state=Mtask.MTASK_ASSGINED)

    # 更新任务进度
    cal_mtask_progress_from_mlog(mlog)

    # 清除关系链
    BatchLog.clear(mlog=mlog)

    # 触发批次统计分析
    xbatches = list(Mlogb.objects.filter(mlog=mlog).values_list('batch', flat=True))
    ana_batch_thread(xbatches, mgroup=mlog.mgroup)

    # 触发单个统计
    wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, wpr__isnull=False).values_list('wpr__id', flat=True))
    if wprIds:
        ana_wpr_thread(wprIds, mlog.mgroup)

def cal_mtask_progress_from_mlog(mlog:Mlog):
    """
    更新mlog关联的任务进度(可线程中执行)
    """
    if mlog.fill_way in [Mlog.MLOG_2, Mlog.MLOG_12] and mlog.mtask:
        update_mtask(mlog.mtask, fill_way=mlog.fill_way)
    elif mlog.fill_way == Mlog.MLOG_23:
        mlog.cal_mlog_count_from_mlogb()
        m_outs_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
        caled_mtask = []
        for item in m_outs_qs.all():
            mtask = item.mtask
            if mtask:
                if mtask in caled_mtask:
                    continue
                update_mtask(mtask, fill_way=mlog.fill_way)
                caled_mtask.append(mtask)
    

def update_mtask(mtask: Mtask, fill_way: int = 10):
    mtask = Mtask.objects.get(id=mtask.id)  # 防止并发修改获取最新的mtask
    from apps.pm.models import Utask
    if fill_way == Mlog.MLOG_2:
        res = Mlog.objects.filter(mtask=mtask).exclude(submit_time=None).aggregate(sum_count_real=Sum(
            'count_real'), sum_count_ok=Sum('count_ok'), sum_count_notok=Sum('count_notok'))
        mtask.count_real = res['sum_count_real'] if res['sum_count_real'] else 0
        mtask.count_ok = res['sum_count_ok'] if res['sum_count_ok'] else 0
        mtask.count_notok = res['sum_count_notok'] if res['sum_count_notok'] else 0
        mtask.save()
        utask = mtask.utask
        if utask and mtask.is_count_utask:
            res2 = Mtask.objects.filter(utask=utask, mgroup=mtask.mgroup).aggregate(sum_count_real=Sum(
                'count_real'), sum_count_ok=Sum('count_ok'), sum_count_notok=Sum('count_notok'))
            utask.count_real = res2['sum_count_real'] if res2['sum_count_real'] else 0
            utask.count_ok = res2['sum_count_ok'] if res2['sum_count_ok'] else 0
            utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0
            if utask.count_ok > 0 and utask.state == Utask.UTASK_ASSGINED:
                utask.state = Utask.UTASK_WORKING
            if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0:
                utask.state = Utask.UTASK_SUBMIT
            elif utask.state == Utask.UTASK_SUBMIT:
                utask.state = Utask.UTASK_WORKING if utask.count_ok > 0 else Utask.UTASK_ASSGINED
            utask.save()
    elif fill_way in [Mlog.MLOG_23, Mlog.MLOG_12]:
        # 已经提交的日志
        m_outs_qs_mtask = Mlogb.objects.filter(mtask=mtask, material_out__isnull=False, mlog__submit_time__isnull=False)
        res = m_outs_qs_mtask.aggregate(
                sum_count_real=Sum('count_real', default=0),
                sum_count_ok=Sum('count_ok', default=0),
                sum_count_notok=Sum('count_notok', default=0)
            )
        mtask.count_real = res['sum_count_real'] or 0
        mtask.count_ok = res['sum_count_ok'] or 0
        mtask.count_notok = res['sum_count_notok'] or 0
        mtask.save()
        utask = mtask.utask
        is_main_mgroup = False
        if utask:
            if utask.state == Utask.UTASK_ASSGINED:
                utask.state = Utask.UTASK_WORKING
                utask.save()
            if mtask.is_count_utask:
                is_main_mgroup = True
            elif mtask.material_out == utask.material:
                is_main_mgroup = True
        if is_main_mgroup:
            res2 = Mtask.objects.filter(utask=utask, mgroup=mtask.mgroup).aggregate(sum_count_real=Sum(
                'count_real'), sum_count_ok=Sum('count_ok'), sum_count_notok=Sum('count_notok'))
            utask.count_real = res2['sum_count_real'] if res2['sum_count_real'] else 0
            utask.count_ok = res2['sum_count_ok'] if res2['sum_count_ok'] else 0
            utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0
            # if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0:
            #     utask.state = Utask.UTASK_SUBMIT
            if utask.state == Utask.UTASK_SUBMIT and Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).exists():
                utask.state = Utask.UTASK_WORKING if utask.count_ok > 0 else Utask.UTASK_ASSGINED
            utask.save()

def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, None]):
    """
    交接提交后需要执行的操作
    """
    handover = (
        Handover.objects
        .select_for_update()
        .get(pk=handover.pk)
    )
    if handover.submit_time is not None:
        return
    now = timezone.now()
    handoverb_qs = Handoverb.objects.filter(handover=handover)
    need_add = True
    material:Material = handover.material
    mtype = handover.mtype
    if '混料' in material.name:  # hard code
        need_add = False
    if handoverb_qs.exists():
        handoverb_list = [(item.wm.id, item.count, item) for item in handoverb_qs]
    else:
        handoverb_list = [(handover.wm.id, handover.count, handover)]
    
    recive_mgroup = handover.recive_mgroup
    recive_dept = handover.recive_dept
    
    new_batch = handover.new_batch
    if new_batch and mtype != Handover.H_MERGE:
        raise ParseError("只有合并时才能提供新批次号")

    batches = []
    mids = []
    exclude_batchst_ids = []
    check_batch_exist = False
    merge_target_defect = None
    merge_clear_defect = False
    if mtype == Handover.H_MERGE:
        if new_batch:
            batches = [new_batch]
        else:
            raise ParseError("合并批次时请提供新批次号")
        if handover.new_wm is None:
            check_batch_exist = True
        target_b, _ = BatchSt.g_create(batch=new_batch, handover=handover, material_start=material, check_batch_exist=check_batch_exist)
        exclude_batchst_ids.append(target_b.id)
        merge_clear_defect = bool(handover.clear_defect)
        if merge_clear_defect:
            merge_target_defect = None
        else:
            source_wm_ids = [hb[0] for hb in handoverb_list]
            merge_target_defect = WMaterial.objects.filter(id__in=source_wm_ids).first().defect
    elif mtype == Handover.H_DIV:
        if handover.wm is None:
            raise ParseError('拆批请选择车间库存')
        batches_to_limit = BatchLog.batches_to(batch=handover.wm.batch)["batches"]
        source_b, _ = BatchSt.g_create(batch=handover.wm.batch)
    for indx, item in enumerate(handoverb_list):
        wmId, xcount, handover_or_b = item
        if xcount <= 0:
            raise ParseError("存在非正数!")
        wm_from = (
                WMaterial.objects
                .select_for_update()
                .get(id=wmId)
            )
        mids.append(wm_from.material.id)

        # 合并为新批
        if mtype == Handover.H_MERGE:
            batch = new_batch
            source, _ = BatchSt.g_create(batch=wm_from.batch, exclude_batchst_ids=exclude_batchst_ids)
            BatchLog.g_create(source=source, target=target_b, handover=handover, relation_type="merge")
            batches.append(wm_from.batch)
        elif mtype == Handover.H_DIV:
            batch = handover_or_b.batch
            batches.append(batch)
            if batch in batches_to_limit:
                raise ParseError(f'{batch} 不在可拆批次中')
            else:
                batches_to_limit.append(batch)
            target, _ = BatchSt.g_create(batch=batch, handover=handover, material_start=material)
            exclude_batchst_ids.append(target.id)
            # 这里暂时忽略check_batch_exist，因为拆批一般不会重复
            BatchLog.g_create(source=source_b, target=target, handover=handover, relation_type="split")
            inherit_zt_batch(source_b, target)
        else:
            batch = wm_from.batch
            batches.append(batch)

        WMaterial.decrease(wm_id=wm_from.id, user=user, count=xcount)

        if need_add:
            # 开始变动
            if handover.type == Handover.H_NORMAL:
                if mtype == Handover.H_MERGE and handover.new_wm:
                    wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
                    defect_ok = wm_to.defect == wm_from.defect or (
                        merge_clear_defect and wm_to.defect is None and wm_from.state == WMaterial.WM_OK
                    )
                    if wm_to.state != wm_from.state or wm_to.material != wm_from.material or not defect_ok:
                        raise ParseError("正常合并到的车间库存状态或物料异常")
                else:
                    wm_to, _ = WMaterial.locked_get_or_create_inventory(
                        batch=batch,
                        material=material,
                        mgroup=recive_mgroup,
                        belong_dept=recive_dept,
                        state=wm_from.state,
                        notok_sign=wm_from.notok_sign,
                        defect=merge_target_defect if mtype == Handover.H_MERGE else wm_from.defect,
                        defaults={
                            "batch_ofrom": wm_from.batch_ofrom,
                            "material_ofrom": wm_from.material_ofrom,
                            "create_by": user,
                            "number_from": wm_from.number_from
                        }
                    )
            elif handover.type == Handover.H_REPAIR:
                # 返修交接
                recive_mgroup = handover.recive_mgroup
                wm_state = WMaterial.WM_REPAIR
                if mtype == Handover.H_MERGE and handover.new_wm:
                    wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
                    if wm_to.state != WMaterial.WM_REPAIR or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
                        raise ParseError("返修合并到的车间库存状态或物料异常")
                elif recive_mgroup:
                    wm_to, _ = WMaterial.locked_get_or_create_inventory(
                        batch=batch,
                        material=material,
                        mgroup=recive_mgroup,
                        belong_dept=recive_dept,
                        notok_sign=wm_from.notok_sign,
                        defect=wm_from.defect,
                        material_origin=material,
                        state=wm_state,
                        defaults={
                        "batch_ofrom": wm_from.batch_ofrom,
                        "material_ofrom": wm_from.material_ofrom,
                        "create_by": user,
                        "number_from": wm_from.number_from
                        }
                    )
                else:
                    raise ParseError("返工交接必须指定接收工段")
            elif handover.type == Handover.H_SCRAP:
                if mtype == Handover.H_MERGE and handover.new_wm:
                    wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
                    if wm_to.state != WMaterial.WM_SCRAP or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
                        raise ParseError("报废合并到的车间库存状态或物料异常")
                elif recive_mgroup:
                    wm_to, _ = WMaterial.locked_get_or_create_inventory(
                        batch=batch,
                        material=material,
                        mgroup=recive_mgroup,
                        belong_dept=recive_dept,
                        notok_sign=wm_from.notok_sign,
                        defect=wm_from.defect,
                        state=WMaterial.WM_SCRAP,
                        defaults={
                        "batch_ofrom": wm_from.batch_ofrom,
                        "material_ofrom": wm_from.material_ofrom,
                        "create_by": user,
                        "number_from": wm_from.number_from
                        }
                    )
                else:
                    raise ParseError("不支持非工段报废")
            elif handover.type == Handover.H_CHANGE:
                if mtype == Handover.H_MERGE and handover.new_wm:
                    wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
                    if wm_to.material != handover.material_changed or wm_to.state != handover.state_changed:
                        raise ParseError("改版合并到的车间库存状态或物料异常")
                elif handover.recive_mgroup:
                    wm_to, _ = WMaterial.locked_get_or_create_inventory(
                        batch=batch,
                        material=handover.material_changed,
                        state=handover.state_changed,
                        mgroup=recive_mgroup,
                        belong_dept=recive_dept,
                        notok_sign=wm_from.notok_sign,
                        defect=wm_from.defect,
                        material_origin=None if handover.material_changed == material else material,
                        state_origin=None if handover.state_changed == wm_from.state else wm_from.state,
                        defaults={
                            "batch_ofrom": wm_from.batch_ofrom,
                            "material_ofrom": wm_from.material_ofrom,
                            "create_by": user,
                            "number_from": wm_from.number_from
                        }
                    )
                else:
                    raise ParseError("改版交接必须指定接收工段")
            elif handover.type == Handover.H_BACK:
                if wm_from and wm_from.state != WMaterial.WM_OK:
                    raise ParseError("仅合格品支持退回")
                if mtype == Handover.H_MERGE and handover.new_wm:
                    wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
                else:
                    wm_to, _ = WMaterial.locked_get_or_create_inventory(
                        batch=batch,
                        material=material,
                        mgroup=recive_mgroup,
                        belong_dept=recive_dept,
                        state=wm_from.state,
                        notok_sign=wm_from.notok_sign,
                        defect=merge_target_defect if mtype == Handover.H_MERGE else wm_from.defect,
                        defaults={
                            "batch_ofrom": wm_from.batch_ofrom,
                            "material_ofrom": wm_from.material_ofrom,
                            "create_by": user,
                            "number_from": wm_from.number_from
                        }
                    )
            else:
                raise ParseError("不支持该交接类型")

            WMaterial.increase(wm_id=wm_to.id, user=user,count=xcount, count_eweight=handover.count_eweight if handover.count_eweight else None)
            handover_or_b.wm_to = wm_to
            handover_or_b.save()
            if material.tracking == Material.MA_TRACKING_SINGLE:
                handoverbws = Handoverbw.objects.filter(handoverb=handover_or_b)
                if handoverbws.count() != xcount:
                    raise ParseError("交接与明细数量不一致,操作失败")
                wm_ids = list(Wpr.objects.filter(wpr_handoverbw__in=handoverbws).values_list("wm_id", flat=True).distinct())
                if len(wm_ids) == 1 and wm_ids[0] == wm_from.id:
                    pass
                else:
                    raise ParseError(f'{batch}物料明细中存在{len(wm_ids)}个不同物料批次')
                for item in handoverbws:
                    wpr:Wpr = item.wpr
                    Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb)
                db_count = WMaterial.objects.filter(id=wm_to.id).values_list("count", flat=True).get()
                if db_count != Wpr.objects.filter(wm=wm_to).count():
                    raise ParseError("交接与明细数量不一致2,操作失败")

    handover.submit_user = user
    handover.submit_time = now
    if handover.recive_user is None:
        handover.recive_user = user
    handover.save()
    
    ana_batch_thread(xbatchs=batches)

def handover_revert(handover:Handover, handler:User=None):
    handover = Handover.objects.select_for_update().get(id=handover.id)
    if handover.submit_time is None:
        raise ParseError('该交接单未提交!')
    ticket:Ticket = handover.ticket
    if ticket:
        # 首先把ticket改回开始状态
        WfService.retreat(ticket=ticket, suggestion="撤回交接单", handler=handler, next_handler=handover.create_by)
    mids = []
    # handover_type = handover.type
    # handover_mtype = handover.mtype
    # if handover_type in [Handover.H_NORMAL, Handover.H_REPAIR] and handover_mtype == Handover.H_NORMAL:
    #     pass
    # else:
    #     raise ParseError('该交接单不支持撤销!')
    xbatchs = []
    if handover.new_batch:
        xbatchs.append(handover.new_batch)
    handoverb_qs = Handoverb.objects.filter(handover=handover)
    material = handover.material
    for item in handoverb_qs:
        if item.wm:
            mids.append(item.wm.material.id)
        if item.batch:
            xbatchs.append(item.batch)
        wm = item.wm
        wm_to = item.wm_to
        if wm is None or wm_to is None:
            raise ParseError('该交接单不支持撤回2!')
        if wm == wm_to:
            # 此时是自己交给自己，不需要做任何操作
            pass
        else:
            WMaterial.increase(wm_id=wm.id, user=handler, count=item.count)
            WMaterial.decrease(wm_id=wm_to.id, user=handler, count=item.count)
        if material.tracking == Material.MA_TRACKING_SINGLE:
                handoverbws = Handoverbw.objects.filter(handoverb=item)
                if handoverbws.count() != item.count:
                    raise ParseError("交接与明细数量不一致,操作失败")
                wm = WMaterial.objects.get(id=wm.id)
                for item in handoverbws:
                    wpr:Wpr = item.wpr
                    Wpr.change_or_new(wpr=wpr, wm=wm, old_wm=wpr.wm, old_mb=wpr.mb, add_version=False)

    handover.submit_time = None
    handover.submit_user = None
    handover.save()
    # 删除追踪链
    BatchLog.clear(handover=handover)
    
    ana_batch_thread(xbatchs=xbatchs)

def mlog_submit_validate(ins: Mlog):
    if ins.submit_time:
        raise ParseError('该日志已提交!')
    if ins.mtask and ins.mtask.state == Mtask.MTASK_STOP:
        raise ParseError('该任务已停止!')
    if ins.fill_way == Mlog.MLOG_23:
        if not Mlogb.objects.filter(material_out__isnull=False, mlog=ins).exists():
            raise ParseError('该日志未指定产出!')
        if not Mlogb.objects.filter(material_in__isnull=False, mlog=ins).exists():
            raise ParseError('该日志未指定消耗!')
        if Mlogb.objects.filter(material_out__isnull=False, count_real=0, mlog=ins).exists():
            raise ParseError('产出数量不能为0!')
    if ins.is_fix is False and ins.route:
        process = ins.route.process
        if Process.objects.filter(parent=process).exists() and not MlogUser.objects.filter(mlog=ins).exists():
            raise ParseError('该日志子工序信息未完善!')

def bind_mlog(ticket: Ticket, transition, new_ticket_data: dict):
    ins = Mlog.objects.get(id=new_ticket_data['t_id'])
    mlog_submit_validate(ins) # 校验是否可submit
    ticket_data = ticket.ticket_data
    ticket_data.update({
        't_model': 'mlog',
        't_id': ins.id,
    })
    ticket.ticket_data = ticket_data
    ticket.create_by = ins.create_by
    ticket.save()
    if ins.ticket is None:
        ins.ticket = ticket
        ins.save()

def bind_handover(ticket: Ticket, transition, new_ticket_data: dict):
    ins = Handover.objects.get(id=new_ticket_data['t_id'])
    ticket_data = ticket.ticket_data
    ticket_data.update({
        't_model': 'handover',
        't_id': ins.id,
    })
    ticket.ticket_data = ticket_data
    ticket.create_by = ins.create_by
    ticket.save()
    if ins.ticket is None:
        ins.ticket = ticket
        ins.save()

def mlog_audit_end(ticket: Ticket):
    now = timezone.now()
    ins = Mlog.objects.get(id=ticket.ticket_data['t_id'])
    mlog_submit(ins, ticket.create_by, now)


def get_batch_dag(batch_number: str, method="full"):
    try:
        batch_ins = BatchSt.objects.get(batch=batch_number, version=1)
    except Exception:
        raise ParseError("该批次号未构建关系链")

    r_dict = {
        "split": "分",
        "merge": "合"
    }

    if method == "full":
        raise ParseError("不支持获取全局关系链条")
        # 完整DAG模式 - 收集所有相关批次和边（原逻辑）
        nodes_set = {batch_ins.id}
        edges = []
        prev_size = 0

        while len(nodes_set) > prev_size:
            prev_size = len(nodes_set)
            logs = BatchLog.objects.filter(Q(source__id__in=nodes_set) | Q(target__id__in=nodes_set)).select_related(
                "source", "target"
            ).order_by("update_time")
            
            for log in logs:
                source = log.source.id
                target = log.target.id
                nodes_set.update([source, target])
                edges.append({
                    'source': source,
                    'target': target,
                    "handover": log.handover.id if log.handover else None,
                    "mlog": log.mlog.id if log.mlog else None,
                    'label': r_dict.get(log.relation_type, ""),
                })

    elif method == "direct":
        # 直接关系模式 - 只查询直接相连的批次
        nodes_set = {batch_ins.id}
        edges = []

        # 查询作为source或target的直接关系
        logs = BatchLog.objects.filter(Q(source__id=batch_ins.id) | Q(target__id=batch_ins.id)).select_related(
            "source", "target"
        ).order_by("update_time")

        exist_log_ids = []
        left_source_ids = []
        right_target_ids = []
        for log in logs:
            exist_log_ids.append(log.id)
            source = log.source.id
            target = log.target.id
            if source == batch_ins.id:
                right_target_ids.append(target)
                nodes_set.add(target)
            if target == batch_ins.id:
                left_source_ids.append(source)
                nodes_set.add(source)
            edges.append({
                'id': log.id,
                'source': source,
                'target': target,
                "handover": log.handover.id if log.handover else None,
                "mlog": log.mlog.id if log.mlog else None,
                'label': r_dict.get(log.relation_type, ""),
            })
        
        # 查询作为source的其他关系
        # leftLogs = BatchLog.objects.filter(source_id__in=left_source_ids).exclude(id__in=exist_log_ids)
        # for log in leftLogs:
        #     source = log.source.id
        #     target = log.target.id
        #     nodes_set.add(log.target.id)
        #     edges.append({
        #         'id': log.id,
        #         'source': source,
        #         'target': target,
        #         "handover": log.handover.id if log.handover else None,
        #         "mlog": log.mlog.id if log.mlog else None,
        #         'label': r_dict.get(log.relation_type, ""),
        #     })
        
        # rightLogs = BatchLog.objects.filter(target_id__in=right_target_ids).exclude(id__in=exist_log_ids)
        # for log in rightLogs:
        #     source = log.source.id
        #     target = log.target.id
        #     nodes_set.add(log.source.id)
        #     edges.append({
        #         'id': log.id,
        #         'source': source,
        #         'target': target,
        #         "handover": log.handover.id if log.handover else None,
        #         "mlog": log.mlog.id if log.mlog else None,
        #         'label': r_dict.get(log.relation_type, ""),
        #     })

    else:
        raise ParseError("不支持的查询方法，请使用'full'或'direct'")

    # 将批次号排序
    nodes_qs = BatchSt.objects.filter(id__in=nodes_set).order_by('update_time')
    
    nodes = [{
        'id': item.id,
        'label': item.batch,
        'shape': 'rect'
    } for item in nodes_qs]

    return {'nodes': nodes, 'edges': edges}

