Конкретно пересмотрел логику работы. Легаси вынесена в архив
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
This commit is contained in:
268
shiftflow/services/kitting.py
Normal file
268
shiftflow/services/kitting.py
Normal file
@@ -0,0 +1,268 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Case, IntegerField, Q, Value, When
|
||||
from django.utils import timezone
|
||||
|
||||
from manufacturing.models import ProductEntity
|
||||
from warehouse.models import Location, StockItem, TransferLine, TransferRecord
|
||||
from warehouse.services.transfers import receive_transfer
|
||||
|
||||
from shiftflow.services.bom_explosion import _accumulate_requirements, _build_bom_graph
|
||||
|
||||
logger = logging.getLogger('mes')
|
||||
|
||||
|
||||
def _session_key(workitem_id: int) -> str:
|
||||
return f'kitting_draft_workitem_{int(workitem_id)}'
|
||||
|
||||
|
||||
def get_kitting_draft(session: Any, workitem_id: int) -> list[dict]:
|
||||
key = _session_key(workitem_id)
|
||||
raw = session.get(key)
|
||||
if isinstance(raw, list):
|
||||
out = []
|
||||
for x in raw:
|
||||
if not isinstance(x, dict):
|
||||
continue
|
||||
out.append({
|
||||
'entity_id': int(x.get('entity_id') or 0),
|
||||
'from_location_id': int(x.get('from_location_id') or 0),
|
||||
'quantity': int(x.get('quantity') or 0),
|
||||
})
|
||||
return out
|
||||
return []
|
||||
|
||||
|
||||
def clear_kitting_draft(session: Any, workitem_id: int) -> None:
|
||||
key = _session_key(workitem_id)
|
||||
if key in session:
|
||||
del session[key]
|
||||
session.modified = True
|
||||
|
||||
|
||||
def add_kitting_line(session: Any, workitem_id: int, entity_id: int, from_location_id: int, quantity: int) -> None:
|
||||
workitem_id = int(workitem_id)
|
||||
entity_id = int(entity_id)
|
||||
from_location_id = int(from_location_id)
|
||||
quantity = int(quantity)
|
||||
|
||||
if workitem_id <= 0 or entity_id <= 0 or from_location_id <= 0 or quantity <= 0:
|
||||
return
|
||||
|
||||
key = _session_key(workitem_id)
|
||||
draft = get_kitting_draft(session, workitem_id)
|
||||
|
||||
merged = False
|
||||
for ln in draft:
|
||||
if int(ln.get('entity_id') or 0) == entity_id and int(ln.get('from_location_id') or 0) == from_location_id:
|
||||
ln['quantity'] = int(ln.get('quantity') or 0) + quantity
|
||||
merged = True
|
||||
break
|
||||
|
||||
if not merged:
|
||||
draft.append({'entity_id': entity_id, 'from_location_id': from_location_id, 'quantity': quantity})
|
||||
|
||||
session[key] = draft
|
||||
session.modified = True
|
||||
|
||||
|
||||
def remove_kitting_line(session: Any, workitem_id: int, entity_id: int, from_location_id: int, quantity: int) -> None:
|
||||
workitem_id = int(workitem_id)
|
||||
entity_id = int(entity_id)
|
||||
from_location_id = int(from_location_id)
|
||||
quantity = int(quantity)
|
||||
|
||||
if workitem_id <= 0 or entity_id <= 0 or from_location_id <= 0 or quantity <= 0:
|
||||
return
|
||||
|
||||
key = _session_key(workitem_id)
|
||||
draft = get_kitting_draft(session, workitem_id)
|
||||
|
||||
out = []
|
||||
for ln in draft:
|
||||
if int(ln.get('entity_id') or 0) == entity_id and int(ln.get('from_location_id') or 0) == from_location_id:
|
||||
cur = int(ln.get('quantity') or 0)
|
||||
cur = max(0, cur - quantity)
|
||||
if cur > 0:
|
||||
ln['quantity'] = cur
|
||||
out.append(ln)
|
||||
continue
|
||||
out.append(ln)
|
||||
|
||||
session[key] = out
|
||||
session.modified = True
|
||||
|
||||
|
||||
def get_work_location_for_workitem(workitem) -> Location | None:
|
||||
m = getattr(workitem, 'machine', None)
|
||||
if m and getattr(m, 'workshop_id', None) and getattr(getattr(m, 'workshop', None), 'location_id', None):
|
||||
return m.workshop.location
|
||||
if m and getattr(m, 'location_id', None):
|
||||
return m.location
|
||||
w = getattr(workitem, 'workshop', None)
|
||||
if w and getattr(w, 'location_id', None):
|
||||
return w.location
|
||||
return None
|
||||
|
||||
|
||||
def build_kitting_requirements(root_entity_id: int, qty_to_make: int) -> dict[int, int]:
|
||||
"""Потребность на комплектацию для сборки/изделия.
|
||||
|
||||
Комментарий: для ручной комплектации мастеру важны прямые компоненты (1 уровень BOM),
|
||||
включая подсборки. Глубину дерева раскрываем отдельными заданиями на подсборки.
|
||||
"""
|
||||
root_entity_id = int(root_entity_id)
|
||||
qty_to_make = int(qty_to_make or 0)
|
||||
if root_entity_id <= 0 or qty_to_make <= 0:
|
||||
return {}
|
||||
|
||||
adjacency = _build_bom_graph({root_entity_id})
|
||||
children = adjacency.get(root_entity_id) or []
|
||||
|
||||
out: dict[int, int] = {}
|
||||
for child_id, per1 in children:
|
||||
cid = int(child_id)
|
||||
need = int(per1 or 0) * qty_to_make
|
||||
if cid <= 0 or need <= 0:
|
||||
continue
|
||||
out[cid] = int(out.get(cid, 0) or 0) + need
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def build_kitting_leaf_requirements(root_entity_id: int, qty_to_make: int) -> dict[int, int]:
|
||||
"""Совместимость: старое имя оставлено, сейчас возвращает потребность 1-го уровня BOM."""
|
||||
return build_kitting_requirements(root_entity_id, qty_to_make)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def _apply_one_transfer(
|
||||
*,
|
||||
deal_id: int,
|
||||
component_entity_id: int,
|
||||
from_location_id: int,
|
||||
to_location_id: int,
|
||||
quantity: int,
|
||||
user_id: int,
|
||||
) -> int:
|
||||
deal_id = int(deal_id)
|
||||
component_entity_id = int(component_entity_id)
|
||||
from_location_id = int(from_location_id)
|
||||
to_location_id = int(to_location_id)
|
||||
quantity = int(quantity)
|
||||
user_id = int(user_id)
|
||||
|
||||
if quantity <= 0:
|
||||
raise RuntimeError('Количество должно быть больше 0.')
|
||||
|
||||
if from_location_id == to_location_id:
|
||||
raise RuntimeError('Склад-источник и склад назначения совпадают.')
|
||||
|
||||
# Комментарий: двигаем только "под сделку" и свободные остатки.
|
||||
# Приоритет: под сделку -> свободные, затем FIFO по поступлению.
|
||||
qs = (
|
||||
StockItem.objects.select_for_update()
|
||||
.filter(is_archived=False, quantity__gt=0)
|
||||
.filter(location_id=from_location_id, entity_id=component_entity_id)
|
||||
.filter(Q(deal_id=deal_id) | Q(deal_id__isnull=True))
|
||||
.annotate(
|
||||
prio=Case(
|
||||
When(deal_id=deal_id, then=Value(0)),
|
||||
default=Value(1),
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
)
|
||||
.order_by('prio', 'created_at', 'id')
|
||||
)
|
||||
|
||||
remaining = float(quantity)
|
||||
picked: list[tuple[int, float]] = []
|
||||
for si in qs:
|
||||
if remaining <= 0:
|
||||
break
|
||||
avail = float(si.quantity or 0)
|
||||
if avail <= 0:
|
||||
continue
|
||||
take = min(remaining, avail)
|
||||
if take <= 0:
|
||||
continue
|
||||
picked.append((int(si.id), float(take)))
|
||||
remaining -= take
|
||||
|
||||
if remaining > 0:
|
||||
# Комментарий: допускаем расхождения с фактом (по данным базы может быть меньше, чем нужно по месту).
|
||||
# Для продолжения процесса создаем "виртуальный" остаток под сделку на складе-источнике и перемещаем его.
|
||||
phantom = StockItem.objects.create(
|
||||
entity_id=int(component_entity_id),
|
||||
deal_id=int(deal_id),
|
||||
location_id=int(from_location_id),
|
||||
quantity=float(remaining),
|
||||
)
|
||||
picked.append((int(phantom.id), float(remaining)))
|
||||
logger.warning(
|
||||
'kitting_transfer: phantom_created deal_id=%s entity_id=%s from_location=%s qty=%s',
|
||||
deal_id,
|
||||
component_entity_id,
|
||||
from_location_id,
|
||||
float(remaining),
|
||||
)
|
||||
remaining = 0.0
|
||||
|
||||
tr = TransferRecord.objects.create(
|
||||
from_location_id=from_location_id,
|
||||
to_location_id=to_location_id,
|
||||
sender_id=user_id,
|
||||
receiver_id=user_id,
|
||||
occurred_at=timezone.now(),
|
||||
status='received',
|
||||
received_at=timezone.now(),
|
||||
is_applied=False,
|
||||
)
|
||||
|
||||
for sid, qty in picked:
|
||||
TransferLine.objects.create(transfer=tr, stock_item_id=sid, quantity=float(qty))
|
||||
|
||||
receive_transfer(tr.id, user_id)
|
||||
logger.info(
|
||||
'kitting_transfer: ok tr_id=%s deal_id=%s component=%s from=%s to=%s qty=%s',
|
||||
tr.id, deal_id, component_entity_id, from_location_id, to_location_id, quantity
|
||||
)
|
||||
return int(tr.id)
|
||||
|
||||
|
||||
def apply_kitting_draft(
|
||||
*,
|
||||
session: Any,
|
||||
workitem_id: int,
|
||||
deal_id: int,
|
||||
to_location_id: int,
|
||||
user_id: int,
|
||||
) -> dict[str, int]:
|
||||
draft = get_kitting_draft(session, int(workitem_id))
|
||||
if not draft:
|
||||
return {'applied': 0, 'errors': 0}
|
||||
|
||||
applied = 0
|
||||
errors = 0
|
||||
|
||||
for ln in draft:
|
||||
try:
|
||||
_apply_one_transfer(
|
||||
deal_id=int(deal_id),
|
||||
component_entity_id=int(ln.get('entity_id') or 0),
|
||||
from_location_id=int(ln.get('from_location_id') or 0),
|
||||
to_location_id=int(to_location_id),
|
||||
quantity=int(ln.get('quantity') or 0),
|
||||
user_id=int(user_id),
|
||||
)
|
||||
applied += 1
|
||||
except Exception:
|
||||
errors += 1
|
||||
logger.exception('kitting_transfer:error deal_id=%s workitem_id=%s line=%s', deal_id, workitem_id, ln)
|
||||
|
||||
if errors == 0:
|
||||
clear_kitting_draft(session, int(workitem_id))
|
||||
|
||||
return {'applied': applied, 'errors': errors}
|
||||
Reference in New Issue
Block a user