from django.db import transaction from django.utils import timezone from manufacturing.models import ProductEntity from shiftflow.models import ( CuttingSession, ProductionReportConsumption, ProductionReportRemnant, ProductionReportStockResult, ShiftItem, ) from warehouse.models import StockItem @transaction.atomic def close_cutting_session(session_id: int) -> None: """ Закрытие CuttingSession (транзакция склада). A) Списать сырьё: - уменьшаем used_stock_item.quantity на 1 - если стало 0 -> удаляем B) Начислить готовые детали: - для каждого ShiftItem создаём StockItem(entity=..., location=machine.location, quantity=quantity_fact) - если использованный материал не совпадает с planned_material КД -> material_substitution=True """ session = ( CuttingSession.objects.select_for_update(of=('self',)) .select_related( "machine", "machine__location", "machine__workshop", "machine__workshop__location", "used_stock_item", "used_stock_item__material", ) .get(pk=session_id) ) if session.is_closed: return work_location = None if getattr(session.machine, 'workshop_id', None) and getattr(session.machine.workshop, 'location_id', None): work_location = session.machine.workshop.location elif session.machine.location_id: work_location = session.machine.location if not work_location: raise RuntimeError('Не задан склад цеха для станка (Цех -> Склад цеха).') consumed_material_ids: set[int] = set() consumptions = list( ProductionReportConsumption.objects.select_related('material', 'stock_item', 'stock_item__material', 'stock_item__location') .filter(report=session) ) if consumptions: for c in consumptions: need = float(c.quantity) if need <= 0: continue if c.stock_item_id: si = StockItem.objects.select_for_update(of=('self',)).select_related('material', 'location').get(pk=c.stock_item_id) if not si.material_id: raise RuntimeError('В списании сырья указана позиция склада без material.') if si.location_id != work_location.id: raise RuntimeError('Списывать сырьё можно только со склада цеха станка.') if need > float(si.quantity): raise RuntimeError('Недостаточно количества в выбранной складской позиции.') si.quantity = float(si.quantity) - need if si.quantity == 0: si.is_archived = True si.archived_at = timezone.now() si.save(update_fields=['quantity', 'is_archived', 'archived_at']) else: si.save(update_fields=['quantity']) consumed_material_ids.add(int(si.material_id)) continue if not c.material_id: raise RuntimeError('В списании сырья не указан материал.') consumed_material_ids.add(int(c.material_id)) qs = ( StockItem.objects.select_for_update(of=('self',)) .select_related('material', 'location') .filter(location=work_location, material_id=c.material_id, entity__isnull=True) .order_by('id') ) for si in qs: if need <= 0: break take = min(float(si.quantity), need) si.quantity = float(si.quantity) - take need -= take if si.quantity == 0: si.delete() else: si.save(update_fields=['quantity']) if need > 0: raise RuntimeError('Недостаточно сырья на складе цеха станка для списания.') else: if not session.used_stock_item_id: raise RuntimeError('Не заполнено списание сырья: добавь строки «Списание сырья» или укажи legacy поле «Взятый материал».') used = StockItem.objects.select_for_update(of=('self',)).select_related('material', 'location').get(pk=session.used_stock_item_id) if not used.material_id: raise RuntimeError('Взятый материал должен ссылаться на сырьё (material), а не на готовую деталь (entity).') if used.location_id != work_location.id: raise RuntimeError('Списывать сырьё можно только со склада цеха станка.') used.quantity = float(used.quantity) - 1.0 if used.quantity < 0: raise RuntimeError('Недостаточно сырья для списания.') if used.quantity == 0: used.is_archived = True used.archived_at = timezone.now() used.save(update_fields=['quantity', 'is_archived', 'archived_at']) else: used.save(update_fields=['quantity']) consumed_material_ids.add(int(used.material_id)) items = list( ShiftItem.objects.select_related("task", "task__entity", "task__entity__planned_material", "task__material") .filter(session=session) ) for it in items: if it.quantity_fact <= 0: continue task = it.task planned_material = None if task.entity_id and getattr(task.entity, 'planned_material_id', None): planned_material = task.entity.planned_material elif getattr(task, 'material_id', None): planned_material = task.material if planned_material and consumed_material_ids: it.material_substitution = planned_material.id not in consumed_material_ids else: it.material_substitution = False it.save(update_fields=['material_substitution']) if not task.entity_id: name = (getattr(task, 'drawing_name', '') or '').strip() or 'Без названия' pe = ProductEntity.objects.create( name=name[:255], drawing_number=f"AUTO-{task.id}", entity_type='part', planned_material=planned_material, ) task.entity = pe task.save(update_fields=['entity']) created = StockItem.objects.create( entity=task.entity, deal_id=getattr(task, 'deal_id', None), location=work_location, quantity=float(it.quantity_fact), ) ProductionReportStockResult.objects.create(report=session, stock_item=created, kind='finished') remnants = list(ProductionReportRemnant.objects.filter(report=session).select_related('material')) for r in remnants: created = StockItem.objects.create( material=r.material, location=work_location, quantity=float(r.quantity), is_remnant=True, current_length=r.current_length, current_width=r.current_width, unique_id=r.unique_id, ) ProductionReportStockResult.objects.create(report=session, stock_item=created, kind='remnant') session.is_closed = True session.save(update_fields=["is_closed"])