import logging from django.db import transaction from django.db.models import Q, Sum from django.db.models.functions import Coalesce from django.utils import timezone from manufacturing.models import EntityOperation from shiftflow.models import DealEntityProgress, DealItem, ProductionTask, WorkItem logger = logging.getLogger('mes') def _workitem_op_code(wi: WorkItem) -> str: if getattr(wi, 'operation_id', None) and getattr(wi, 'operation', None): code = (wi.operation.code or '').strip() if code: return code return (wi.stage or '').strip() def _target_qty_for_workitem(wi: WorkItem) -> int | None: if getattr(wi, 'delivery_batch_id', None): qty = ( ProductionTask.objects.filter( deal_id=wi.deal_id, delivery_batch_id=wi.delivery_batch_id, entity_id=wi.entity_id, ) .values_list('quantity_ordered', flat=True) .first() ) return int(qty) if qty is not None else None di = DealItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).first() return int(di.quantity) if di else None @transaction.atomic def advance_progress_and_generate_next_workitem(*, workitem_id: int) -> int | None: wi = ( WorkItem.objects.select_for_update(of=('self',)) .select_related('operation') .filter(id=int(workitem_id)) .first() ) if not wi: return None op_code = _workitem_op_code(wi) if not op_code: return None target_qty = _target_qty_for_workitem(wi) if target_qty is None: return None progress, _ = DealEntityProgress.objects.select_for_update(of=('self',)).get_or_create( deal_id=wi.deal_id, delivery_batch_id=(int(wi.delivery_batch_id) if getattr(wi, 'delivery_batch_id', None) else None), entity_id=wi.entity_id, defaults={'current_seq': 1}, ) cur = int(progress.current_seq or 1) cur_eo = EntityOperation.objects.select_related('operation').filter(entity_id=wi.entity_id, seq=cur).first() if not cur_eo or not cur_eo.operation: return None cur_code = (cur_eo.operation.code or '').strip() if cur_code != op_code: return None wi_qs = WorkItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).filter(Q(operation__code=op_code) | Q(stage=op_code)) if getattr(wi, 'delivery_batch_id', None): wi_qs = wi_qs.filter(delivery_batch_id=wi.delivery_batch_id) else: wi_qs = wi_qs.filter(delivery_batch_id__isnull=True) total_done = wi_qs.aggregate(s=Coalesce(Sum('quantity_done'), 0))['s'] if int(total_done or 0) < int(target_qty): return None progress.current_seq = cur + 1 progress.save(update_fields=['current_seq']) next_eo = ( EntityOperation.objects.select_related('operation', 'operation__workshop') .filter(entity_id=wi.entity_id, seq=int(progress.current_seq)) .first() ) if not next_eo or not next_eo.operation: return None next_op = next_eo.operation next_code = (next_op.code or '').strip() planned_qs = WorkItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id) if getattr(wi, 'delivery_batch_id', None): planned_qs = planned_qs.filter(delivery_batch_id=wi.delivery_batch_id) else: planned_qs = planned_qs.filter(delivery_batch_id__isnull=True) planned_total = planned_qs.filter(Q(operation_id=next_op.id) | Q(operation__code=next_code) | Q(stage=next_code)).aggregate( s=Coalesce(Sum('quantity_plan'), 0) )['s'] remaining_to_plan = max(0, int(target_qty) - int(planned_total or 0)) if remaining_to_plan <= 0: return None created = WorkItem.objects.create( deal_id=wi.deal_id, delivery_batch_id=(int(wi.delivery_batch_id) if getattr(wi, 'delivery_batch_id', None) else None), entity_id=wi.entity_id, operation_id=next_op.id, stage=(next_code or next_op.name or '')[:32], workshop_id=(int(next_op.workshop_id) if getattr(next_op, 'workshop_id', None) else None), machine_id=None, quantity_plan=int(remaining_to_plan), quantity_done=0, status='planned', date=timezone.localdate(), ) logger.info( 'route_flow:created_next_workitem id=%s deal_id=%s batch_id=%s entity_id=%s op=%s qty=%s', created.id, created.deal_id, getattr(created, 'delivery_batch_id', None), created.entity_id, next_code or '', created.quantity_plan, ) return int(created.id)