This commit is contained in:
191
shiftflow/services/sessions.py
Normal file
191
shiftflow/services/sessions.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from django.db import transaction
|
||||
|
||||
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.delete()
|
||||
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.delete()
|
||||
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"])
|
||||
Reference in New Issue
Block a user