Files
MES_Core/shiftflow/services/sessions.py
ackFromRedmi 86215c9fa8
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
Открыл админу изделия, доработал списание
2026-04-07 12:57:43 +03:00

210 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.db import transaction
import logging
from django.utils import timezone
from manufacturing.models import ProductEntity
from shiftflow.models import (
CuttingSession,
ProductionReportConsumption,
ProductionReportRemnant,
ProductionReportStockResult,
ShiftItem,
)
from warehouse.models import StockItem
logger = logging.getLogger('mes')
@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
"""
logger.info('close_cutting_session:start id=%s', session_id)
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)
logger.info('close_cutting_session:consume stock_item=%s qty=%s before=%s', si.id, c.quantity, si.quantity)
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'])
logger.info('close_cutting_session:archived stock_item=%s', si.id)
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.is_archived = True
si.archived_at = timezone.now()
si.save(update_fields=['quantity', 'is_archived', 'archived_at'])
logger.info('close_cutting_session:archived stock_item=%s', si.id)
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)
logger.info('close_cutting_session:used stock_item=%s before=%s', used.id, used.quantity)
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'])
logger.info('close_cutting_session:archived used=%s', used.id)
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"])
logger.info('close_cutting_session:done id=%s', session_id)