from django.db import models
from apps.utils.models import CommonADModel, CommonBDModel, BaseModel
from apps.mtm.models import Mgroup, Team, Shift, Material, Route, Process, WmScope
from apps.pm.models import Mtask, Mtaskb
from apps.system.models import User
from django.utils.timezone import localtime
from apps.em.models import Equipment
from apps.system.models import Dept
from apps.pum.models import Supplier
from django.db.models import Sum, Subquery
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ParseError
from django.db.models import Count
from django.db import connection, transaction
from django.db.models import Max
import json
import re
from django.db.models import Q, F
import django.utils.timezone as timezone
from apps.utils.sql import query_all_dict
import logging
myLogger = logging.getLogger('log')

# Create your models here.
class SfLog(CommonADModel):
    """TN: 值班记录
    """
    mgroup = models.ForeignKey(
        Mgroup, verbose_name='关联工段', on_delete=models.CASCADE)
    team = models.ForeignKey(Team, verbose_name='班组',
                             on_delete=models.CASCADE, null=True, blank=True)
    shift = models.ForeignKey(
        Shift, verbose_name='当班班次', on_delete=models.CASCADE)
    leader = models.ForeignKey(
        'system.user', verbose_name='班长', on_delete=models.CASCADE, null=True, blank=True)
    work_date = models.DateField('值班日期', null=True, blank=True)
    start_time = models.DateTimeField('值班开始')
    end_time = models.DateTimeField('值班结束')
    note = models.TextField('其他备注', null=True, blank=True)
    stlogs = models.ManyToManyField(
        'wpm.stlog', verbose_name='关联异常记录', through='wpm.sflogexp', related_name='sflog_stlogs')
    last_test_time = models.DateTimeField('最后质检时间', null=True, blank=True)
    total_sec_now = models.PositiveIntegerField('总时长动', default=0)
    total_sec = models.PositiveIntegerField('总时长', default=43200)
    shut_sec = models.PositiveIntegerField('停机时长', default=0)
    pcoal_heat = models.FloatField('煤粉热值', null=True, blank=True)

    @property
    def get_ymd(self):
        """
        返回值班记录所属年月日
        """
        start_time_local = localtime(self.start_time)
        return start_time_local.year, start_time_local.month, start_time_local.day


class StLog(CommonADModel):
    """
    TN: 生产异常记录
    """
    title = models.CharField('异常名称', max_length=20, default='')
    is_shutdown = models.BooleanField('是否是停机', default=False)
    mgroup = models.ForeignKey(
        Mgroup, verbose_name='关联工段', on_delete=models.CASCADE)
    sflog = models.ForeignKey(
        SfLog, on_delete=models.SET_NULL, verbose_name='发生时所在值班', null=True, blank=True)
    sflogs = models.ManyToManyField(
        'wpm.sflog', verbose_name='关联所有当班', through='wpm.sflogexp', related_name='stlog_sflogs')
    start_time = models.DateTimeField('发生时间')
    end_time = models.DateTimeField('结束时间', null=True, blank=True)
    duration_sec = models.PositiveIntegerField('持续时长(s)', null=True, blank=True)
    cate = models.CharField('原因类别', max_length=10, null=True, blank=True)
    reason = models.TextField('事件原因', null=True, blank=True)
    measure = models.TextField('处置措施', null=True, blank=True)
    handler = models.CharField('处理人', null=True, blank=True, max_length=100)

    class Meta:
        unique_together = ('mgroup', 'start_time')



class SfLogExp(CommonADModel):
    """
    TN: 生产异常处理
    """
    sflog = models.ForeignKey(
        SfLog, on_delete=models.CASCADE, verbose_name='关联值班记录')
    stlog = models.ForeignKey(
        StLog, verbose_name='关联异常记录', on_delete=models.CASCADE)
    duration_sec = models.PositiveIntegerField('持续时长(s)', null=True, blank=True)
    note = models.TextField('处理备注', default='', blank=True)

    class Meta:
        unique_together = ('sflog', 'stlog')

class WmStateOption(models.IntegerChoices):
    OK = 10, _("合格")
    NOTOK = 20, _("不合格")
    REPAIR = 30, _("返修")
    REPAIRED = 34, _("返修完成")
    TEST = 40, _("检验")
    SCRAP = 50, _("报废")

class WMaterial(CommonBDModel):
    """
    TN: 车间库存

    归属范围分三级：mgroup 指定工段，只有 belong_dept 指定部门，
    两者都为空表示全局库存。
    """
    WM_OK = 10
    WM_NOTOK = 20
    WM_REPAIR = 30
    WM_REPAIRED = 34
    WM_TEST = 40
    WM_SCRAP = 50
    state = models.PositiveSmallIntegerField('状态', default=10, choices=WmStateOption.choices)
    material = models.ForeignKey(
        Material, verbose_name='物料', on_delete=models.CASCADE, related_name='wm_m')
    supplier = models.ForeignKey(Supplier, verbose_name='外协供应商', on_delete=models.SET_NULL, null=True, blank=True)
    mgroup = models.ForeignKey(Mgroup, verbose_name='所在工段', on_delete=models.CASCADE, null=True, blank=True)
    batch = models.TextField('批次号', db_index=True)
    count = models.DecimalField('当前数量', default=0, max_digits=11, decimal_places=1)
    count_eweight = models.FloatField('单数重量', default=0)
    defect = models.ForeignKey('qm.defect', verbose_name='缺陷', on_delete=models.SET_NULL, null=True, blank=True)
    notok_sign = models.CharField('不合格标记', max_length=10, null=True, blank=True)
    material_origin = models.ForeignKey(Material, verbose_name='原始物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mo')
    state_origin = models.PositiveSmallIntegerField('原始状态', choices=WmStateOption.choices, null=True, blank=True)
    count_xtest = models.DecimalField('已检数量', null=True, blank=True, max_digits=11, decimal_places=1)
    batch_ofrom = models.TextField('原料批次号', null=True, blank=True)
    material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='wm_mofrom')
    number_from = models.TextField("来源于个号", null=True, blank=True)
    is_manual = models.BooleanField('手动创建', default=False)

    INVENTORY_KEY_FIELDS = (
        'material',
        'batch',
        'mgroup',
        'belong_dept',
        'state',
        'defect',
        'notok_sign',
        'material_origin',
        'state_origin',
    )

    @classmethod
    def _normalize_inventory_lookup(cls, **kwargs):
        """生成唯一、完整的库存业务键，避免省略 NULL 字段产生不同锁键。"""
        unknown_fields = set(kwargs) - set(cls.INVENTORY_KEY_FIELDS)
        if unknown_fields:
            fields = ', '.join(sorted(unknown_fields))
            raise TypeError(f'不支持的车间库存定位字段: {fields}')

        if kwargs.get('material') is None or kwargs.get('batch') is None:
            raise ValueError('车间库存业务键必须包含 material 和 batch')

        lookup = {
            field: kwargs.get(field)
            for field in cls.INVENTORY_KEY_FIELDS
        }
        if lookup['state'] is None:
            lookup['state'] = cls._meta.get_field('state').get_default()

        mgroup = lookup['mgroup']
        belong_dept = lookup['belong_dept']
        if mgroup is not None:
            mgroup_dept_id = getattr(mgroup, 'belong_dept_id', None)
            belong_dept_id = getattr(belong_dept, 'pk', belong_dept)
            if belong_dept is None:
                lookup['belong_dept'] = mgroup.belong_dept
            elif mgroup_dept_id != belong_dept_id:
                raise ValueError('车间库存的工段与所属部门不匹配')

        return lookup

    @classmethod
    def _inventory_advisory_lock_payload(cls, lookup):
        lock_values = {}
        for name in cls.INVENTORY_KEY_FIELDS:
            field = cls._meta.get_field(name)
            value = lookup[name]
            if field.is_relation and value is not None:
                value = getattr(value, 'pk', value)
            lock_values[field.attname] = value
        return json.dumps(
            {'model': cls._meta.label_lower, 'lookup': lock_values},
            sort_keys=True,
            ensure_ascii=False,
            default=str,
            separators=(',', ':'),
        )

    @classmethod
    def locked_get_or_create_inventory(cls, defaults=None, **kwargs):
        """
        在事务中按完整库存业务键获取或创建记录。

        已存在记录使用行锁；首次创建使用 PostgreSQL 事务级 advisory lock，
        并在取得锁后重新查询，避免两个事务同时创建相同库存。
        """
        if not connection.in_atomic_block:
            raise RuntimeError(
                'locked_get_or_create_inventory 必须在事务中调用'
            )
        if connection.vendor != 'postgresql':
            raise RuntimeError(
                'locked_get_or_create_inventory 仅支持 PostgreSQL'
            )

        defaults = defaults or {}
        lookup = cls._normalize_inventory_lookup(**kwargs)
        create_defaults = {
            key: value
            for key, value in defaults.items()
            if key not in cls.INVENTORY_KEY_FIELDS
        }

        rows = list(
            cls.objects.select_for_update().filter(**lookup)[:2]
        )
        if len(rows) > 1:
            raise RuntimeError(
                f'{cls.__name__} 数据异常：库存业务键 {lookup} 命中多条'
            )
        if rows:
            return rows[0], False

        lock_payload = cls._inventory_advisory_lock_payload(lookup)
        with connection.cursor() as cursor:
            cursor.execute(
                'SELECT pg_advisory_xact_lock(hashtextextended(%s, 0))',
                [lock_payload],
            )

        rows = list(
            cls.objects.select_for_update().filter(**lookup)[:2]
        )
        if len(rows) > 1:
            raise RuntimeError(
                f'{cls.__name__} 数据异常：库存业务键 {lookup} 命中多条'
            )
        if rows:
            return rows[0], False

        return cls.objects.create(
            **lookup,
            **create_defaults,
        ), True

    def delete(self, *args, **kwargs):
        if not self.is_manual:
            raise ParseError('只能删除手动创建的车间库存')
        checks = [
            (self.mlogb_set.exists, '存在关联的生产明细'),
            (self.handoverb_wm.exists, '存在关联的交接明细'),
            (self.ftestwork_set.exists, '存在关联的检验记录'),
            (self.wm_mioitem.exists, '存在关联的出入库明细'),
        ]
        for check, msg in checks:
            if check():
                raise ParseError(msg)
        super().delete(*args, **kwargs)

    @property
    def belong_dept_or_mgroup_id(self):
        if self.mgroup_id:
            return ("mgroup", self.mgroup_id)
        if self.belong_dept_id:
            return ("belong_dept", self.belong_dept_id)
        return ("global", None)
        
    @property
    def count_working(self):
        return Mlogb.objects.filter(wm_in=self, mlog__submit_time__isnull=True).aggregate(count=Sum('count_use'))['count'] or 0

    @property
    def count_handovering(self): 
        return  Handoverb.objects.filter(wm=self, handover__submit_time__isnull=True).aggregate(count=Sum('count'))['count'] or 0

    @classmethod
    def ava_qs(cls, mgroup:Mgroup, qs=None):
        """
        可用的queryset
        """
        if qs is None:
            qs = cls.objects
        return qs.filter(Q(mgroup=mgroup)|Q(mgroup=None, belong_dept=mgroup.belong_dept)|Q(mgroup=None, belong_dept=None))
    
    @classmethod
    def mat_in_qs(cls, mtask: Mtask, qs=None):
        """
        TN: 可用于该子任务的queryset
        """
        if qs is None:
            qs = cls.objects
        return WMaterial.ava_qs(mgroup=mtask.mgroup, qs=qs).filter(
                material=mtask.material_in,
                batch__in=Subquery(
                    Mlogb.objects.filter(
                        mtask__utask=mtask.utask,
                        mlog__submit_time__isnull=False,
                        material_out=mtask.material_in
                    ).values_list('batch', flat=True)
                ),
                state__in=[WMaterial.WM_OK, WMaterial.WM_REPAIR]
            )

    @classmethod
    def increase(cls, wm_id: str, user:User, count, count_eweight=None):
        updates = {}
        if count:
            updates['count'] = F('count') + count
        if count_eweight:
            updates['count_eweight'] = count_eweight
        if not updates:
            return 0
        updates["update_by"] = user
        updates['update_time'] = timezone.now()
        return cls.objects.filter(id=wm_id).update(**updates)

    @classmethod
    def decrease(cls, wm_id: str, user:User, count):
        if not count:
            return 0
        updated = cls.objects.filter(id=wm_id, count__gte= count).update(
            count=F('count') - count, update_by=user, update_time=timezone.now())
        if updated == 0:
            batch = WMaterial.objects.get(id=wm_id).batch
            raise ParseError(f'{batch}_库存不足，无法完成扣减')
        return updated
    
class Fmlog(CommonADModel):
    """TN: 父级生产日志
    """
    mtask = models.ForeignKey(Mtask, verbose_name='任务', 
        on_delete=models.CASCADE, related_name='fmlog_mtask', null=True, blank=True)
    route = models.ForeignKey(Route, verbose_name="所用步骤", 
        on_delete=models.SET_NULL, related_name="fmlog_route", null=True, blank=True)
    mgroup = models.ForeignKey(Mgroup, verbose_name='工段', on_delete=models.CASCADE, related_name='fmlog_mgroup')
    note = models.TextField('备注', default='', blank=True)
    is_fix = models.BooleanField('是否用于返修', default=False) # 返工/复检
    enabled = models.BooleanField("是否启用", default=True)

class Mlog(CommonADModel):
    """
    TN: 生产/检验日志
    """
    # 变成父级的字段
    MLOG_2 = 10
    MLOG_23 = 20
    MLOG_12 = 30
    MTYPE_SELF = 10
    MTYPE_OUT = 20
    fmlog = models.ForeignKey(Fmlog, verbose_name='关联生产日志', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlog_fmlog')
    mtaskb  = models.ForeignKey(Mtaskb, verbose_name='关联个人任务', on_delete=models.CASCADE, related_name='mlog_mtaskb', null=True, blank=True)
    fill_way = models.PositiveSmallIntegerField("填写方式", default=10, help_text='10:仅二级;20:二三级;30:一二级')
    mtype = models.PositiveSmallIntegerField('生产类型', default=10, help_text='10:自生产;20:外协生产', choices=((10, '自生产'), (20, '外协生产')))
    is_fix = models.BooleanField('是否用于返修', default=False) # 返工/复检
    qct = models.ForeignKey("qm.qct", verbose_name='所用质检表', on_delete=models.SET_NULL, null=True, blank=True)
    supplier = models.ForeignKey(Supplier, verbose_name='外协供应商', on_delete=models.SET_NULL, null=True, blank=True)
    work_start_time = models.DateTimeField('生产开始时间', null=True, blank=True)
    work_end_time = models.DateTimeField('生产结束时间', null=True, blank=True)
    hour_work = models.FloatField('预计工时', null=True, blank=True)
    reminder_interval_list = models.JSONField('提醒间隔', default=list, blank=True)
    stored_scope = models.PositiveSmallIntegerField(
        '产出库存归属', choices=WmScope.choices, default=WmScope.DEPT)
    stored_notok = models.BooleanField('不合格品是否已入库', default=False)
    clear_defect = models.BooleanField('合格B类记为合格', default=False)
    route = models.ForeignKey(Route, verbose_name='生产路线', on_delete=models.SET_NULL, null=True, blank=True)
    mtask = models.ForeignKey(
        Mtask, verbose_name='关联任务', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_mtask')
    mgroup = models.ForeignKey(
        Mgroup, verbose_name='工段', on_delete=models.CASCADE, null=True, blank=True, db_index=True)
    wm_in = models.ForeignKey(WMaterial, verbose_name='投入物料所在库存', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlog_wm_in')
    material_in = models.ForeignKey(
        Material, verbose_name='消耗物', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_material_in')
    material_out = models.ForeignKey(
        Material, verbose_name='产物', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_material_out')
    equipment = models.ForeignKey(
        Equipment, verbose_name='生产设备', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_equipment')
    equipment_2 = models.ForeignKey(
        Equipment, verbose_name='生产设备2', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_equipment_2')
    equipments = models.ManyToManyField(
        Equipment, verbose_name='多选生产设备', blank=True)
    index = models.PositiveSmallIntegerField('第几锅', default=0)

    shift = models.ForeignKey(
        Shift, verbose_name='关联班次', on_delete=models.SET_NULL, null=True, blank=True)
    batch = models.TextField('批次号', null=True, blank=True, db_index=True)
    count_use = models.DecimalField('领用数', default=0, max_digits=11, decimal_places=1)
    count_real = models.DecimalField('实际生产数', default=0, max_digits=11, decimal_places=1)
    count_real_eweight = models.FloatField('单数重量', default=0)
    count_break = models.DecimalField('加工碎料数', default=0, max_digits=11, decimal_places=1)

    count_ok = models.DecimalField('合格数', default=0, max_digits=11, decimal_places=1)
    count_ok_full = models.DecimalField('完全合格数', null=True, blank=True, max_digits=11, decimal_places=1)
    count_notok = models.DecimalField('不合格数', default=0, max_digits=11, decimal_places=1)
    count_break_t = models.DecimalField('检验碎料数', default=0, max_digits=11, decimal_places=1)

    count_pn_jgqbl = models.DecimalField('加工前不良', default=0, max_digits=11, decimal_places=1)
    count_n_zw = models.DecimalField('炸纹', default=0, max_digits=11, decimal_places=1)
    count_n_tw = models.DecimalField('条纹', default=0, max_digits=11, decimal_places=1)
    count_n_qp = models.DecimalField('气泡', default=0, max_digits=11, decimal_places=1)
    count_n_wq = models.DecimalField('弯曲', default=0, max_digits=11, decimal_places=1)
    count_n_dl = models.DecimalField('断裂', default=0, max_digits=11, decimal_places=1)
    count_n_pb = models.DecimalField('偏壁', default=0, max_digits=11, decimal_places=1)
    count_n_dxt = models.DecimalField('大小头', default=0, max_digits=11, decimal_places=1)
    count_n_js = models.DecimalField('结石', default=0, max_digits=11, decimal_places=1)
    count_n_qx = models.DecimalField('气线', default=0, max_digits=11, decimal_places=1)
    count_n_hs = models.DecimalField('划伤', default=0, max_digits=11, decimal_places=1)
    count_n_cs = models.DecimalField('挫伤', default=0, max_digits=11, decimal_places=1)
    count_n_bl = models.DecimalField('不亮', default=0, max_digits=11, decimal_places=1)
    count_n_zz = models.DecimalField('杂质', default=0, max_digits=11, decimal_places=1)
    count_n_d = models.DecimalField('短', default=0, max_digits=11, decimal_places=1)
    count_n_zdd = models.DecimalField('锥度大', default=0, max_digits=11, decimal_places=1)
    count_n_hw = models.DecimalField('横纹', default=0, max_digits=11, decimal_places=1)
    count_n_yp = models.DecimalField('有皮', default=0, max_digits=11, decimal_places=1)
    count_n_bp = models.DecimalField('爆皮', default=0, max_digits=11, decimal_places=1)
    count_n_sc = models.DecimalField('色差', default=0, max_digits=11, decimal_places=1)
    count_n_tydd = models.DecimalField('椭圆度大', default=0, max_digits=11, decimal_places=1)
    count_n_sw = models.DecimalField('水雾', default=0, max_digits=11, decimal_places=1)
    count_n_zjx = models.DecimalField('直径小', default=0, max_digits=11, decimal_places=1)
    count_n_zjd = models.DecimalField('直径大', default=0, max_digits=11, decimal_places=1)
    count_n_bhpcd = models.DecimalField('壁厚偏差大', default=0, max_digits=11, decimal_places=1)
    count_n_xzp = models.DecimalField('箱中破', default=0, max_digits=11, decimal_places=1)
    count_n_thhs = models.DecimalField('退火后碎', default=0, max_digits=11, decimal_places=1)
    count_n_swen = models.DecimalField('水纹', default=0, max_digits=11, decimal_places=1)
    count_n_bb = models.DecimalField('崩边', default=0, max_digits=11, decimal_places=1)
    count_n_xbb = models.DecimalField('小崩边', default=0, max_digits=11, decimal_places=1)
    count_n_wm = models.DecimalField('雾面', default=0, max_digits=11, decimal_places=1)
    count_n_md = models.DecimalField('麻点', default=0, max_digits=11, decimal_places=1)
    count_n_xh = models.DecimalField('线痕', default=0, max_digits=11, decimal_places=1)
    count_n_ps = models.DecimalField('破损', default=0, max_digits=11, decimal_places=1)
    count_n_wj = models.DecimalField('外经', default=0, max_digits=11, decimal_places=1)
    count_n_yd = models.DecimalField('圆度', default=0, max_digits=11, decimal_places=1)
    count_n_txd = models.DecimalField('同心度', default=0, max_digits=11, decimal_places=1)
    count_n_hd = models.DecimalField('厚度', default=0, max_digits=11, decimal_places=1)
    count_n_zt = models.DecimalField('炸头', default=0, max_digits=11, decimal_places=1)
    count_n_b = models.DecimalField('扁', default=0, max_digits=11, decimal_places=1)   # 光芯七车间
    count_n_qt = models.DecimalField('其他', default=0, max_digits=11, decimal_places=1)

    handle_date = models.DateField('操作日期', null=True, blank=True, db_index=True)
    team = models.ForeignKey(Team, verbose_name='班组', on_delete=models.SET_NULL, null=True, blank=True)
    handle_user = models.ForeignKey(
        User, verbose_name='操作人', on_delete=models.CASCADE, related_name='mlog_handle_user', null=True, blank=True, db_index=True)  # 成型人
    handle_user_2 = models.ForeignKey(
        User, verbose_name='操作人2', on_delete=models.CASCADE, related_name='mlog_handle_user_2', null=True, blank=True)  # 切料人
    handle_users = models.ManyToManyField(
        User, verbose_name='操作人(多选)', blank=True)
    handle_leader = models.ForeignKey(
        User, verbose_name='班长', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_handle_leader')
    note = models.TextField('备注', default='', blank=True)
    material_outs = models.ManyToManyField(
        Material, verbose_name='多个产出', blank=True, through='wpm.mlogb', related_name='mlog_material_outs', through_fields=('mlog', 'material_out'))

    submit_time = models.DateTimeField('提交时间', null=True, blank=True)
    submit_user = models.ForeignKey(
        User, verbose_name='提交人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_submit_user')

    oinfo_json = models.JSONField('其他信息', default=dict, blank=True)
    ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
                               on_delete=models.SET_NULL, related_name='mlog_ticket', null=True, blank=True, db_constraint=False)
    test_file = models.TextField('检验文件', null=True, blank=True)
    test_user = models.ForeignKey(
        User, verbose_name='检验人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_test_user')
    test_time = models.DateTimeField('检验时间', null=True, blank=True)
    create_time = models.DateTimeField(
        default=timezone.now, verbose_name='创建时间', help_text='创建时间', db_index=True)
    
    @property
    def mlogb(self):
        return Mlogb.objects.filter(mlog=self).exclude(material_out=None)
    
    @property
    def mlogb_full(self):
        return Mlogb.objects.filter(mlog=self)
    
    @property
    def audit_ignore_fields(self):
        return ['create_by', 'update_by',
                'create_time', 'update_time', 'id']

    @property
    def mlogdefect(self):
        return MlogbDefect.objects.filter(mlogb__mlog=self, mlogb__material_out__isnull=False)
    
    @property
    def mlogindefect(self):
        return MlogbDefect.objects.filter(mlogb__mlog=self, mlogb__material_in__isnull=False)

    @classmethod
    def count_fields(cls, without_count_ok_full=True):
        mlog_count_fields = []
        for f in Mlog._meta.fields:
            if f.name.startswith("count"):
                mlog_count_fields.append(f.name)
        mlog_count_fields.remove("count_real_eweight")
        if without_count_ok_full:
            mlog_count_fields.remove("count_ok_full")
        return mlog_count_fields
    
    def cal_mlog_count_from_mlogb(self):
        """
        通过mlogb计算mlog count 合计
        """
        mlog = self
        if mlog.fill_way in [Mlog.MLOG_23, Mlog.MLOG_12]:
            a_dict = {
                "total_count_use": Sum('count_use'),
                "total_count_break": Sum('count_break'),
                "total_count_break_t": Sum('count_break_t'),
                "total_count_real": Sum('count_real'),
                "total_count_ok": Sum('count_ok'),
                "total_count_ok_full": Sum('count_ok_full'),
                "total_count_notok": Sum('count_notok'),
            }
            f_names = [f.name for f in Mlogb._meta.fields if 'count_n_' in f.name]
            for f in f_names:
                a_dict[f'total_{f}'] = Sum(f)
            mlogb_summary = Mlogb.objects.filter(mlog=mlog, need_inout=True).aggregate(
                **a_dict
            )
            # 更新Mlog对象的相应字段
            mlog.count_use = mlogb_summary['total_count_use'] or 0
            mlog.count_break = mlogb_summary['total_count_break'] or 0
            mlog.count_break_t = mlogb_summary['total_count_break_t'] or 0
            mlog.count_real = mlogb_summary['total_count_real'] or 0
            mlog.count_ok = mlogb_summary['total_count_ok'] or 0
            mlog.count_ok_full = mlogb_summary['total_count_ok_full'] or 0
            mlog.count_notok = mlogb_summary['total_count_notok'] or 0
            for f in f_names:
                setattr(mlog, f, mlogb_summary[f'total_{f}'] or 0)
            # 保存更新后的Mlog对象
            mlog.save()

class MlogUser(BaseModel):
    """TN: 子级生产/检验日志操作人"""
    mlog = models.ForeignKey(Mlog, verbose_name='关联日志', on_delete=models.CASCADE)
    handle_user = models.ForeignKey(User, verbose_name='操作人', on_delete=models.CASCADE)
    process = models.ForeignKey(Process, verbose_name='子工序', on_delete=models.CASCADE)
    work_start_time = models.DateTimeField('生产开始时间', null=True, blank=True)
    equipment = models.ForeignKey(
        Equipment, verbose_name='生产设备', on_delete=models.CASCADE, null=True, blank=True, related_name='mloguser_equipment')
    shift = models.ForeignKey(Shift, verbose_name='关联班次', on_delete=models.CASCADE)
    handle_date = models.DateField('操作日期')

class Mlogb(BaseModel):
    """
    TN: 子级生产/检验日志
    """
    qct = models.ForeignKey("qm.qct", verbose_name='所用质检表', on_delete=models.SET_NULL, null=True, blank=True)
    mlog = models.ForeignKey(Mlog, verbose_name='关联日志',
                             on_delete=models.CASCADE, related_name='b_mlog')
    note = models.TextField('备注', default='', blank=True)
    batch = models.TextField('批次号', null=True, blank=True, db_index=True)
    mtask = models.ForeignKey(Mtask, verbose_name='关联任务',
                              on_delete=models.CASCADE, related_name='mlogb_mtask', null=True, blank=True)
    route = models.ForeignKey(Route, verbose_name='生产步骤', on_delete=models.SET_NULL, null=True, blank=True)
    wm_in = models.ForeignKey(WMaterial, verbose_name='投入物料所在库存', 
    on_delete=models.SET_NULL, null=True, blank=True)
    material_in = models.ForeignKey(
        Material, verbose_name='投入物料', on_delete=models.CASCADE, related_name='mlogb_material_in',
        null=True, blank=True)

    parent = models.ForeignKey("self", verbose_name='父级物料', on_delete=models.CASCADE, null=True, blank=True,
                               related_name='mlogb_parent')
    mlogb_from = models.ForeignKey("self", verbose_name='来源批', on_delete=models.CASCADE, null=True, blank=True,
                                related_name='mlogb_from_mlogb')
    mlogbw_from = models.ForeignKey("wpm.mlogbw", verbose_name='来源个', on_delete=models.CASCADE, null=True, blank=True,
                                related_name='mlogb_from_mlogb')
    material_out = models.ForeignKey(
        Material, verbose_name='产物', on_delete=models.CASCADE, related_name='mlogb_material_out',
        null=True, blank=True)

    batch_ofrom = models.TextField('原料批次号', null=True, blank=True)
    material_ofrom = models.ForeignKey(Material, verbose_name='原料物料', on_delete=models.SET_NULL,
                                       null=True, blank=True, related_name='mlogb_mofrom')

    count_use = models.DecimalField('领用数量', default=0, max_digits=11, decimal_places=1)
    count_break = models.DecimalField('加工破碎数', default=0, max_digits=11, decimal_places=1)

    count_break_t = models.DecimalField('检验碎料数', default=0, max_digits=11, decimal_places=1)
    count_real = models.DecimalField('实际生产数', default=0, max_digits=11, decimal_places=1)
    count_ok = models.DecimalField('合格数量', default=0, max_digits=11, decimal_places=1)
    count_ok_full = models.DecimalField('完全合格数', null=True, blank=True, max_digits=11, decimal_places=1)
    count_notok = models.DecimalField('不合格数', default=0, max_digits=11, decimal_places=1)

    count_pn_jgqbl = models.DecimalField('加工前不良', default=0, max_digits=11, decimal_places=1)
    # 添加不合格字段后需要更改cal_mlog_count_from_mlogb
    count_n_hs = models.DecimalField('划伤', default=0, max_digits=11, decimal_places=1)
    count_n_qp = models.DecimalField('气泡', default=0, max_digits=11, decimal_places=1)
    count_n_swen = models.DecimalField('水纹', default=0, max_digits=11, decimal_places=1)
    count_n_bb = models.DecimalField('崩边', default=0, max_digits=11, decimal_places=1)
    count_n_xbb = models.DecimalField('小崩边', default=0, max_digits=11, decimal_places=1)
    count_n_wm = models.DecimalField('雾面', default=0, max_digits=11, decimal_places=1)
    count_n_md = models.DecimalField('麻点', default=0, max_digits=11, decimal_places=1)
    count_n_xh = models.DecimalField('线痕', default=0, max_digits=11, decimal_places=1)
    count_n_ps = models.DecimalField('破损', default=0, max_digits=11, decimal_places=1)
    count_n_wj = models.DecimalField('外经', default=0, max_digits=11, decimal_places=1)
    count_n_yd = models.DecimalField('圆度', default=0, max_digits=11, decimal_places=1)
    count_n_txd = models.DecimalField('同心度', default=0, max_digits=11, decimal_places=1)
    count_n_hd = models.DecimalField('厚度', default=0, max_digits=11, decimal_places=1)
    count_n_zt = models.DecimalField('炸头', default=0, max_digits=11, decimal_places=1)
    count_n_qt = models.DecimalField('其他', default=0, max_digits=11, decimal_places=1)
    count_notok_json = models.JSONField('不合格情况', default=list, blank=True)
    count_json = models.JSONField('各物料数量', default=list, blank=True)
    count_json_from = models.JSONField("层布局数据", default=list, blank=True)
    count_json_wx = models.JSONField('外协自定义数据', default=list, blank=True)
    test_user = models.ForeignKey(
        User, verbose_name='抽检人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlogb_test_user')
    need_inout = models.BooleanField('是否需要出入库', default=True)
    number_from = models.TextField('来源个编号', null=True, blank=True)

    class Meta:
        indexes = [
            models.Index(
                fields=["material_out"],
                name="mlogb_mat_out_not_null",
                condition=Q(material_out__isnull=False),
            ),
            models.Index(
                fields=["material_in"],
                name="mlogb_mat_in_not_null",
                condition=Q(material_in__isnull=False),
            ),
        ]


    def get_tracking(self):
        if self.material_in:
            return "in", self.material_in.tracking
        elif self.material_out:
            return "out", self.material_out.tracking
    
    @property
    def mlogbdefect(self):
        return self.mlogbdefect_set.all()

    def cal_count_pn_jgqbl(self, cal_mlog=False):
        mqs = MlogbDefect.get_defect_qs_from_mlogb(self, ftype="in")
        count_pn_jgqbl = mqs.aggregate(total=Sum("count"))["total"] or 0
        self.count_pn_jgqbl = count_pn_jgqbl
        self.save(update_fields=["count_pn_jgqbl"])
        mlog = self.mlog
        if mlog and cal_mlog:
            mlog.cal_mlog_count_from_mlogb()

    def cal_count_notok(self, cal_mlog=True):
        mqs = MlogbDefect.get_defect_qs_from_mlogb(self, ftype="out")
        count_notok = mqs.filter(defect__okcate=30).aggregate(total=Sum("count"))["total"] or 0
        count_notok_full = mqs.exclude(defect__okcate=10).aggregate(total=Sum("count"))["total"] or 0
        self.count_notok = count_notok
        self.count_ok = self.count_real - count_notok
        self.count_ok_full = self.count_real - count_notok_full
        if self.count_ok_full < 0:
            raise ParseError(f"完全合格数不能小于0:{self.count_real}-{self.count_ok}-{self.count_notok}-{self.count_ok_full}")
        self.save(update_fields=["count_ok", "count_notok", "count_ok_full"])
        mlog = self.mlog
        if mlog and cal_mlog:
            mlog.cal_mlog_count_from_mlogb()

    def get_default_inherited_defect(self):
        if self.material_out is None or self.material_out.tracking != Material.MA_TRACKING_BATCH:
            return None
        if self.mlog_id and self.mlog and self.mlog.is_fix:
            return None
        if self.mlogb_from_id and self.mlogb_from and self.mlogb_from.wm_in_id:
            return self.mlogb_from.wm_in.defect
        if self.wm_in_id and self.wm_in:
            return self.wm_in.defect
        if self.mlog and self.mlog.wm_in_id:
            return self.mlog.wm_in.defect
        return None

    def has_legacy_defect_count(self):
        return any(getattr(self, f.name) > 0 for f in Mlogb._meta.fields if 'count_n_' in f.name)

    def sync_inherited_defect(self, cal_count=True):
        inherited_qs = MlogbDefect.objects.filter(mlogb=self, is_inherited=True)
        had_inherited = inherited_qs.exists()
        if MlogbDefect.objects.filter(mlogb=self, is_inherited=False).exists() or self.has_legacy_defect_count():
            inherited_qs.delete()
            if had_inherited and cal_count:
                self.cal_count_notok(cal_mlog=False)
            return

        defect = self.get_default_inherited_defect()
        if defect is None:
            inherited_qs.delete()
            if had_inherited and cal_count:
                self.cal_count_notok(cal_mlog=False)
            return

        count = self.count_real
        inherited, _ = MlogbDefect.objects.get_or_create(
            mlogb=self,
            defect=defect,
            is_inherited=True,
        )
        inherited.count = count
        inherited.count_has = count
        inherited.save(update_fields=["count", "count_has"])
        inherited_qs.exclude(id=inherited.id).delete()
        if cal_count:
            self.cal_count_notok(cal_mlog=False)

class MlogbDefect(BaseModel):
    """TN: 生成记录的缺陷记录"""
    mlogb = models.ForeignKey(Mlogb, verbose_name='生产记录', on_delete=models.CASCADE)
    defect = models.ForeignKey("qm.Defect", verbose_name='缺陷', on_delete=models.CASCADE, null=True, blank=True)
    count = models.DecimalField('数量', default=0, max_digits=11, decimal_places=1)
    count_has = models.DecimalField('含有该缺陷的数量', default=0, max_digits=11, decimal_places=1)
    is_inherited = models.BooleanField("是否继承", default=False)

    @classmethod
    def get_defect_qs(cls, ftype="all"):
        qs = MlogbDefect.objects.filter(defect__isnull=False)
        if ftype == "in":
            qs = qs.filter(mlogb__material_in__isnull=False)
        elif ftype == "out":
            qs = qs.filter(mlogb__material_out__isnull=False)
        return qs

    @classmethod
    def get_defect_qs_from_mlogb(cls, mlogb:Mlogb, ftype="all"):
        return cls.get_defect_qs(ftype).filter(mlogb=mlogb)

    @classmethod
    def get_defect_qs_from_mlog(cls, mlog:Mlog, ftype="all"):
        return cls.get_defect_qs(ftype).filter(mlogb__mlog=mlog)

class Mlogbw(BaseModel):
    """TN: 单个产品生产/检验日志
    """
    number = models.TextField('单个编号', db_index=True)
    mlogb = models.ForeignKey(Mlogb, verbose_name='生产记录', on_delete=models.CASCADE, related_name="w_mlogb")
    mlogbw_from = models.ForeignKey("self", verbose_name='来源个', on_delete=models.CASCADE, null=True, blank=True, related_name="w_mlogbw_from")
    wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL
                            , related_name='wpr_mlogbw', null=True, blank=True)
    equip = models.ForeignKey(Equipment, verbose_name='设备', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlogbw_equip')
    tooling = models.ForeignKey(Equipment, verbose_name='工装', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlogbw_tooling')
    work_start_time = models.DateTimeField('开始加工时间', null=True, blank=True)
    work_end_time = models.DateTimeField('结束加工时间', null=True, blank=True)
    ftest = models.OneToOneField("qm.ftest", verbose_name='关联检验',
                                 on_delete=models.PROTECT, null=True, blank=True, related_name="mlogbw_ftest")
    note = models.TextField('备注', null=True, blank=True)

    @classmethod
    def cal_count_notok(cls, mlogb: Mlogb):
        from apps.qm.models import Defect, FtestDefect
        # 锁定mlogb以防止并发修改
        # mlogb:Mlogb = Mlogb.objects.select_for_update().get(pk=mlogb.pk)
        count = Mlogbw.objects.filter(mlogb=mlogb).count()
        if mlogb.material_in:
            mlogb.count_use = count
            mlogb.save(update_fields=["count_use"])
        elif mlogb.material_out:
            mlogb.count_real = count
            count_notok = 0
            count_notok_full = 0
            # 个追踪的合格b类不分开，这里会导致count_ok_full与count_ok一样了，暂时不做处理
            tqs = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False)
            tqs_a = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False).values("ftest__defect_main").annotate(xcount=Count('id'))
            defect_ids = tqs.values_list("ftest__defect_main", flat=True)
            tqs_has_a = FtestDefect.objects.filter(ftest__mlogbw_ftest__mlogb=mlogb, has=True).values("defect").annotate(xcount=Count('id'))
            defect_ids = defect_ids.union(tqs_has_a.values_list("defect", flat=True))
            defects = {defect.id: defect for defect in Defect.objects.filter(id__in=defect_ids)}
            md_ids = []
            MlogbDefect.objects.filter(mlogb=mlogb).delete()
            for t in tqs_a:
                md, _ = MlogbDefect.objects.get_or_create(mlogb=mlogb, defect=defects[t["ftest__defect_main"]])
                md.count = t["xcount"]
                md.save()
                md_ids.append(md.id)
                count_notok += t["xcount"]
                if defects[t["ftest__defect_main"]].okcate != 10:
                    count_notok_full += t["xcount"]
            
            for t2 in tqs_has_a:
                md, _ = MlogbDefect.objects.get_or_create(mlogb=mlogb, defect=defects[t2["defect"]], defaults={"count": 0})
                md.count_has = t2["xcount"]
                md.save()
                md_ids.append(md.id)

            mlogb.count_notok = count_notok
            mlogb.count_ok = count - mlogb.count_notok
            mlogb.count_ok_full = count - count_notok_full
            mlogb.save()

class Handover(CommonADModel):
    """
    TN: 交接记录
    """
    H_NORMAL = 10
    H_REPAIR = 20
    H_TEST = 30
    H_SCRAP = 40
    H_CHANGE = 50
    H_BACK = 60

    H_MERGE = 30
    H_DIV = 20
    new_batch = models.TextField('新批次号', null=True, blank=True, db_index=True)
    new_wm = models.ForeignKey(WMaterial, on_delete=models.SET_NULL, null=True, blank=True)
    clear_defect = models.BooleanField('合批清除缺陷', default=False)
    mtype = models.PositiveSmallIntegerField("合并类型", default=H_NORMAL, choices=
                                             [(H_NORMAL, '正常'), (H_DIV, '分批'), (H_MERGE, '合批')])
    type = models.PositiveSmallIntegerField('交接类型', choices=[
        (H_NORMAL, '正常交接'), (H_REPAIR, '返修交接'), 
        (H_TEST, '检验交接'), (H_SCRAP, '报废交接'), (H_CHANGE, '改版交接'), (H_BACK, '退料交接')], default=H_NORMAL)
    send_date = models.DateField('送料日期')
    send_user = models.ForeignKey(
        User, verbose_name='交送人', on_delete=models.CASCADE, related_name='handover_send_user')
    send_mgroup = models.ForeignKey(
        Mgroup, verbose_name='送料工段', on_delete=models.CASCADE, null=True, blank=True)
    send_dept = models.ForeignKey(
        Dept, verbose_name='送料部门', on_delete=models.CASCADE, related_name='handover_send_dept',
        null=True, blank=True)
    batch = models.TextField('批次号', null=True, blank=True, db_index=True)
    material = models.ForeignKey(
        Material, verbose_name='物料', on_delete=models.CASCADE, related_name='h_ma')
    material_changed = models.ForeignKey(Material, verbose_name='变更后物料', on_delete=models.CASCADE, null=True, blank=True, related_name='h_ma_c')
    state_changed = models.PositiveSmallIntegerField('变更后状态', choices=WmStateOption.choices, default=10, null=True, blank=True)
    count = models.DecimalField('送料数', default=0, max_digits=11, decimal_places=1)
    count_eweight = models.FloatField('单数重量', default=0)
    recive_dept = models.ForeignKey(
        Dept, verbose_name='接收部门', on_delete=models.CASCADE, related_name='handover_recive_dept',
        null=True, blank=True)
    recive_mgroup = models.ForeignKey(Mgroup, verbose_name='接收工段', on_delete=models.CASCADE, related_name='handover_recive_mgroup', null=True, blank=True)
    recive_user = models.ForeignKey(
        User, verbose_name='接收人', on_delete=models.CASCADE, related_name='handover_recive_user', null=True, blank=True)
    wm = models.ForeignKey(WMaterial, verbose_name='关联车间库存', on_delete=models.SET_NULL,
                           null=True, blank=True, related_name='handover_wm')
    mlog = models.ForeignKey(Mlog, verbose_name='关联日志记录',
                             on_delete=models.SET_NULL, null=True, blank=True, related_name='handover_mlog')
    doin_date = models.DateField('加料日期', null=True, blank=True)
    doout_date = models.DateField('出料日期', null=True, blank=True)

    submit_time = models.DateTimeField('提交时间', null=True, blank=True)
    submit_user = models.ForeignKey(
        User, verbose_name='提交人', on_delete=models.CASCADE, null=True, blank=True, related_name='handover_submit_user')
    note = models.TextField('备注', null=True, blank=True)
    oinfo_json = models.JSONField('其他信息', default=dict, blank=True)
    ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
                               on_delete=models.SET_NULL, related_name='handover_ticket', null=True, blank=True, db_constraint=False)

    @property
    def handoverb(self):
        # 用反向关联管理器, 以命中 list 接口的 Prefetch("b_handover") 缓存, 避免 N+1
        return self.b_handover.all()

class Handoverb(BaseModel):
    """TN: 子级交接记录
    """
    handover = models.ForeignKey(Handover, verbose_name='关联交接记录', on_delete=models.CASCADE, related_name="b_handover")
    batch = models.TextField("批次号", null=True, blank=True, db_index=True)
    wm = models.ForeignKey(WMaterial, verbose_name='关联车间库存', on_delete=models.SET_NULL,
                           null=True, blank=True, related_name='handoverb_wm')
    wm_to = models.ForeignKey(WMaterial, verbose_name='所到车间库存', on_delete=models.SET_NULL,
                           null=True, blank=True, related_name='handoverb_wm_to')
    count = models.DecimalField('送料数', default=0, max_digits=11, decimal_places=1)
    oinfo_json = models.JSONField('其他信息', default=dict, blank=True)

    @property
    def handoverbw(self):
        return Handoverbw.objects.filter(handoverb=self)

class Handoverbw(BaseModel):
    """TN: 单个产品交接记录
    """
    handoverb = models.ForeignKey(Handoverb, verbose_name='关联交接记录', on_delete=models.CASCADE, related_name="w_handoverb")
    number = models.TextField('单个编号')
    wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.CASCADE
                            , related_name='wpr_handoverbw', null=True, blank=True)
    note = models.TextField('备注', null=True, blank=True)

class AttLog(CommonADModel):
    """
    TN: 到岗记录
    """
    ATT_STATE_CHOICES = [
        ('pending', '待定'),
        ('normal', '正常'),
        ('late', '迟到'),
        ('early_leave', '早退'),
        ('absent', '未到岗'),
        ('leave', '请假'),
        # 可以根据需要添加更多状态
    ]
    sflog = models.ForeignKey(
        SfLog, verbose_name='关联值班记录', on_delete=models.CASCADE)
    user = models.ForeignKey(
        'system.user', verbose_name='到岗人', on_delete=models.CASCADE)
    post = models.ForeignKey(
        'system.post', verbose_name='岗位', on_delete=models.CASCADE)
    state = models.CharField('状态', max_length=20,
                             choices=ATT_STATE_CHOICES, default='pending', help_text=str(ATT_STATE_CHOICES))
    note = models.TextField('备注信息', default='', blank=True)


class OtherLog(CommonADModel):
    """
    TN: 其他生产日志
    """
    product = models.CharField('产品', max_length=10)
    handle_date = models.DateField('操作日期')
    count_real = models.PositiveIntegerField('实际生产数', default=0)
    count_ok = models.PositiveIntegerField('合格数', default=0)
    count_delivered = models.PositiveIntegerField('交付数', default=0)

class BatchSt(BaseModel):
    """
    TN: 产品批次统计
    """
    batch = models.TextField("批次号", db_index=True)
    version = models.IntegerField("版本号", default=1, db_index=True)
    zt_batch = models.TextField("直通统计大批号", null=True, blank=True, db_index=True)  # 锁定该批归属的大批(拆出检验小批的批), 由批次统计脚本维护
    first_time = models.DateTimeField("首次操作时间", null=True, blank=True)
    last_time = models.DateTimeField("最后操作时间", null=True, blank=True)
    data = models.JSONField("数据", default=dict, blank=True)
    material_start = models.ForeignKey(Material, verbose_name="起始物料", on_delete=models.SET_NULL, null=True, blank=True)
    mio = models.ForeignKey("inm.mio", verbose_name="由何出入库记录创建", on_delete=models.CASCADE, null=True, blank=True)
    mioitem = models.ForeignKey("inm.mioitem", verbose_name="由何出入库记录明细创建", on_delete=models.CASCADE, null=True, blank=True)
    handover = models.ForeignKey(Handover, verbose_name='由何交接记录创建', on_delete=models.CASCADE, null=True, blank=True)
    mlog = models.ForeignKey(Mlog, verbose_name='由何日志创建', on_delete=models.CASCADE, null=True, blank=True)

    class Meta:
        unique_together = [("batch", "version")]

    @classmethod
    def g_create(cls, batch:str, mio=None, mioitem=None, handover=None, mlog=None, material_start=None, check_mat_start=False, exclude_batchst_ids=[], check_batch_exist=False):
        """
        创建新的批次
        """
        if mioitem:
            mio = mioitem.mio
        if check_batch_exist:
            if cls.objects.filter(batch=batch).exclude(id__in=exclude_batchst_ids).exists():
                raise ParseError(f"{batch}-该批次号已存在不可使用")
        node, created = cls.safe_get_or_create(batch=batch, version=1, defaults={
            "mio":mio, "mioitem":mioitem, "handover":handover, "mlog":mlog, "material_start":material_start})
        if not created and check_mat_start:
            if node.material_start is None:
                node.material_start = material_start
                node.save(update_fields = ["material_start"])
            if node.material_start != material_start:
                raise ParseError(f"{batch}-该批次号因物料不同不可引用")
        return node, created

        # if mio is None and handover is None and mlog is None:
        #     try:
        #         node = cls.objects.exclude(id__in=exclude_batchst_ids).get(batch=batch)
        #     except cls.DoesNotExist:
        #         return cls.objects.create(batch=batch), True
        #     except cls.MultipleObjectsReturned:
        #         # 兼容性处理
        #         node = cls.objects.filter(batch=batch).exclude(id__in=exclude_batchst_ids).order_by('-version').first()
        #         if node is None:
        #             raise ParseError(f"{node.batch}-该批次号本次操作不可引用")
        #     return node, False
        # else:
        #     version = 1
        #     if mio is None and handover is None and mlog is None:
        #         raise ParseError("mio or handover or mlog must be provided")
        #     # 带有来源的批次获取，需检查批次号是否可用
        #     cls_qs = cls.objects.filter(batch=batch)
        #     if cls_qs.exists():
        #         if reuse_node:
        #             node:BatchSt = (cls_qs.filter(mio__isnull=False)|cls_qs.filter(handover=None, mio=None, mlog=None)).order_by('-version').first()
        #             if node is None:
        #                 raise ParseError(f"{batch}-该批次号因物料不同不可引用")
        #             elif node.material_start is None:
        #                 node.material_start = material_start
        #                 node.save(update_fields = ["material_start"])
        #             # elif node.material_start is not None and node.material_start != material_start:
        #             #     raise ParseError(f"{batch}-该批次号因物料不同不可引用-{str(node.material_start)} vs {str(material_start)}")
        #             return node, False
        #         else:
        #             latest_version = BatchSt.objects.filter(batch=batch).aggregate(Max("version"))["version__max"]
        #             version = latest_version + 1
        #     ins = cls.objects.create(batch=batch, mio=mio, mioitem=mioitem, handover=handover, mlog=mlog, material_start=material_start, version=version)
        #     return ins, True
    
    @classmethod
    def init_dag(cls, batch:str):
        """
        更新批次数据关系链(初步)
        """
        ins, _ = cls.g_create(batch)
        if ins.mio is None and ins.handover is None and ins.mlog is None:
            from apps.inm.models import MIOItem
            mioitem = MIOItem.objects.filter(batch=batch, mio__submit_time__isnull=False).order_by('mio__submit_time').first()
            handover = Handover.objects.filter(mtype=20, b_handover__batch=batch, submit_time__isnull=False).order_by('submit_time').first() # 拆分
            handover2 = Handover.objects.filter(mtype=10, b_handover__batch=batch, submit_time__isnull=False).order_by('submit_time').first() # 合并
        return ins

def custom_key(s):
        match = re.search(r'(\d+)(?!.*\d)', s)  # 匹配最后一个数字
        return int(match.group(1)) if match else float('inf')
 
class BatchLog(BaseModel):
    """
    TN: 拆合批变更记录
    """
    source = models.ForeignKey(BatchSt, verbose_name='来源批次', on_delete=models.CASCADE, related_name="batch_s")
    target = models.ForeignKey(BatchSt, verbose_name='目标批次', on_delete=models.CASCADE, related_name="batch_t")
    handover = models.ForeignKey(Handover, verbose_name='关联交接记录', on_delete=models.CASCADE, null=True, blank=True)
    mlog = models.ForeignKey(Mlog, verbose_name='关联生产记录', on_delete=models.CASCADE, null=True, blank=True)
    relation_type = models.CharField('关联类型', max_length=20, help_text="split/merge", default="split")

    @classmethod
    def g_create(cls, source:str, target:str=None, relation_type="split", handover=None, mlog=None):
        """
        创建新的关系
        """
        if relation_type not in ["split", "merge"]:
            raise ParseError("relation_type must be split or merge")
        if handover is None and mlog is None:
            raise ParseError("handover or mlog must be provided")
        return cls.objects.get_or_create(source=source, target=target, handover=handover, mlog=mlog,
                                         defaults={"relation_type": relation_type})

    @classmethod
    def clear(cls, handover=None, mlog=None, mio=None, mioitem=None):
        if handover:
            logs = cls.objects.filter(handover=handover)
            for log in logs:
                source = log.source
                target = log.target
                log.delete()
                if not BatchLog.objects.filter(Q(source=source) | Q(target=source)).exists():
                    if source.mio is None and source.mioitem is None:
                        source.delete()
                if not BatchLog.objects.filter(Q(source=target) | Q(target=target)).exists():
                    if target.mio is None and target.mioitem is None:
                        target.delete()
        if mlog:
            logs = cls.objects.filter(mlog=mlog)
            for log in logs:
                source = log.source
                target = log.target
                log.delete()
                if not BatchLog.objects.filter(Q(source=source) | Q(target=source)).exists():
                    if source.mio is None and source.mioitem is None:
                        source.delete()
                if not BatchLog.objects.filter(Q(source=target) | Q(target=target)).exists():
                    if target.mio is None and target.mioitem is None:
                        target.delete()
        if mio:
            ts = BatchSt.objects.filter(mio=mio)
            for t in ts:
                if not BatchLog.objects.filter(Q(source=t) | Q(target=t)).exists():
                    t.delete()
        if mioitem:
            ts = BatchSt.objects.filter(mioitem=mioitem)
            for t in ts:
                if not BatchLog.objects.filter(Q(source=t) | Q(target=t)).exists():
                    t.delete()

    @classmethod
    def batches_to(cls, batch:str):
        query = """
        SELECT
            batch,
            CAST(substring(batch FROM LENGTH(%s) + 2) AS INTEGER) AS batch_num
        FROM wpm_batchst
        WHERE batch LIKE %s  AND translate(
        substring(batch FROM LENGTH(%s) + 2),
        '0123456789',
        ''
      ) = ''
        ORDER BY batch_num DESC
    """

        prefix = batch
        params = (
            prefix,
            f"{prefix}-%",
            prefix
        )

        try:
            rows = query_all_dict(query, params=params)
        except Exception as e:
            myLogger.error(f"BatchLog.batches_to error: {(str(e), query, params)}")
            raise

        if not rows:
            return {
                "batches": [],
                "last_batch_num": None,
                "last_batch": None,
            }

        batches = [r["batch"] for r in rows]
        last = rows[0]

        return {
            "batches": batches,
            "last_batch_num": last["batch_num"],
            "last_batch": last["batch"],
        }
                    
        
