Ввел логику сделок через партии
All checks were successful
Deploy MES Core / deploy (push) Successful in 4m16s
All checks were successful
Deploy MES Core / deploy (push) Successful in 4m16s
This commit is contained in:
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user