Files
MES_Core/shiftflow/services/shipping.py

285 lines
9.3 KiB
Python
Raw Permalink 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.

import logging
from typing import Dict
from django.db import transaction
from django.db.models import Sum
from django.db.models.functions import Coalesce
from django.utils import timezone
from manufacturing.models import EntityOperation
from shiftflow.models import DealItem, WorkItem
from warehouse.models import Material, StockItem, TransferLine, TransferRecord
from warehouse.services.transfers import receive_transfer
logger = logging.getLogger('mes')
def build_shipment_rows(
*,
deal_id: int,
shipping_location_id: int,
) -> tuple[list[dict], list[dict]]:
"""
Формирует данные для интерфейса отгрузки по сделке:
- Список деталей (Entities), готовых к отгрузке и уже отгруженных.
- Список давальческого сырья (Materials), доступного для возврата/отгрузки.
"""
deal_items = list(
DealItem.objects.select_related('entity')
.filter(deal_id=int(deal_id))
.order_by('entity__entity_type', 'entity__drawing_number', 'entity__name', 'id')
)
entities = [it.entity for it in deal_items if it.entity_id and it.entity]
ent_ids = [int(e.id) for e in entities if e]
entity_ops = list(
EntityOperation.objects.select_related('operation')
.filter(entity_id__in=ent_ids)
.order_by('entity_id', 'seq', 'id')
)
route_codes: dict[int, list[str]] = {}
last_code: dict[int, str] = {}
for eo in entity_ops:
if not eo.operation_id or not eo.operation:
continue
code = (eo.operation.code or '').strip()
if not code:
continue
route_codes.setdefault(int(eo.entity_id), []).append(code)
# Определяем последнюю операцию в маршруте для каждой детали
for eid, codes in route_codes.items():
if codes:
last_code[int(eid)] = str(codes[-1])
wi_qs = WorkItem.objects.select_related('operation').filter(deal_id=int(deal_id), entity_id__in=ent_ids)
done_by: dict[tuple[int, str], int] = {}
done_total_by_entity: dict[int, int] = {}
# Подсчитываем количество выполненных деталей по операциям
for wi in wi_qs:
op_code = ''
if getattr(wi, 'operation_id', None) and getattr(wi, 'operation', None):
op_code = (wi.operation.code or '').strip()
if not op_code:
op_code = (wi.stage or '').strip()
if not op_code:
continue
eid = int(wi.entity_id)
done = int(wi.quantity_done or 0)
done_by[(eid, str(op_code))] = done_by.get((eid, str(op_code)), 0) + done
done_total_by_entity[eid] = done_total_by_entity.get(eid, 0) + done
shipped_by = {
int(r['entity_id']): float(r['s'] or 0.0)
for r in StockItem.objects.filter(
is_archived=False,
location_id=int(shipping_location_id),
deal_id=int(deal_id),
entity_id__in=ent_ids,
)
.values('entity_id')
.annotate(s=Coalesce(Sum('quantity'), 0.0))
}
ent_avail = {
int(r['entity_id']): float(r['s'] or 0.0)
for r in StockItem.objects.filter(
deal_id=int(deal_id),
is_archived=False,
quantity__gt=0,
entity_id__in=ent_ids,
)
.exclude(location_id=int(shipping_location_id))
.values('entity_id')
.annotate(s=Coalesce(Sum('quantity'), 0.0))
}
entity_rows = []
for di in deal_items:
e = di.entity
if not e:
continue
need = int(di.quantity or 0)
if need <= 0:
continue
eid = int(e.id)
last = last_code.get(eid)
# Количество готовых деталей: берем по последней операции маршрута,
# либо общее количество, если маршрут не задан
ready_done = int(done_by.get((eid, str(last)), 0) or 0) if last else int(done_total_by_entity.get(eid, 0) or 0)
ready_val = min(need, ready_done)
# Сколько уже отгружено на склад отгрузки
shipped_val = int(shipped_by.get(eid, 0.0) or 0.0)
shipped_val = min(need, shipped_val)
remaining_ready = int(max(0, ready_val - shipped_val))
if remaining_ready <= 0:
continue
entity_rows.append({
'entity': e,
'available': float(ent_avail.get(eid, 0.0) or 0.0),
'ready': int(ready_val),
'shipped': int(shipped_val),
'remaining_ready': int(remaining_ready),
})
mat_rows = list(
StockItem.objects.filter(
deal_id=int(deal_id),
is_archived=False,
quantity__gt=0,
material_id__isnull=False,
is_customer_supplied=True,
)
.exclude(location_id=int(shipping_location_id))
.values('material_id')
.annotate(available=Coalesce(Sum('quantity'), 0.0))
.order_by('material_id')
)
mat_ids = [int(r['material_id']) for r in mat_rows if r.get('material_id')]
mats = {m.id: m for m in Material.objects.filter(id__in=mat_ids)}
material_rows = []
for r in mat_rows:
mid = int(r['material_id'])
m = mats.get(mid)
if not m:
continue
material_rows.append({
'material': m,
'available': float(r.get('available') or 0.0),
})
return entity_rows, material_rows
@transaction.atomic
def create_shipment_transfers(
*,
deal_id: int,
shipping_location_id: int,
entity_qty: Dict[int, int],
material_qty: Dict[int, float],
user_id: int,
) -> list[int]:
"""
Создает документы перемещения (TransferRecord) на склад отгрузки
для указанных деталей и давальческого сырья.
"""
logger.info(
'fn:start create_shipment_transfers deal_id=%s shipping_location_id=%s user_id=%s',
deal_id, shipping_location_id, user_id
)
now = timezone.now()
transfers_by_location: dict[int, TransferRecord] = {}
def get_transfer(from_location_id: int) -> TransferRecord:
tr = transfers_by_location.get(int(from_location_id))
if tr:
return tr
tr = TransferRecord.objects.create(
from_location_id=int(from_location_id),
to_location_id=int(shipping_location_id),
sender_id=int(user_id),
receiver_id=int(user_id),
occurred_at=now,
status='received',
received_at=now,
is_applied=False,
)
transfers_by_location[int(from_location_id)] = tr
return tr
def alloc_stock_lines(qs, need_qty: float) -> None:
"""
Резервирует необходимое количество из доступных складских остатков.
Использует select_for_update для предотвращения гонок данных.
"""
remaining = float(need_qty)
if remaining <= 0:
return
items = list(qs.select_for_update().order_by('location_id', 'created_at', 'id'))
for si in items:
if remaining <= 0:
break
if int(si.location_id) == int(shipping_location_id):
continue
si_qty = float(si.quantity or 0.0)
if si_qty <= 0:
continue
if si.unique_id:
if remaining < si_qty:
raise ValueError('Нельзя частично отгружать позицию с маркировкой (unique_id).')
take = si_qty
else:
take = min(remaining, si_qty)
tr = get_transfer(int(si.location_id))
TransferLine.objects.create(transfer=tr, stock_item=si, quantity=float(take))
remaining -= float(take)
if remaining > 0:
raise ValueError('Недостаточно количества на складах для отгрузки.')
for ent_id, qty in (entity_qty or {}).items():
q = int(qty or 0)
if q <= 0:
continue
alloc_stock_lines(
StockItem.objects.filter(
deal_id=int(deal_id),
is_archived=False,
quantity__gt=0,
entity_id=int(ent_id),
).select_related('entity', 'location'),
float(q),
)
for mat_id, qty in (material_qty or {}).items():
q = float(qty or 0.0)
if q <= 0:
continue
alloc_stock_lines(
StockItem.objects.filter(
deal_id=int(deal_id),
is_archived=False,
quantity__gt=0,
material_id=int(mat_id),
is_customer_supplied=True,
).select_related('material', 'location'),
float(q),
)
ids = []
for tr in transfers_by_location.values():
receive_transfer(int(tr.id), int(user_id))
ids.append(int(tr.id))
ids.sort()
logger.info(
'fn:done create_shipment_transfers deal_id=%s transfers=%s',
deal_id,
ids,
)
return ids