ДО формируются под сделку
All checks were successful
Deploy MES Core / deploy (push) Successful in 12s

This commit is contained in:
2026-04-23 08:36:57 +03:00
parent ae9c747c78
commit 963dc7105a
8 changed files with 73 additions and 7 deletions

View File

@@ -15,6 +15,7 @@
- Отгрузка: можно добавлять несколько сделок в одну сессию отгрузки, выбирать позиции и подтверждать общий список.
- Журнал отгрузки: добавлены фильтр по периоду (по умолчанию 2 недели) и поиск по сделкам (номер/описание/заказчик), убран столбец «Куда».
- Списание / Производство: в блоках «Списано» и «Остаток ДО» выводится масса материалов (по размерам и «Масса на ед. учёта»); если масса не задана — показывается прочерк.
- Закрытие: деловой остаток (ДО) может наследовать сделку от списанного сырья (отключается чекбоксом) и доступен к отгрузке как сырьё по сделке.
- Реестр заданий: комментарий сменного задания/операции отображается под наименованием.
- Паспорта изделий/компонентов: ссылки на PDF/DXF/картинки отображаются иконками и открываются в новой вкладке.
- Паспорта изделий/сборок: блок «Состав» перенесён в верхнюю часть страницы, в таблицу состава добавлена колонка «Файлы».

View File

@@ -24,6 +24,7 @@ def apply_closing(
item_actions: dict[int, dict],
consumptions: dict[int, float],
remnants: list[dict],
inherit_deal_for_remnants: bool = False,
) -> None:
logger.info('apply_closing:start user=%s machine=%s material=%s items=%s consumptions=%s remnants=%s', user_id, machine_id, material_id, list(item_actions.keys()), list(consumptions.keys()), len(remnants))
@@ -93,7 +94,7 @@ def apply_closing(
)
logger.info('apply_closing:close_session id=%s', report.id)
close_cutting_session(report.id)
close_cutting_session(report.id, inherit_deal_for_remnants=bool(inherit_deal_for_remnants))
for it in items:
spec = item_actions.get(it.id) or {}
@@ -142,6 +143,7 @@ def apply_closing_workitems(
item_actions: dict[int, dict], # workitem_id -> {'action': 'done'|'partial', 'fact': int}
consumptions: dict[int, float],
remnants: list[dict],
inherit_deal_for_remnants: bool = False,
) -> None:
logger.info('apply_closing_workitems:start user=%s machine=%s material=%s workitems=%s cons=%s rem=%s', user_id, machine_id, material_id, list(item_actions.keys()), list(consumptions.keys()), len(remnants))
@@ -224,5 +226,5 @@ def apply_closing_workitems(
unique_id=None,
)
close_cutting_session(report.id)
close_cutting_session(report.id, inherit_deal_for_remnants=bool(inherit_deal_for_remnants))
logger.info('apply_closing_workitems:done report=%s shift_items=%s', report.id, created_shift)

View File

@@ -17,7 +17,7 @@ logger = logging.getLogger('mes')
@transaction.atomic
def close_cutting_session(session_id: int) -> None:
def close_cutting_session(session_id: int, *, inherit_deal_for_remnants: bool = False) -> None:
"""
Закрытие CuttingSession (транзакция склада).
@@ -56,6 +56,8 @@ def close_cutting_session(session_id: int) -> None:
raise RuntimeError('Не задан склад цеха для станка (Цех -> Склад цеха).')
consumed_material_ids: set[int] = set()
consumed_deal_ids: set[int] = set()
consumed_is_customer_supplied = False
consumptions = list(
ProductionReportConsumption.objects.select_related('material', 'stock_item', 'stock_item__material', 'stock_item__location')
@@ -72,6 +74,11 @@ def close_cutting_session(session_id: int) -> None:
si = StockItem.objects.select_for_update(of=('self',)).select_related('material', 'location').get(pk=c.stock_item_id)
logger.info('close_cutting_session:consume stock_item=%s qty=%s before=%s', si.id, c.quantity, si.quantity)
if getattr(si, 'deal_id', None):
consumed_deal_ids.add(int(si.deal_id))
if bool(getattr(si, 'is_customer_supplied', False)):
consumed_is_customer_supplied = True
if not si.material_id:
raise RuntimeError('В списании сырья указана позиция склада без material.')
@@ -129,6 +136,11 @@ def close_cutting_session(session_id: int) -> None:
used = StockItem.objects.select_for_update(of=('self',)).select_related('material', 'location').get(pk=session.used_stock_item_id)
logger.info('close_cutting_session:used stock_item=%s before=%s', used.id, used.quantity)
if getattr(used, 'deal_id', None):
consumed_deal_ids.add(int(used.deal_id))
if bool(getattr(used, 'is_customer_supplied', False)):
consumed_is_customer_supplied = True
if not used.material_id:
raise RuntimeError('Взятый материал должен ссылаться на сырьё (material), а не на готовую деталь (entity).')
@@ -191,13 +203,19 @@ def close_cutting_session(session_id: int) -> None:
)
ProductionReportStockResult.objects.create(report=session, stock_item=created, kind='finished')
remnant_deal_id = None
if inherit_deal_for_remnants and len(consumed_deal_ids) == 1:
remnant_deal_id = int(next(iter(consumed_deal_ids)))
remnants = list(ProductionReportRemnant.objects.filter(report=session).select_related('material'))
for r in remnants:
created = StockItem.objects.create(
material=r.material,
deal_id=remnant_deal_id,
location=work_location,
quantity=float(r.quantity),
is_remnant=True,
is_customer_supplied=bool(consumed_is_customer_supplied),
current_length=r.current_length,
current_width=r.current_width,
unique_id=r.unique_id,

View File

@@ -140,7 +140,6 @@ def build_shipment_rows(
is_archived=False,
quantity__gt=0,
material_id__isnull=False,
is_customer_supplied=True,
)
.exclude(location_id=int(shipping_location_id))
.values('material_id')
@@ -264,7 +263,6 @@ def create_shipment_transfers(
is_archived=False,
quantity__gt=0,
material_id=int(mat_id),
is_customer_supplied=True,
).select_related('material', 'location'),
float(q),
)

View File

@@ -132,7 +132,13 @@
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0">Остаток ДО</h5>
<div class="d-flex flex-wrap gap-3 align-items-center">
<h5 class="mb-0">Остаток ДО</h5>
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" name="inherit_deal_for_remnants" id="inheritDealForRemnants" value="1" checked>
<label class="form-check-label" for="inheritDealForRemnants">Привязать ДО к сделке сырья</label>
</div>
</div>
<button type="button" class="btn btn-outline-accent btn-sm" id="addRemnantBtn" {% if not can_edit %}disabled{% endif %}>Добавить ДО</button>
</div>
<div class="table-responsive">

View File

@@ -135,7 +135,13 @@
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0">Остаток ДО</h5>
<div class="d-flex flex-wrap gap-3 align-items-center">
<h5 class="mb-0">Остаток ДО</h5>
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" name="inherit_deal_for_remnants" id="inheritDealForRemnants" value="1" checked>
<label class="form-check-label" for="inheritDealForRemnants">Привязать ДО к сделке сырья</label>
</div>
</div>
<button type="button" class="btn btn-outline-accent btn-sm" id="addRemnantBtn" {% if not can_edit %}disabled{% endif %}>Добавить ДО</button>
</div>
<div class="table-responsive">

View File

@@ -57,6 +57,35 @@
</tbody>
</table>
</div>
<div class="border-top border-secondary"></div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr class="table-custom-header">
<th data-sort="false" style="width:44px;"></th>
<th>Сырьё</th>
<th class="text-center" style="width:120px;">Доступно</th>
<th class="text-center" style="width:180px;">К отгрузке</th>
</tr>
</thead>
<tbody>
{% for r in b.material_rows %}
<tr>
<td class="text-center"><input class="form-check-input border-secondary js-pick" type="checkbox" data-target="#qty_d{{ b.deal.id }}_mat_{{ r.material.id }}"></td>
<td><div class="fw-bold">{{ r.material.full_name|default:r.material.name }}</div><div class="small text-muted">Сырьё</div></td>
<td class="text-center fw-bold">{{ r.available|floatformat:"-g" }}</td>
<td class="text-center">
<input id="qty_d{{ b.deal.id }}_mat_{{ r.material.id }}" class="form-control bg-body text-body border-secondary ship-qty" type="number" min="0" step="0.001" max="{{ r.available|floatformat:'-g' }}" name="d{{ b.deal.id }}_mat_{{ r.material.id }}" value="0" data-deal="№{{ b.deal.number }}" data-label="Сырьё: {{ r.material.full_name|default:r.material.name }}" disabled>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center text-muted py-4">Нет сырья к отгрузке</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endfor %}

View File

@@ -5350,6 +5350,8 @@ class ClosingView(LoginRequiredMixin, TemplateView):
messages.error(request, 'Заполни списание: укажи, какие единицы на складе использованы и в каком количестве.')
return redirect(f"{reverse_lazy('closing')}?machine_id={machine_id}&material_id={material_id}")
inherit_deal_for_remnants = bool(request.POST.get('inherit_deal_for_remnants'))
try:
apply_closing_workitems(
user_id=request.user.id,
@@ -5358,6 +5360,7 @@ class ClosingView(LoginRequiredMixin, TemplateView):
item_actions=item_actions,
consumptions=consumptions,
remnants=remnants,
inherit_deal_for_remnants=inherit_deal_for_remnants,
)
messages.success(request, 'Закрытие выполнено.')
except Exception as e:
@@ -5969,6 +5972,8 @@ class LegacyClosingView(LoginRequiredMixin, TemplateView):
if idx > 60:
break
inherit_deal_for_remnants = bool(request.POST.get('inherit_deal_for_remnants'))
try:
apply_closing(
user_id=request.user.id,
@@ -5977,6 +5982,7 @@ class LegacyClosingView(LoginRequiredMixin, TemplateView):
item_actions=item_actions,
consumptions=consumptions,
remnants=remnants,
inherit_deal_for_remnants=inherit_deal_for_remnants,
)
messages.success(request, 'Сохранено.')
except Exception as e: