from django.db import transaction from django.db.models import F from django.utils import timezone import logging from shiftflow.models import ( CuttingSession, Item, ProductionReportConsumption, ProductionReportRemnant, ShiftItem, ) from shiftflow.services.sessions import close_cutting_session logger = logging.getLogger('mes') @transaction.atomic def apply_closing( *, user_id: int, machine_id: int, material_id: int, item_actions: dict[int, dict], consumptions: dict[int, float], remnants: list[dict], ) -> None: logger.info('apply_closing:start user=%s machine=%s material=%s items=%s consumptions=%s remnants=%s', user_id, machine_id, material_id, list(item_actions.keys()), list(consumptions.keys()), len(remnants)) items = list( Item.objects.select_for_update(of=('self',)) .select_related('task', 'task__deal', 'task__material', 'machine') .filter(id__in=list(item_actions.keys()), machine_id=machine_id, status='work', task__material_id=material_id) ) if not items: logger.error('apply_closing:no_items machine=%s material=%s', machine_id, material_id) raise RuntimeError('Не найдено пунктов сменки для закрытия.') report = CuttingSession.objects.create( operator_id=user_id, machine_id=machine_id, used_stock_item=None, date=timezone.localdate(), is_closed=False, ) logger.info('apply_closing:report_created id=%s', report.id) logger.info('apply_closing:update_items start items=%s', [it.id for it in items]) for it in items: spec = item_actions.get(it.id) or {} action = (spec.get('action') or '').strip() fact = int(spec.get('fact') or 0) if action not in ['done', 'partial']: continue plan = int(it.quantity_plan or 0) if plan <= 0: continue if action == 'done': fact = plan else: fact = max(0, min(fact, plan)) if fact <= 0: raise RuntimeError('При частичном закрытии факт должен быть больше 0.') ShiftItem.objects.create(session=report, task=it.task, quantity_fact=fact) logger.info('apply_closing:consumption_count=%s', len(consumptions)) for stock_item_id, qty in consumptions.items(): if qty <= 0: continue ProductionReportConsumption.objects.create( report=report, stock_item_id=stock_item_id, material_id=None, quantity=float(qty), ) logger.info('apply_closing:remnants_count=%s', len(remnants)) for r in remnants: qty = float(r.get('quantity') or 0) if qty <= 0: continue ProductionReportRemnant.objects.create( report=report, material_id=material_id, quantity=qty, current_length=r.get('current_length'), current_width=r.get('current_width'), unique_id=None, ) logger.info('apply_closing:close_session id=%s', report.id) close_cutting_session(report.id) for it in items: spec = item_actions.get(it.id) or {} action = (spec.get('action') or '').strip() fact = int(spec.get('fact') or 0) if action not in ['done', 'partial']: continue plan = int(it.quantity_plan or 0) if plan <= 0: continue if action == 'done': it.quantity_fact = plan it.status = 'done' it.save(update_fields=['quantity_fact', 'status']) continue fact = max(0, min(fact, plan)) residual = plan - fact it.quantity_fact = fact it.status = 'partial' it.save(update_fields=['quantity_fact', 'status']) if residual > 0: Item.objects.create( task=it.task, date=it.date, machine=it.machine, quantity_plan=residual, quantity_fact=0, status='leftover', is_synced_1c=False, ) logger.info('apply_closing:done report=%s', report.id) @transaction.atomic def apply_closing_workitems( *, user_id: int, machine_id: int, material_id: int, item_actions: dict[int, dict], # workitem_id -> {'action': 'done'|'partial', 'fact': int} consumptions: dict[int, float], remnants: list[dict], ) -> None: logger.info('apply_closing_workitems:start user=%s machine=%s material=%s workitems=%s cons=%s rem=%s', user_id, machine_id, material_id, list(item_actions.keys()), list(consumptions.keys()), len(remnants)) from shiftflow.models import WorkItem, ProductionTask wis = list( WorkItem.objects.select_for_update(of=("self",)) .select_related('deal', 'entity', 'machine') .filter(id__in=list(item_actions.keys()), machine_id=machine_id, status__in=['planned'], entity__planned_material_id=material_id) .filter(quantity_done__lt=F('quantity_plan')) ) if not wis: raise RuntimeError('Не найдено сменных заданий для закрытия.') report = CuttingSession.objects.create( operator_id=user_id, machine_id=machine_id, used_stock_item=None, date=timezone.localdate(), is_closed=False, ) created_shift = 0 for wi in wis: spec = item_actions.get(wi.id) or {} action = (spec.get('action') or '').strip() fact = int(spec.get('fact') or 0) if action not in ['done', 'partial']: continue plan_total = int(wi.quantity_plan or 0) done_total = int(wi.quantity_done or 0) remaining = max(0, plan_total - done_total) if remaining <= 0: continue if action == 'done': fact = remaining else: fact = max(0, min(fact, remaining)) if fact <= 0: raise RuntimeError('При частичном закрытии факт должен быть больше 0.') pt = ProductionTask.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).first() if not pt: raise RuntimeError('Не найден ProductionTask для задания.') ShiftItem.objects.create(session=report, task=pt, quantity_fact=fact) created_shift += 1 wi.quantity_done = done_total + fact if wi.quantity_done >= plan_total: wi.status = 'done' elif wi.quantity_done > 0: wi.status = 'leftover' else: wi.status = 'planned' wi.save(update_fields=['quantity_done', 'status']) for stock_item_id, qty in consumptions.items(): if qty and float(qty) > 0: ProductionReportConsumption.objects.create(report=report, stock_item_id=stock_item_id, material_id=None, quantity=float(qty)) for r in remnants: qty = float(r.get('quantity') or 0) if qty <= 0: continue ProductionReportRemnant.objects.create( report=report, material_id=material_id, quantity=qty, current_length=r.get('current_length'), current_width=r.get('current_width'), unique_id=None, ) close_cutting_session(report.id) logger.info('apply_closing_workitems:done report=%s shift_items=%s', report.id, created_shift)