Починили закрытие сварки, доработали интерфейс
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
This commit is contained in:
@@ -2051,6 +2051,7 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
||||
self.request.session['sf_missing_tech_process'] = raw_missing
|
||||
|
||||
context['missing_material_rows'] = []
|
||||
context['missing_material_details_url'] = ''
|
||||
context['missing_material_autoshow'] = False
|
||||
|
||||
raw_mat = self.request.session.get('sf_missing_material')
|
||||
@@ -2070,6 +2071,7 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
||||
})
|
||||
|
||||
context['missing_material_rows'] = rows
|
||||
context['missing_material_details_url'] = str(reverse_lazy('deal_missing_material', kwargs={'pk': int(deal.id)}))
|
||||
context['missing_material_autoshow'] = not bool(raw_mat.get('shown'))
|
||||
|
||||
if not bool(raw_mat.get('shown')):
|
||||
@@ -2338,6 +2340,47 @@ class DealMissingTechProcessView(LoginRequiredMixin, TemplateView):
|
||||
'url': f"{reverse_lazy('product_info', kwargs={'pk': int(e.id)})}?next={next_url}",
|
||||
})
|
||||
|
||||
ctx['page_title'] = 'Позиции без техпроцесса'
|
||||
ctx['page_hint'] = 'Для запуска в производство нужен техпроцесс (операция seq=1).'
|
||||
ctx['items'] = rows
|
||||
return ctx
|
||||
|
||||
|
||||
class DealMissingMaterialView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'shiftflow/missing_techprocess.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
roles = get_user_roles(request.user)
|
||||
if not has_any_role(roles, ['admin', 'technologist', 'prod_head']):
|
||||
return redirect('registry')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
roles = get_user_roles(self.request.user)
|
||||
ctx['user_roles'] = sorted(roles)
|
||||
ctx['user_role'] = primary_role(roles)
|
||||
|
||||
deal = get_object_or_404(Deal.objects.select_related('company'), pk=int(self.kwargs['pk']))
|
||||
ctx['deal'] = deal
|
||||
|
||||
raw = self.request.session.get('sf_missing_material')
|
||||
entity_ids = []
|
||||
if raw and int(raw.get('deal_id') or 0) == int(deal.id):
|
||||
entity_ids = [int(x) for x in (raw.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_url = str(reverse_lazy('planning_deal', kwargs={'pk': int(deal.id)}))
|
||||
next_qs = urlencode({'next': next_url})
|
||||
rows = []
|
||||
for e in qs:
|
||||
rows.append({
|
||||
'entity': e,
|
||||
'url': f"{reverse_lazy('product_info', kwargs={'pk': int(e.id)})}?{next_qs}",
|
||||
})
|
||||
|
||||
ctx['page_title'] = 'Позиции без материала'
|
||||
ctx['page_hint'] = 'Для запуска в производство у детали должен быть заполнен material в паспорте.'
|
||||
ctx['items'] = rows
|
||||
return ctx
|
||||
|
||||
@@ -3551,7 +3594,7 @@ class DealBatchActionView(LoginRequiredMixin, View):
|
||||
'entity_ids': missing_ids,
|
||||
'shown': False,
|
||||
}
|
||||
messages.error(request, 'В спецификации есть детали без материала. Добавь material в паспорт(ы) и повтори запуск.')
|
||||
messages.error(request, 'В спецификации есть детали без материала. Добавь material в паспорт(ы) и повтори запуск. Список доступен по ссылке в карточке сделки.')
|
||||
return redirect(next_url)
|
||||
except Exception:
|
||||
logger.exception('start_batch_item_production: failed deal_id=%s item_id=%s qty=%s', deal_id, item_id, qty)
|
||||
@@ -4578,7 +4621,77 @@ class WarehouseReceiptCreateView(LoginRequiredMixin, View):
|
||||
|
||||
|
||||
class ShippingView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'shiftflow/shipping.html'
|
||||
template_name = 'shiftflow/shipping_cart.html'
|
||||
|
||||
SESSION_DEALS_KEY = 'shipping_cart_deal_ids'
|
||||
SESSION_QTY_KEY = 'shipping_cart_qty'
|
||||
|
||||
def _get_cart_deal_ids(self) -> list[int]:
|
||||
raw = self.request.session.get(self.SESSION_DEALS_KEY) or []
|
||||
out = []
|
||||
for x in raw:
|
||||
try:
|
||||
v = int(x)
|
||||
except Exception:
|
||||
continue
|
||||
if v > 0:
|
||||
out.append(v)
|
||||
return sorted(set(out))
|
||||
|
||||
def _set_cart_deal_ids(self, ids: list[int]) -> None:
|
||||
cleaned = sorted({int(x) for x in (ids or []) if int(x) > 0})
|
||||
self.request.session[self.SESSION_DEALS_KEY] = cleaned
|
||||
self.request.session.modified = True
|
||||
|
||||
def _get_cart_qty(self) -> dict:
|
||||
raw = self.request.session.get(self.SESSION_QTY_KEY)
|
||||
return raw if isinstance(raw, dict) else {}
|
||||
|
||||
def _set_cart_qty(self, data: dict) -> None:
|
||||
self.request.session[self.SESSION_QTY_KEY] = data if isinstance(data, dict) else {}
|
||||
self.request.session.modified = True
|
||||
|
||||
def _parse_post_qty(self) -> tuple[dict[int, dict[int, int]], dict[int, dict[int, float]]]:
|
||||
ent_by_deal: dict[int, dict[int, int]] = {}
|
||||
mat_by_deal: dict[int, dict[int, float]] = {}
|
||||
|
||||
for k, v in (self.request.POST or {}).items():
|
||||
if not k:
|
||||
continue
|
||||
s = (str(v) if v is not None else '').strip().replace(',', '.')
|
||||
|
||||
if k.startswith('d') and '_ent_' in k:
|
||||
try:
|
||||
left, ent_id_raw = k.split('_ent_', 1)
|
||||
deal_id_raw = left[1:]
|
||||
except Exception:
|
||||
continue
|
||||
if not deal_id_raw.isdigit() or not ent_id_raw.isdigit():
|
||||
continue
|
||||
try:
|
||||
qty = int(float(s)) if s else 0
|
||||
except Exception:
|
||||
qty = 0
|
||||
if qty > 0:
|
||||
ent_by_deal.setdefault(int(deal_id_raw), {})[int(ent_id_raw)] = int(qty)
|
||||
continue
|
||||
|
||||
if k.startswith('d') and '_mat_' in k:
|
||||
try:
|
||||
left, mat_id_raw = k.split('_mat_', 1)
|
||||
deal_id_raw = left[1:]
|
||||
except Exception:
|
||||
continue
|
||||
if not deal_id_raw.isdigit() or not mat_id_raw.isdigit():
|
||||
continue
|
||||
try:
|
||||
qty_f = float(s) if s else 0.0
|
||||
except Exception:
|
||||
qty_f = 0.0
|
||||
if qty_f > 0:
|
||||
mat_by_deal.setdefault(int(deal_id_raw), {})[int(mat_id_raw)] = float(qty_f)
|
||||
|
||||
return ent_by_deal, mat_by_deal
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
@@ -4600,32 +4713,70 @@ class ShippingView(LoginRequiredMixin, TemplateView):
|
||||
ctx['can_edit'] = bool(self.can_edit)
|
||||
ctx['is_readonly'] = bool(self.is_readonly)
|
||||
|
||||
deal_id_raw = (self.request.GET.get('deal_id') or '').strip()
|
||||
deal_id = int(deal_id_raw) if deal_id_raw.isdigit() else None
|
||||
|
||||
shipping_loc, _ = Location.objects.get_or_create(
|
||||
name='Склад отгруженных позиций',
|
||||
defaults={'is_production_area': False},
|
||||
)
|
||||
ctx['shipping_location'] = shipping_loc
|
||||
|
||||
ctx['deals'] = list(Deal.objects.select_related('company').order_by('-id')[:300])
|
||||
add_deal_id_raw = (self.request.GET.get('deal_id') or '').strip()
|
||||
if add_deal_id_raw.isdigit():
|
||||
cart_ids = self._get_cart_deal_ids()
|
||||
cart_ids.append(int(add_deal_id_raw))
|
||||
self._set_cart_deal_ids(cart_ids)
|
||||
|
||||
ctx['selected_deal_id'] = deal_id
|
||||
ctx['available_deals'] = list(
|
||||
Deal.objects.select_related('company').filter(status='work').order_by('-id')[:500]
|
||||
)
|
||||
|
||||
ctx['entity_rows'] = []
|
||||
ctx['material_rows'] = []
|
||||
cart_ids = self._get_cart_deal_ids()
|
||||
deals_by_id = {
|
||||
int(d.id): d
|
||||
for d in Deal.objects.select_related('company').filter(id__in=cart_ids)
|
||||
}
|
||||
|
||||
if deal_id:
|
||||
from shiftflow.services.shipping import build_shipment_rows
|
||||
qty_data = self._get_cart_qty() or {}
|
||||
ent_qty_data = qty_data.get('ent') if isinstance(qty_data.get('ent'), dict) else {}
|
||||
mat_qty_data = qty_data.get('mat') if isinstance(qty_data.get('mat'), dict) else {}
|
||||
|
||||
from shiftflow.services.shipping import build_shipment_rows
|
||||
|
||||
cart = []
|
||||
for deal_id in cart_ids:
|
||||
d = deals_by_id.get(int(deal_id))
|
||||
if not d:
|
||||
continue
|
||||
|
||||
entity_rows, material_rows = build_shipment_rows(
|
||||
deal_id=int(deal_id),
|
||||
shipping_location_id=int(shipping_loc.id),
|
||||
)
|
||||
ctx['entity_rows'] = entity_rows
|
||||
ctx['material_rows'] = material_rows
|
||||
|
||||
ent_sel = ent_qty_data.get(str(deal_id)) if isinstance(ent_qty_data, dict) else None
|
||||
mat_sel = mat_qty_data.get(str(deal_id)) if isinstance(mat_qty_data, dict) else None
|
||||
ent_sel = ent_sel if isinstance(ent_sel, dict) else {}
|
||||
mat_sel = mat_sel if isinstance(mat_sel, dict) else {}
|
||||
|
||||
for r in entity_rows:
|
||||
try:
|
||||
r['selected_qty'] = int(ent_sel.get(str(int(r['entity'].id)), 0))
|
||||
except Exception:
|
||||
r['selected_qty'] = 0
|
||||
|
||||
for r in material_rows:
|
||||
try:
|
||||
r['selected_qty'] = float(mat_sel.get(str(int(r['material'].id)), 0.0))
|
||||
except Exception:
|
||||
r['selected_qty'] = 0.0
|
||||
|
||||
cart.append({
|
||||
'deal': d,
|
||||
'entity_rows': entity_rows,
|
||||
'material_rows': material_rows,
|
||||
})
|
||||
|
||||
ctx['cart'] = cart
|
||||
ctx['journal_url'] = str(reverse_lazy('shipping_journal'))
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -4633,70 +4784,198 @@ class ShippingView(LoginRequiredMixin, TemplateView):
|
||||
messages.error(request, 'Доступ только для просмотра.')
|
||||
return redirect('shipping')
|
||||
|
||||
deal_id_raw = (request.POST.get('deal_id') or '').strip()
|
||||
|
||||
if not deal_id_raw.isdigit():
|
||||
messages.error(request, 'Выбери сделку.')
|
||||
return redirect('shipping')
|
||||
|
||||
deal_id = int(deal_id_raw)
|
||||
action = (request.POST.get('action') or '').strip()
|
||||
|
||||
shipping_loc, _ = Location.objects.get_or_create(
|
||||
name='Склад отгруженных позиций',
|
||||
defaults={'is_production_area': False},
|
||||
)
|
||||
|
||||
entity_qty: dict[int, int] = {}
|
||||
material_qty: dict[int, float] = {}
|
||||
ent_by_deal, mat_by_deal = self._parse_post_qty()
|
||||
qty_data = {
|
||||
'ent': {str(k): {str(kk): int(vv) for kk, vv in (v or {}).items()} for k, v in (ent_by_deal or {}).items()},
|
||||
'mat': {str(k): {str(kk): float(vv) for kk, vv in (v or {}).items()} for k, v in (mat_by_deal or {}).items()},
|
||||
}
|
||||
self._set_cart_qty(qty_data)
|
||||
|
||||
for k, v in request.POST.items():
|
||||
if not k or v is None:
|
||||
continue
|
||||
s = (str(v) or '').strip().replace(',', '.')
|
||||
cart_ids = self._get_cart_deal_ids()
|
||||
|
||||
if k.startswith('ent_'):
|
||||
ent_id_raw = k.replace('ent_', '').strip()
|
||||
if not ent_id_raw.isdigit():
|
||||
continue
|
||||
try:
|
||||
qty = int(float(s)) if s else 0
|
||||
except ValueError:
|
||||
qty = 0
|
||||
if qty > 0:
|
||||
entity_qty[int(ent_id_raw)] = int(qty)
|
||||
if action == 'add_deal':
|
||||
add_id_raw = (request.POST.get('add_deal_id') or '').strip()
|
||||
if not add_id_raw.isdigit():
|
||||
messages.error(request, 'Выбери сделку.')
|
||||
return redirect('shipping')
|
||||
|
||||
if k.startswith('mat_'):
|
||||
mat_id_raw = k.replace('mat_', '').strip()
|
||||
if not mat_id_raw.isdigit():
|
||||
continue
|
||||
try:
|
||||
qty_f = float(s) if s else 0.0
|
||||
except ValueError:
|
||||
qty_f = 0.0
|
||||
if qty_f > 0:
|
||||
material_qty[int(mat_id_raw)] = float(qty_f)
|
||||
add_id = int(add_id_raw)
|
||||
ok = Deal.objects.filter(id=add_id, status='work').exists()
|
||||
if not ok:
|
||||
messages.error(request, 'Сделка не найдена или не в статусе «В работе».')
|
||||
return redirect('shipping')
|
||||
|
||||
if not entity_qty and not material_qty:
|
||||
messages.error(request, 'Укажи количество к отгрузке хотя бы по одной позиции.')
|
||||
return redirect(f"{reverse_lazy('shipping')}?deal_id={deal_id}&from_location_id={from_location_id}")
|
||||
cart_ids.append(int(add_id))
|
||||
self._set_cart_deal_ids(cart_ids)
|
||||
return redirect('shipping')
|
||||
|
||||
if action == 'remove_deal':
|
||||
rem_id_raw = (request.POST.get('remove_deal_id') or '').strip()
|
||||
if rem_id_raw.isdigit():
|
||||
rem_id = int(rem_id_raw)
|
||||
cart_ids = [x for x in cart_ids if int(x) != int(rem_id)]
|
||||
self._set_cart_deal_ids(cart_ids)
|
||||
return redirect('shipping')
|
||||
|
||||
if action != 'ship':
|
||||
return redirect('shipping')
|
||||
|
||||
if not ent_by_deal and not mat_by_deal:
|
||||
messages.error(request, 'Нечего отгружать: выбери позиции и количество.')
|
||||
return redirect('shipping')
|
||||
|
||||
from shiftflow.services.shipping import create_shipment_transfers
|
||||
|
||||
all_transfer_ids: list[int] = []
|
||||
try:
|
||||
ids = create_shipment_transfers(
|
||||
deal_id=int(deal_id),
|
||||
shipping_location_id=int(shipping_loc.id),
|
||||
entity_qty=entity_qty,
|
||||
material_qty=material_qty,
|
||||
user_id=int(request.user.id),
|
||||
)
|
||||
msg = ', '.join([str(i) for i in ids])
|
||||
for deal_id, ent_map in (ent_by_deal or {}).items():
|
||||
mat_map = (mat_by_deal or {}).get(int(deal_id), {})
|
||||
if not ent_map and not mat_map:
|
||||
continue
|
||||
|
||||
ids = create_shipment_transfers(
|
||||
deal_id=int(deal_id),
|
||||
shipping_location_id=int(shipping_loc.id),
|
||||
entity_qty=ent_map,
|
||||
material_qty=mat_map,
|
||||
user_id=int(request.user.id),
|
||||
)
|
||||
all_transfer_ids += [int(x) for x in (ids or [])]
|
||||
|
||||
all_transfer_ids = sorted(set(all_transfer_ids))
|
||||
msg = ', '.join([str(i) for i in all_transfer_ids])
|
||||
messages.success(request, f'Отгрузка оформлена. Документы перемещения: {msg}.')
|
||||
|
||||
self._set_cart_deal_ids([])
|
||||
self._set_cart_qty({})
|
||||
except Exception as e:
|
||||
logger.exception('shipping:error deal_id=%s', deal_id)
|
||||
logger.exception('shipping:error')
|
||||
messages.error(request, f'Ошибка отгрузки: {e}')
|
||||
|
||||
return redirect(f"{reverse_lazy('shipping')}?deal_id={deal_id}")
|
||||
return redirect('shipping')
|
||||
|
||||
|
||||
class ShippingJournalView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'shiftflow/shipping_journal.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
roles = get_user_roles(request.user)
|
||||
if not has_any_role(roles, ['admin', 'clerk', 'manager', 'prod_head', 'director', 'technologist', 'observer']):
|
||||
return redirect('registry')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
roles = get_user_roles(self.request.user)
|
||||
ctx['user_roles'] = sorted(roles)
|
||||
ctx['user_role'] = primary_role(roles)
|
||||
|
||||
shipping_loc, _ = Location.objects.get_or_create(
|
||||
name='Склад отгруженных позиций',
|
||||
defaults={'is_production_area': False},
|
||||
)
|
||||
ctx['shipping_location'] = shipping_loc
|
||||
|
||||
q = (self.request.GET.get('q') or '').strip()
|
||||
start_date = (self.request.GET.get('start_date') or '').strip()
|
||||
end_date = (self.request.GET.get('end_date') or '').strip()
|
||||
filtered = self.request.GET.get('filtered')
|
||||
reset = self.request.GET.get('reset')
|
||||
is_default = (not filtered) or bool(reset)
|
||||
|
||||
if is_default:
|
||||
today = timezone.localdate()
|
||||
start = today - timezone.timedelta(days=14)
|
||||
ctx['start_date'] = start.strftime('%Y-%m-%d')
|
||||
ctx['end_date'] = today.strftime('%Y-%m-%d')
|
||||
else:
|
||||
ctx['start_date'] = start_date
|
||||
ctx['end_date'] = end_date
|
||||
|
||||
ctx['q'] = q
|
||||
|
||||
from warehouse.models import TransferRecord, TransferLine
|
||||
|
||||
trs_qs = (
|
||||
TransferRecord.objects.select_related('from_location', 'to_location', 'sender', 'receiver')
|
||||
.filter(to_location_id=int(shipping_loc.id))
|
||||
)
|
||||
|
||||
start_val = (ctx.get('start_date') or '').strip()
|
||||
end_val = (ctx.get('end_date') or '').strip()
|
||||
if start_val:
|
||||
trs_qs = trs_qs.filter(occurred_at__date__gte=start_val)
|
||||
if end_val:
|
||||
trs_qs = trs_qs.filter(occurred_at__date__lte=end_val)
|
||||
|
||||
if q:
|
||||
matched_tr_ids = (
|
||||
TransferLine.objects.select_related('stock_item', 'stock_item__deal', 'stock_item__deal__company')
|
||||
.filter(transfer__in=trs_qs)
|
||||
.filter(stock_item__deal__isnull=False)
|
||||
.filter(
|
||||
Q(stock_item__deal__number__icontains=q)
|
||||
| Q(stock_item__deal__description__icontains=q)
|
||||
| Q(stock_item__deal__company__name__icontains=q)
|
||||
)
|
||||
.values_list('transfer_id', flat=True)
|
||||
.distinct()
|
||||
)
|
||||
trs_qs = trs_qs.filter(id__in=matched_tr_ids)
|
||||
|
||||
trs = list(trs_qs.order_by('-occurred_at', '-id')[:200])
|
||||
|
||||
tr_ids = [int(x.id) for x in trs]
|
||||
lines = list(
|
||||
TransferLine.objects.select_related(
|
||||
'stock_item',
|
||||
'stock_item__deal',
|
||||
'stock_item__deal__company',
|
||||
'stock_item__entity',
|
||||
'stock_item__material',
|
||||
)
|
||||
.filter(transfer_id__in=tr_ids)
|
||||
.order_by('transfer_id', 'id')
|
||||
)
|
||||
|
||||
by_tr: dict[int, list] = {}
|
||||
for ln in lines:
|
||||
by_tr.setdefault(int(ln.transfer_id), []).append(ln)
|
||||
|
||||
rows = []
|
||||
for tr in trs:
|
||||
lns = by_tr.get(int(tr.id), [])
|
||||
|
||||
deals_by_id: dict[int, dict] = {}
|
||||
for ln in lns:
|
||||
d = getattr(getattr(ln, 'stock_item', None), 'deal', None)
|
||||
if not d or not getattr(d, 'id', None):
|
||||
continue
|
||||
deals_by_id[int(d.id)] = {
|
||||
'number': getattr(d, 'number', '') or '',
|
||||
'description': getattr(d, 'description', '') or '',
|
||||
'company': getattr(getattr(d, 'company', None), 'name', '') or '',
|
||||
}
|
||||
|
||||
deals = list(deals_by_id.values())
|
||||
deals = sorted(deals, key=lambda x: (str(x.get('number') or ''), str(x.get('description') or '')))
|
||||
|
||||
rows.append({
|
||||
'transfer': tr,
|
||||
'deals': deals,
|
||||
'lines': lns,
|
||||
'admin_url': f"/admin/warehouse/transferrecord/{int(tr.id)}/change/",
|
||||
})
|
||||
|
||||
ctx['rows'] = rows
|
||||
return ctx
|
||||
|
||||
|
||||
from shiftflow.services.assembly_closing import get_assembly_closing_info, apply_assembly_closing
|
||||
@@ -4731,7 +5010,14 @@ class AssemblyClosingView(LoginRequiredMixin, TemplateView):
|
||||
ctx.update(info)
|
||||
|
||||
ws_id = getattr(self.workitem, 'workshop_id', None)
|
||||
ctx['workshop_machines'] = list(Machine.objects.filter(workshop_id=ws_id).order_by('name')) if ws_id else []
|
||||
if ws_id:
|
||||
ctx['workshop_machines'] = list(
|
||||
Machine.objects.select_related('workshop').filter(workshop_id=ws_id, machine_type='post').order_by('name')
|
||||
)
|
||||
else:
|
||||
ctx['workshop_machines'] = list(
|
||||
Machine.objects.select_related('workshop').filter(machine_type='post').order_by('workshop__name', 'name')
|
||||
)
|
||||
|
||||
if info.get('error'):
|
||||
messages.warning(self.request, info['error'])
|
||||
@@ -4758,17 +5044,24 @@ class AssemblyClosingView(LoginRequiredMixin, TemplateView):
|
||||
return redirect('assembly_closing', pk=self.workitem.id)
|
||||
|
||||
mid = int(mid_raw)
|
||||
machine = Machine.objects.select_related('workshop').filter(id=mid).first()
|
||||
if not machine or (machine.machine_type or '') != 'post':
|
||||
messages.error(request, 'Выбери пост (тип: Пост).')
|
||||
return redirect('assembly_closing', pk=self.workitem.id)
|
||||
|
||||
ws_id = getattr(self.workitem, 'workshop_id', None)
|
||||
if ws_id:
|
||||
ok = Machine.objects.filter(id=mid, workshop_id=int(ws_id)).exists()
|
||||
else:
|
||||
ok = Machine.objects.filter(id=mid).exists()
|
||||
if not ok:
|
||||
if ws_id and int(machine.workshop_id or 0) != int(ws_id):
|
||||
messages.error(request, 'Выбранный пост не относится к цеху задания.')
|
||||
return redirect('assembly_closing', pk=self.workitem.id)
|
||||
|
||||
WorkItem.objects.filter(id=int(self.workitem.id), machine_id__isnull=True).update(machine_id=mid)
|
||||
self.workitem.machine_id = mid
|
||||
upd = {'machine_id': int(machine.id)}
|
||||
if not getattr(self.workitem, 'workshop_id', None) and getattr(machine, 'workshop_id', None):
|
||||
upd['workshop_id'] = int(machine.workshop_id)
|
||||
|
||||
WorkItem.objects.filter(id=int(self.workitem.id), machine_id__isnull=True).update(**upd)
|
||||
self.workitem.machine_id = int(machine.id)
|
||||
if 'workshop_id' in upd:
|
||||
self.workitem.workshop_id = int(upd['workshop_id'])
|
||||
|
||||
try:
|
||||
apply_assembly_closing(self.workitem.id, qty, request.user.id)
|
||||
@@ -5399,10 +5692,72 @@ class LegacyWriteOffsView(LoginRequiredMixin, TemplateView):
|
||||
)
|
||||
)
|
||||
|
||||
def _calc_mass_kg(material, length_mm, width_mm, qty):
|
||||
if not material or getattr(material, 'mass_per_unit', None) in (None, ''):
|
||||
return None
|
||||
try:
|
||||
mpu = float(material.mass_per_unit)
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
q = float(qty or 0)
|
||||
except Exception:
|
||||
q = 0.0
|
||||
if q <= 0:
|
||||
return None
|
||||
|
||||
try:
|
||||
l = float(length_mm) if length_mm not in (None, '') else None
|
||||
except Exception:
|
||||
l = None
|
||||
try:
|
||||
w = float(width_mm) if width_mm not in (None, '') else None
|
||||
except Exception:
|
||||
w = None
|
||||
|
||||
if l is not None and w is not None:
|
||||
factor = (l * w) / 1_000_000.0
|
||||
elif l is not None:
|
||||
factor = l / 1000.0
|
||||
else:
|
||||
return None
|
||||
|
||||
return factor * mpu * q
|
||||
|
||||
cons_by_report_id = {}
|
||||
rem_by_report_id = {}
|
||||
|
||||
for r in reports:
|
||||
cons = list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else [])
|
||||
for c in cons:
|
||||
mat = None
|
||||
if getattr(c, 'material_id', None):
|
||||
mat = c.material
|
||||
elif getattr(c, 'stock_item_id', None) and getattr(c.stock_item, 'material_id', None):
|
||||
mat = c.stock_item.material
|
||||
|
||||
length_mm = getattr(getattr(c, 'stock_item', None), 'current_length', None)
|
||||
width_mm = getattr(getattr(c, 'stock_item', None), 'current_width', None)
|
||||
c.mass_kg = _calc_mass_kg(mat, length_mm, width_mm, getattr(c, 'quantity', None))
|
||||
|
||||
rems = list(getattr(r, 'remnants', []).all() if hasattr(getattr(r, 'remnants', None), 'all') else [])
|
||||
for rm in rems:
|
||||
mat = getattr(rm, 'material', None)
|
||||
rm.mass_kg = _calc_mass_kg(
|
||||
mat,
|
||||
getattr(rm, 'current_length', None),
|
||||
getattr(rm, 'current_width', None),
|
||||
getattr(rm, 'quantity', None),
|
||||
)
|
||||
|
||||
cons_by_report_id[int(r.id)] = cons
|
||||
rem_by_report_id[int(r.id)] = rems
|
||||
|
||||
report_cards = []
|
||||
for r in reports:
|
||||
consumed = {}
|
||||
for c in list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else []):
|
||||
cons = cons_by_report_id.get(int(r.id), [])
|
||||
for c in cons:
|
||||
mat = None
|
||||
if getattr(c, 'material_id', None):
|
||||
mat = c.material
|
||||
@@ -5431,6 +5786,8 @@ class LegacyWriteOffsView(LoginRequiredMixin, TemplateView):
|
||||
'consumed': consumed,
|
||||
'produced': produced,
|
||||
'remnants': remnants,
|
||||
'consumption_rows': cons_by_report_id.get(int(r.id), []),
|
||||
'remnant_rows': rem_by_report_id.get(int(r.id), []),
|
||||
'tasks': list(getattr(r, 'tasks', []).all() if hasattr(getattr(r, 'tasks', None), 'all') else []),
|
||||
})
|
||||
|
||||
@@ -6246,12 +6603,27 @@ class ProductInfoView(LoginRequiredMixin, TemplateView):
|
||||
if swap_with < 0 or swap_with >= len(ops):
|
||||
return redirect(stay_url)
|
||||
|
||||
a = ops[idx]
|
||||
b = ops[swap_with]
|
||||
a_seq, b_seq = int(a.seq), int(b.seq)
|
||||
EntityOperation.objects.filter(pk=a.id).update(seq=0)
|
||||
EntityOperation.objects.filter(pk=b.id).update(seq=a_seq)
|
||||
EntityOperation.objects.filter(pk=a.id).update(seq=b_seq)
|
||||
with transaction.atomic():
|
||||
qs = EntityOperation.objects.select_for_update().filter(entity_id=entity.id)
|
||||
ops = list(qs.order_by('seq', 'id'))
|
||||
idx = next((i for i, x in enumerate(ops) if int(x.id) == int(eo.id)), None)
|
||||
if idx is None:
|
||||
return redirect(stay_url)
|
||||
|
||||
swap_with = idx - 1 if direction == 'up' else idx + 1
|
||||
if swap_with < 0 or swap_with >= len(ops):
|
||||
return redirect(stay_url)
|
||||
|
||||
ops[idx], ops[swap_with] = ops[swap_with], ops[idx]
|
||||
|
||||
max_seq = qs.aggregate(m=Max('seq'))['m']
|
||||
tmp_base = int(max_seq or 0) + 1000
|
||||
|
||||
for j, x in enumerate(ops, start=1):
|
||||
EntityOperation.objects.filter(pk=x.id).update(seq=tmp_base + j)
|
||||
|
||||
for j, x in enumerate(ops, start=1):
|
||||
EntityOperation.objects.filter(pk=x.id).update(seq=j)
|
||||
|
||||
messages.success(request, 'Порядок обновлён.')
|
||||
return redirect(stay_url)
|
||||
@@ -6333,21 +6705,35 @@ class ProductInfoView(LoginRequiredMixin, TemplateView):
|
||||
valid = set(Operation.objects.filter(id__in=op_ids).values_list('id', flat=True))
|
||||
op_ids = [int(x) for x in op_ids if int(x) in valid]
|
||||
|
||||
EntityOperation.objects.filter(entity_id=entity.id).exclude(operation_id__in=op_ids).delete()
|
||||
with transaction.atomic():
|
||||
qs = EntityOperation.objects.select_for_update().filter(entity_id=entity.id)
|
||||
qs.exclude(operation_id__in=op_ids).delete()
|
||||
|
||||
existing = list(EntityOperation.objects.filter(entity_id=entity.id, operation_id__in=op_ids).order_by('id'))
|
||||
by_op = {int(eo.operation_id): eo for eo in existing}
|
||||
if op_ids:
|
||||
existing = list(qs.filter(operation_id__in=op_ids).order_by('id'))
|
||||
by_op = {int(eo.operation_id): eo for eo in existing}
|
||||
|
||||
if existing:
|
||||
EntityOperation.objects.filter(id__in=[eo.id for eo in existing]).update(seq=0)
|
||||
max_seq = qs.aggregate(m=Max('seq'))['m']
|
||||
tmp_base = int(max_seq or 0) + 1000
|
||||
next_tmp = tmp_base + 1
|
||||
|
||||
for i, op_id in enumerate(op_ids, start=1):
|
||||
eo = by_op.get(int(op_id))
|
||||
if eo:
|
||||
if int(eo.seq or 0) != i:
|
||||
EntityOperation.objects.filter(id=eo.id).update(seq=i)
|
||||
else:
|
||||
EntityOperation.objects.create(entity_id=entity.id, operation_id=int(op_id), seq=i)
|
||||
for op_id in op_ids:
|
||||
if int(op_id) in by_op:
|
||||
continue
|
||||
eo = EntityOperation.objects.create(entity_id=entity.id, operation_id=int(op_id), seq=next_tmp)
|
||||
by_op[int(op_id)] = eo
|
||||
next_tmp += 1
|
||||
|
||||
all_ids = [int(eo.id) for eo in by_op.values() if getattr(eo, 'id', None)]
|
||||
all_ids = sorted(set(all_ids))
|
||||
|
||||
for j, eo_id in enumerate(all_ids, start=1):
|
||||
EntityOperation.objects.filter(pk=eo_id).update(seq=tmp_base + j)
|
||||
|
||||
for i, op_id in enumerate(op_ids, start=1):
|
||||
eo = by_op.get(int(op_id))
|
||||
if eo:
|
||||
EntityOperation.objects.filter(pk=eo.id).update(seq=i)
|
||||
|
||||
messages.success(request, 'Сохранено.')
|
||||
return redirect(stay_url)
|
||||
@@ -6400,19 +6786,81 @@ class WriteOffsView(LoginRequiredMixin, TemplateView):
|
||||
'tasks__task__deal',
|
||||
'tasks__task__material',
|
||||
'consumptions__material',
|
||||
'consumptions__material__category',
|
||||
'consumptions__stock_item__material',
|
||||
'consumptions__stock_item__material__category',
|
||||
'consumptions__stock_item__deal',
|
||||
'results__stock_item__material',
|
||||
'results__stock_item__entity',
|
||||
'results__stock_item__deal',
|
||||
'remnants__material',
|
||||
'remnants__material__category',
|
||||
)
|
||||
)
|
||||
|
||||
def _calc_mass_kg(material, length_mm, width_mm, qty):
|
||||
if not material or getattr(material, 'mass_per_unit', None) in (None, ''):
|
||||
return None
|
||||
try:
|
||||
mpu = float(material.mass_per_unit)
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
q = float(qty or 0)
|
||||
except Exception:
|
||||
q = 0.0
|
||||
if q <= 0:
|
||||
return None
|
||||
|
||||
try:
|
||||
l = float(length_mm) if length_mm not in (None, '') else None
|
||||
except Exception:
|
||||
l = None
|
||||
try:
|
||||
w = float(width_mm) if width_mm not in (None, '') else None
|
||||
except Exception:
|
||||
w = None
|
||||
|
||||
ff = getattr(getattr(material, 'category', None), 'form_factor', '') or ''
|
||||
if ff == 'sheet':
|
||||
if l is None or w is None:
|
||||
return None
|
||||
factor = (l * w) / 1_000_000.0
|
||||
elif ff == 'bar':
|
||||
if l is None:
|
||||
return None
|
||||
factor = l / 1000.0
|
||||
else:
|
||||
if l is not None and w is not None:
|
||||
factor = (l * w) / 1_000_000.0
|
||||
elif l is not None:
|
||||
factor = l / 1000.0
|
||||
else:
|
||||
return None
|
||||
|
||||
return factor * mpu * q
|
||||
|
||||
report_cards = []
|
||||
for r in reports:
|
||||
cons_rows = list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else [])
|
||||
for c in cons_rows:
|
||||
mat = None
|
||||
if getattr(c, 'material_id', None):
|
||||
mat = c.material
|
||||
elif getattr(c, 'stock_item_id', None) and getattr(c.stock_item, 'material_id', None):
|
||||
mat = c.stock_item.material
|
||||
|
||||
length_mm = getattr(getattr(c, 'stock_item', None), 'current_length', None)
|
||||
width_mm = getattr(getattr(c, 'stock_item', None), 'current_width', None)
|
||||
c.mass_kg = _calc_mass_kg(mat, length_mm, width_mm, getattr(c, 'quantity', None))
|
||||
|
||||
rem_rows = list(getattr(r, 'remnants', []).all() if hasattr(getattr(r, 'remnants', None), 'all') else [])
|
||||
for rm in rem_rows:
|
||||
mat = getattr(rm, 'material', None)
|
||||
rm.mass_kg = _calc_mass_kg(mat, getattr(rm, 'current_length', None), getattr(rm, 'current_width', None), getattr(rm, 'quantity', None))
|
||||
|
||||
consumed = {}
|
||||
for c in list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else []):
|
||||
for c in cons_rows:
|
||||
mat = None
|
||||
if getattr(c, 'material_id', None):
|
||||
mat = c.material
|
||||
@@ -6441,6 +6889,8 @@ class WriteOffsView(LoginRequiredMixin, TemplateView):
|
||||
'consumed': consumed,
|
||||
'produced': produced,
|
||||
'remnants': remnants,
|
||||
'consumption_rows': cons_rows,
|
||||
'remnant_rows': rem_rows,
|
||||
'tasks': list(getattr(r, 'tasks', []).all() if hasattr(getattr(r, 'tasks', None), 'all') else []),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user