Ввел логику сделок через партии
All checks were successful
Deploy MES Core / deploy (push) Successful in 4m16s

This commit is contained in:
2026-04-22 08:35:07 +03:00
parent 3efd8e5060
commit 6da7b775c7
14 changed files with 489 additions and 45 deletions

View File

@@ -962,9 +962,20 @@ class WorkItemUpdateView(LoginRequiredMixin, View):
wi.save(update_fields=list(dict.fromkeys(changed_fields)))
# Комментарий: автоматический переход на следующую операцию по маршруту для пары (сделка, сущность).
# Сдвигаем только когда выполнено количество по позиции сделки.
if ordered_qty is not None:
# Комментарий: автоматический переход на следующую операцию по маршруту.
# Для партийного производства двигаем прогресс в рамках партии, ориентируясь на объём серии.
target_qty = None
if getattr(wi, 'delivery_batch_id', None):
target_qty = ProductionTask.objects.filter(
deal_id=wi.deal_id,
delivery_batch_id=wi.delivery_batch_id,
entity_id=wi.entity_id,
).values_list('quantity_ordered', flat=True).first()
else:
deal_item = DealItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).first()
target_qty = int(deal_item.quantity) if deal_item else None
if target_qty is not None:
op_code = None
if getattr(wi, 'operation_id', None):
op_code = Operation.objects.filter(pk=wi.operation_id).values_list('code', flat=True).first()
@@ -972,13 +983,26 @@ class WorkItemUpdateView(LoginRequiredMixin, View):
op_code = (wi.stage or '').strip()
if op_code:
progress, _ = DealEntityProgress.objects.get_or_create(deal_id=wi.deal_id, entity_id=wi.entity_id, defaults={'current_seq': 1})
progress, _ = DealEntityProgress.objects.get_or_create(
deal_id=wi.deal_id,
delivery_batch_id=(int(wi.delivery_batch_id) if getattr(wi, 'delivery_batch_id', None) else None),
entity_id=wi.entity_id,
defaults={'current_seq': 1},
)
cur = int(progress.current_seq or 1)
cur_eo = EntityOperation.objects.select_related('operation').filter(entity_id=wi.entity_id, seq=cur).first()
if cur_eo and cur_eo.operation and cur_eo.operation.code == op_code:
total_done = WorkItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).filter(Q(operation__code=op_code) | Q(stage=op_code)).aggregate(s=Coalesce(Sum('quantity_done'), 0))['s']
if int(total_done or 0) >= int(ordered_qty):
wi_qs = WorkItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).filter(
Q(operation__code=op_code) | Q(stage=op_code)
)
if getattr(wi, 'delivery_batch_id', None):
wi_qs = wi_qs.filter(delivery_batch_id=wi.delivery_batch_id)
else:
wi_qs = wi_qs.filter(delivery_batch_id__isnull=True)
total_done = wi_qs.aggregate(s=Coalesce(Sum('quantity_done'), 0))['s']
if int(total_done or 0) >= int(target_qty):
progress.current_seq = cur + 1
progress.save(update_fields=['current_seq'])
@@ -1396,9 +1420,18 @@ class WorkItemOpClosingView(LoginRequiredMixin, TemplateView):
wi.status = 'planned'
wi.save(update_fields=['quantity_done', 'status'])
deal_item = DealItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).first()
ordered_qty = int(deal_item.quantity) if deal_item else None
if ordered_qty is not None:
target_qty = None
if getattr(wi, 'delivery_batch_id', None):
target_qty = ProductionTask.objects.filter(
deal_id=wi.deal_id,
delivery_batch_id=wi.delivery_batch_id,
entity_id=wi.entity_id,
).values_list('quantity_ordered', flat=True).first()
else:
deal_item = DealItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).first()
target_qty = int(deal_item.quantity) if deal_item else None
if target_qty is not None:
op_code = None
if getattr(wi, 'operation_id', None):
op_code = Operation.objects.filter(pk=wi.operation_id).values_list('code', flat=True).first()
@@ -1406,12 +1439,25 @@ class WorkItemOpClosingView(LoginRequiredMixin, TemplateView):
op_code = (wi.stage or '').strip()
if op_code:
progress, _ = DealEntityProgress.objects.get_or_create(deal_id=wi.deal_id, entity_id=wi.entity_id, defaults={'current_seq': 1})
progress, _ = DealEntityProgress.objects.get_or_create(
deal_id=wi.deal_id,
delivery_batch_id=(int(wi.delivery_batch_id) if getattr(wi, 'delivery_batch_id', None) else None),
entity_id=wi.entity_id,
defaults={'current_seq': 1},
)
cur = int(progress.current_seq or 1)
cur_eo = EntityOperation.objects.select_related('operation').filter(entity_id=wi.entity_id, seq=cur).first()
if cur_eo and cur_eo.operation and cur_eo.operation.code == op_code:
total_done = WorkItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).filter(Q(operation__code=op_code) | Q(stage=op_code)).aggregate(s=Coalesce(Sum('quantity_done'), 0))['s']
if int(total_done or 0) >= int(ordered_qty):
wi_qs = WorkItem.objects.filter(deal_id=wi.deal_id, entity_id=wi.entity_id).filter(
Q(operation__code=op_code) | Q(stage=op_code)
)
if getattr(wi, 'delivery_batch_id', None):
wi_qs = wi_qs.filter(delivery_batch_id=wi.delivery_batch_id)
else:
wi_qs = wi_qs.filter(delivery_batch_id__isnull=True)
total_done = wi_qs.aggregate(s=Coalesce(Sum('quantity_done'), 0))['s']
if int(total_done or 0) >= int(target_qty):
progress.current_seq = cur + 1
progress.save(update_fields=['current_seq'])
@@ -2052,14 +2098,22 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
tasks = list(
ProductionTask.objects.filter(deal=deal)
.select_related('material', 'entity')
.select_related('material', 'entity', 'delivery_batch')
.order_by('-id')
)
task_entity_ids = {int(x.entity_id) for x in tasks if getattr(x, 'entity_id', None)}
task_batch_ids = {int(x.delivery_batch_id) for x in tasks if getattr(x, 'delivery_batch_id', None)}
progress_qs = DealEntityProgress.objects.filter(deal=deal, entity_id__in=list(task_entity_ids))
if task_batch_ids:
progress_qs = progress_qs.filter(Q(delivery_batch_id__in=list(task_batch_ids)) | Q(delivery_batch_id__isnull=True))
else:
progress_qs = progress_qs.filter(delivery_batch_id__isnull=True)
progress_task_map = {
int(p.entity_id): int(p.current_seq or 1)
for p in DealEntityProgress.objects.filter(deal=deal, entity_id__in=list(task_entity_ids))
((int(p.delivery_batch_id) if getattr(p, 'delivery_batch_id', None) else None), int(p.entity_id)): int(p.current_seq or 1)
for p in progress_qs
}
ops_task_map = {}
for eo in (
@@ -2078,7 +2132,8 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
if not getattr(t, 'entity_id', None):
continue
seq = int(progress_task_map.get(int(t.entity_id), 1) or 1)
batch_key = int(t.delivery_batch_id) if getattr(t, 'delivery_batch_id', None) else None
seq = int(progress_task_map.get((batch_key, int(t.entity_id)), 1) or 1)
eo = ops_task_map.get((int(t.entity_id), seq))
if not eo:
continue
@@ -2093,8 +2148,12 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
wi_qs = wi_qs.filter(workshop_id__in=allowed_ws)
wi_sums = {
(int(r['entity_id']), int(r['operation_id'])): (int(r['planned'] or 0), int(r['done'] or 0))
for r in wi_qs.values('entity_id', 'operation_id').annotate(
(
(int(r['delivery_batch_id']) if r['delivery_batch_id'] is not None else None),
int(r['entity_id']),
int(r['operation_id']),
): (int(r['planned'] or 0), int(r['done'] or 0))
for r in wi_qs.values('delivery_batch_id', 'entity_id', 'operation_id').annotate(
planned=Coalesce(Sum('quantity_plan'), 0),
done=Coalesce(Sum('quantity_done'), 0),
)
@@ -2108,7 +2167,11 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
need = int(t.quantity_ordered or 0)
key = None
if getattr(t, 'entity_id', None) and getattr(t, 'current_operation_id', None):
key = (int(t.entity_id), int(t.current_operation_id))
key = (
(int(t.delivery_batch_id) if getattr(t, 'delivery_batch_id', None) else None),
int(t.entity_id),
int(t.current_operation_id),
)
planned_qty, done_qty = wi_sums.get(key, (0, 0)) if key else (0, 0)
planned_qty = int(planned_qty or 0)
@@ -2649,6 +2712,7 @@ class WorkItemPlanAddView(LoginRequiredMixin, View):
return int(s) if s.isdigit() else None
deal_id = parse_int(request.POST.get('deal_id'))
delivery_batch_id = parse_int(request.POST.get('delivery_batch_id'))
entity_id = parse_int(request.POST.get('entity_id'))
operation_id = parse_int(request.POST.get('operation_id'))
machine_id = parse_int(request.POST.get('machine_id'))
@@ -2703,7 +2767,11 @@ class WorkItemPlanAddView(LoginRequiredMixin, View):
progress_map = {
int(p.entity_id): int(p.current_seq or 1)
for p in DealEntityProgress.objects.filter(deal_id=int(deal_id), entity_id__in=node_ids)
for p in DealEntityProgress.objects.filter(
deal_id=int(deal_id),
delivery_batch_id=(int(delivery_batch_id) if delivery_batch_id else None),
entity_id__in=node_ids,
)
}
ops_map = {
@@ -2718,6 +2786,7 @@ class WorkItemPlanAddView(LoginRequiredMixin, View):
if int(c_id) == int(entity_id):
WorkItem.objects.create(
deal_id=deal_id,
delivery_batch_id=(int(delivery_batch_id) if delivery_batch_id else None),
entity_id=entity_id,
operation_id=operation_id,
workshop_id=resolved_workshop_id,
@@ -2739,6 +2808,7 @@ class WorkItemPlanAddView(LoginRequiredMixin, View):
cur_op = eo.operation
WorkItem.objects.create(
deal_id=deal_id,
delivery_batch_id=(int(delivery_batch_id) if delivery_batch_id else None),
entity_id=int(c_id),
operation_id=int(cur_op.id),
workshop_id=(int(cur_op.workshop_id) if getattr(cur_op, 'workshop_id', None) else None),
@@ -2758,6 +2828,7 @@ class WorkItemPlanAddView(LoginRequiredMixin, View):
else:
wi = WorkItem.objects.create(
deal_id=int(deal_id),
delivery_batch_id=(int(delivery_batch_id) if delivery_batch_id else None),
entity_id=int(entity_id),
operation_id=int(operation_id),
workshop_id=resolved_workshop_id,
@@ -3281,7 +3352,11 @@ class DealBatchActionView(LoginRequiredMixin, View):
messages.error(request, 'Нельзя откатить: по этой позиции уже есть постановка в смену (план/факт).')
return redirect(next_url)
stats = rollback_roots_additive(int(deal_id), [(int(bi.entity_id), int(qty))])
stats = rollback_roots_additive(
int(deal_id),
[(int(bi.entity_id), int(qty))],
delivery_batch_id=int(bi.batch_id),
)
bi.started_qty = started - int(qty)
bi.save(update_fields=['started_qty'])
@@ -3336,7 +3411,11 @@ class DealBatchActionView(LoginRequiredMixin, View):
logger.info('start_batch_item_production: qty_exceeds_remaining remaining=%s started=%s total=%s', remaining, started, total)
return redirect(next_url)
stats = explode_roots_additive(int(deal_id), [(int(bi.entity_id), int(qty))])
stats = explode_roots_additive(
int(deal_id),
[(int(bi.entity_id), int(qty))],
delivery_batch_id=int(bi.batch_id),
)
bi.started_qty = started + int(qty)
bi.save(update_fields=['started_qty'])
@@ -3354,11 +3433,22 @@ class DealBatchActionView(LoginRequiredMixin, View):
)
return redirect(next_url)
except ExplosionValidationError as ev:
try:
from manufacturing.models import ProductEntity
bad = list(ProductEntity.objects.filter(id__in=list(ev.missing_material_ids)).values_list('drawing_number', 'name'))
except Exception:
bad = []
if getattr(ev, 'missing_route_entity_ids', None):
bad = list(
ProductEntity.objects.filter(id__in=list(ev.missing_route_entity_ids)).values_list('id', 'drawing_number', 'name')
)
if bad:
preview = ", ".join([f"/products/{eid}/info/ — {dn or ''} {nm}" for eid, dn, nm in bad[:5]])
more = '' if len(bad) <= 5 else f" и ещё {len(bad)-5}"
messages.error(
request,
f'Нельзя запустить в производство: нет техпроцесса (операция seq=1) у: {preview}{more}. Добавь техпроцесс и повтори запуск.',
)
else:
messages.error(request, 'Нельзя запустить в производство: у части позиций отсутствует техпроцесс. Добавь техпроцесс и повтори запуск.')
return redirect(next_url)
bad = list(ProductEntity.objects.filter(id__in=list(getattr(ev, 'missing_material_ids', []) or [])).values_list('drawing_number', 'name'))
if bad:
preview = ", ".join([f"{dn or ''} {nm}" for dn, nm in bad[:5]])
more = '' if len(bad) <= 5 else f" и ещё {len(bad)-5}"