Отслеживаем компоненты без материала и техпроцесса
All checks were successful
Deploy MES Core / deploy (push) Successful in 12s
All checks were successful
Deploy MES Core / deploy (push) Successful in 12s
This commit is contained in:
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Производственные задачи и прогресс техпроцесса ведутся в разрезе партий поставки (серий) для одной сделки.
|
- Производственные задачи и прогресс техпроцесса ведутся в разрезе партий поставки (серий) для одной сделки.
|
||||||
- Улучшено сообщение о блокировке запуска «В производство» при отсутствии техпроцесса: показывается модалка и отдельная страница со списком проблемных позиций.
|
- Улучшено сообщение о блокировке запуска «В производство» при отсутствии техпроцесса или материала: показывается модалка (для техпроцесса также есть отдельная страница) со списком проблемных позиций.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Запуск «В производство» блокируется, если в BOM есть узлы без техпроцесса (EntityOperation seq=1), чтобы компоненты не попадали в «без техпроцесса».
|
- Запуск «В производство» блокируется, если в BOM есть узлы без техпроцесса (EntityOperation seq=1), чтобы компоненты не попадали в «без техпроцесса».
|
||||||
|
|||||||
@@ -564,6 +564,49 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if missing_material_rows %}
|
||||||
|
<div class="modal fade" id="missingMaterialModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content border-secondary">
|
||||||
|
<div class="modal-header border-secondary">
|
||||||
|
<h5 class="modal-title">Нельзя запустить: нет материала</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="text-muted mb-2">Заполни material в паспорте(ах) деталей ниже и повтори запуск.</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0 align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-custom-header">
|
||||||
|
<th style="width:110px;">Тип</th>
|
||||||
|
<th style="width:180px;">Обозначение</th>
|
||||||
|
<th>Наименование</th>
|
||||||
|
<th data-sort="false" class="text-end" style="width:140px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in missing_material_rows %}
|
||||||
|
<tr>
|
||||||
|
<td class="small text-muted">{{ r.entity.get_entity_type_display }}</td>
|
||||||
|
<td class="fw-bold">{{ r.entity.drawing_number|default:"—" }}</td>
|
||||||
|
<td>{{ r.entity.name }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<a class="btn btn-outline-secondary btn-sm" href="{{ r.url }}" target="_blank">Открыть</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-secondary">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- productInfoModal удалён: паспорт компонента открывается отдельной страницей -->
|
<!-- productInfoModal удалён: паспорт компонента открывается отдельной страницей -->
|
||||||
<div class="d-none" id="productInfoModal" tabindex="-1" aria-hidden="true">
|
<div class="d-none" id="productInfoModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-xl">
|
<div class="modal-dialog modal-xl">
|
||||||
@@ -1119,5 +1162,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if missing_material_autoshow and missing_material_rows %}
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const el = document.getElementById('missingMaterialModal');
|
||||||
|
if (!el) return;
|
||||||
|
try {
|
||||||
|
const m = new bootstrap.Modal(el);
|
||||||
|
m.show();
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2022,29 +2022,59 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
|||||||
deal = get_object_or_404(Deal.objects.select_related('company'), pk=self.kwargs['pk'])
|
deal = get_object_or_404(Deal.objects.select_related('company'), pk=self.kwargs['pk'])
|
||||||
context['deal'] = deal
|
context['deal'] = deal
|
||||||
|
|
||||||
missing_ctx = None
|
context['missing_tech_process_rows'] = []
|
||||||
|
context['missing_tech_process_details_url'] = ''
|
||||||
|
context['missing_tech_process_autoshow'] = False
|
||||||
|
|
||||||
raw_missing = self.request.session.get('sf_missing_tech_process')
|
raw_missing = self.request.session.get('sf_missing_tech_process')
|
||||||
if raw_missing and int(raw_missing.get('deal_id') or 0) == int(deal.id):
|
if raw_missing and int(raw_missing.get('deal_id') or 0) == int(deal.id):
|
||||||
entity_ids = [int(x) for x in (raw_missing.get('entity_ids') or []) if str(x).isdigit() or isinstance(x, int)]
|
entity_ids = [
|
||||||
|
int(x)
|
||||||
|
for x in (raw_missing.get('entity_ids') or [])
|
||||||
|
if isinstance(x, int) or str(x).isdigit()
|
||||||
|
]
|
||||||
qs = ProductEntity.objects.filter(id__in=entity_ids).order_by('entity_type', 'drawing_number', 'name', 'id')
|
qs = ProductEntity.objects.filter(id__in=entity_ids).order_by('entity_type', 'drawing_number', 'name', 'id')
|
||||||
items = []
|
next_qs = urlencode({'next': self.request.get_full_path()})
|
||||||
|
rows = []
|
||||||
for e in qs:
|
for e in qs:
|
||||||
items.append({
|
rows.append({
|
||||||
'entity': e,
|
'entity': e,
|
||||||
'url': f"{reverse_lazy('product_info', kwargs={'pk': int(e.id)})}?next={self.request.get_full_path()}" ,
|
'url': f"{reverse_lazy('product_info', kwargs={'pk': int(e.id)})}?{next_qs}",
|
||||||
})
|
})
|
||||||
|
|
||||||
missing_ctx = {
|
context['missing_tech_process_rows'] = rows
|
||||||
'items': items,
|
context['missing_tech_process_details_url'] = str(reverse_lazy('deal_missing_tech_process', kwargs={'pk': int(deal.id)}))
|
||||||
'details_url': str(reverse_lazy('deal_missing_tech_process', kwargs={'pk': int(deal.id)})),
|
context['missing_tech_process_autoshow'] = not bool(raw_missing.get('shown'))
|
||||||
'autoshow': not bool(raw_missing.get('shown')),
|
|
||||||
}
|
|
||||||
|
|
||||||
if not bool(raw_missing.get('shown')):
|
if not bool(raw_missing.get('shown')):
|
||||||
raw_missing['shown'] = True
|
raw_missing['shown'] = True
|
||||||
self.request.session['sf_missing_tech_process'] = raw_missing
|
self.request.session['sf_missing_tech_process'] = raw_missing
|
||||||
|
|
||||||
context['missing_tech_process'] = missing_ctx
|
context['missing_material_rows'] = []
|
||||||
|
context['missing_material_autoshow'] = False
|
||||||
|
|
||||||
|
raw_mat = self.request.session.get('sf_missing_material')
|
||||||
|
if raw_mat and int(raw_mat.get('deal_id') or 0) == int(deal.id):
|
||||||
|
entity_ids = [
|
||||||
|
int(x)
|
||||||
|
for x in (raw_mat.get('entity_ids') or [])
|
||||||
|
if isinstance(x, int) or str(x).isdigit()
|
||||||
|
]
|
||||||
|
qs = ProductEntity.objects.filter(id__in=entity_ids).order_by('entity_type', 'drawing_number', 'name', 'id')
|
||||||
|
next_qs = urlencode({'next': self.request.get_full_path()})
|
||||||
|
rows = []
|
||||||
|
for e in qs:
|
||||||
|
rows.append({
|
||||||
|
'entity': e,
|
||||||
|
'url': f"{reverse_lazy('product_info', kwargs={'pk': int(e.id)})}?{next_qs}",
|
||||||
|
})
|
||||||
|
|
||||||
|
context['missing_material_rows'] = rows
|
||||||
|
context['missing_material_autoshow'] = not bool(raw_mat.get('shown'))
|
||||||
|
|
||||||
|
if not bool(raw_mat.get('shown')):
|
||||||
|
raw_mat['shown'] = True
|
||||||
|
self.request.session['sf_missing_material'] = raw_mat
|
||||||
|
|
||||||
di = list(
|
di = list(
|
||||||
DealItem.objects.select_related('entity', 'entity__assembly_passport')
|
DealItem.objects.select_related('entity', 'entity__assembly_passport')
|
||||||
@@ -3514,13 +3544,14 @@ class DealBatchActionView(LoginRequiredMixin, View):
|
|||||||
)
|
)
|
||||||
return redirect(next_url)
|
return redirect(next_url)
|
||||||
|
|
||||||
bad = list(ProductEntity.objects.filter(id__in=list(getattr(ev, 'missing_material_ids', []) or [])).values_list('drawing_number', 'name'))
|
missing_ids = sorted({int(x) for x in (getattr(ev, 'missing_material_ids', []) or [])})
|
||||||
if bad:
|
if missing_ids:
|
||||||
preview = ", ".join([f"{dn or '—'} {nm}" for dn, nm in bad[:5]])
|
request.session['sf_missing_material'] = {
|
||||||
more = '' if len(bad) <= 5 else f" и ещё {len(bad)-5}"
|
'deal_id': int(deal_id),
|
||||||
messages.error(request, f'В спецификации есть детали без материала: {preview}{more}. Добавь material в паспорт(ы) и повтори запуск.')
|
'entity_ids': missing_ids,
|
||||||
else:
|
'shown': False,
|
||||||
messages.error(request, 'В спецификации есть детали без материала. Добавь material и повтори запуск.')
|
}
|
||||||
|
messages.error(request, 'В спецификации есть детали без материала. Добавь material в паспорт(ы) и повтори запуск.')
|
||||||
return redirect(next_url)
|
return redirect(next_url)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('start_batch_item_production: failed deal_id=%s item_id=%s qty=%s', deal_id, item_id, qty)
|
logger.exception('start_batch_item_production: failed deal_id=%s item_id=%s qty=%s', deal_id, item_id, qty)
|
||||||
|
|||||||
Reference in New Issue
Block a user