Открыл мастеру возможность просмотра сделок и потребность в деталях, подправил окно редактирования позиции сделки, подправил работу фильтра
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from django.db.models import Case, ExpressionWrapper, F, IntegerField, Sum, Value, When
|
||||
from django.db.models.functions import Coalesce
|
||||
@@ -37,7 +38,10 @@ class RegistryView(LoginRequiredMixin, ListView):
|
||||
user = self.request.user
|
||||
profile = getattr(user, 'profile', None)
|
||||
role = profile.role if profile else 'operator'
|
||||
# Флаг, что фильтрация была применена через форму. Если нет — используем дефолты
|
||||
filtered = self.request.GET.get('filtered')
|
||||
# Принудительный сброс фильтров (?reset=1) — ведёт себя как первый заход на страницу
|
||||
reset = self.request.GET.get('reset')
|
||||
|
||||
# Станки
|
||||
m_ids = self.request.GET.getlist('m_ids')
|
||||
@@ -59,18 +63,18 @@ class RegistryView(LoginRequiredMixin, ListView):
|
||||
expanded.append(s)
|
||||
queryset = queryset.filter(status__in=expanded)
|
||||
|
||||
# Даты
|
||||
# Диапазон дат, задаваемый пользователем. Если фильтры не активны или явно указан reset=1 — используем дефолты
|
||||
start_date = self.request.GET.get('start_date')
|
||||
end_date = self.request.GET.get('end_date')
|
||||
if not filtered:
|
||||
# Дефолтный режим: последние 7 дней и только статус "В работе"
|
||||
is_default = (not filtered) or bool(reset)
|
||||
|
||||
if is_default:
|
||||
today = timezone.localdate()
|
||||
if role == 'clerk':
|
||||
queryset = queryset.filter(date=today, status__in=['done', 'partial'])
|
||||
elif role in ['operator', 'master']:
|
||||
queryset = queryset.filter(date=today, status__in=['work'])
|
||||
else:
|
||||
queryset = queryset.filter(date=today, status__in=['work', 'leftover'])
|
||||
week_ago = today - timezone.timedelta(days=7)
|
||||
queryset = queryset.filter(date__gte=week_ago, date__lte=today, status__in=['work'])
|
||||
else:
|
||||
# Пользователь указал фильтры вручную — применяем их как есть
|
||||
if start_date:
|
||||
queryset = queryset.filter(date__gte=start_date)
|
||||
if end_date:
|
||||
@@ -103,16 +107,15 @@ class RegistryView(LoginRequiredMixin, ListView):
|
||||
context['machines'] = machines
|
||||
filtered = self.request.GET.get('filtered')
|
||||
|
||||
if not filtered:
|
||||
today_str = timezone.localdate().strftime('%Y-%m-%d')
|
||||
context['start_date'] = today_str
|
||||
context['end_date'] = today_str
|
||||
if role == 'clerk':
|
||||
context['selected_statuses'] = ['closed']
|
||||
elif role in ['operator', 'master']:
|
||||
context['selected_statuses'] = ['work']
|
||||
else:
|
||||
context['selected_statuses'] = ['work', 'leftover']
|
||||
reset = self.request.GET.get('reset')
|
||||
# Дефолтное состояние формы фильтра: все станки включены, статус "В работе",
|
||||
# период от сегодня−7 до сегодня. Совпадает с серверной выборкой выше
|
||||
if (not filtered) or reset:
|
||||
today = timezone.localdate()
|
||||
week_ago = today - timezone.timedelta(days=7)
|
||||
context['start_date'] = week_ago.strftime('%Y-%m-%d')
|
||||
context['end_date'] = today.strftime('%Y-%m-%d')
|
||||
context['selected_statuses'] = ['work']
|
||||
context['selected_machines'] = [m.id for m in machines]
|
||||
context['all_selected_machines'] = True
|
||||
else:
|
||||
@@ -123,6 +126,19 @@ class RegistryView(LoginRequiredMixin, ListView):
|
||||
context['is_synced'] = self.request.GET.get('is_synced', '')
|
||||
context['all_selected_machines'] = False
|
||||
|
||||
items = list(context.get('items') or [])
|
||||
for it in items:
|
||||
plan = int(it.quantity_plan or 0)
|
||||
fact = int(it.quantity_fact or 0)
|
||||
if plan > 0:
|
||||
fact_pct = int(round(fact * 100 / plan))
|
||||
else:
|
||||
fact_pct = 0
|
||||
it.fact_pct = fact_pct
|
||||
it.fact_width = max(0, min(100, fact_pct))
|
||||
it.fact_bar_class = 'bg-success' if it.status in ['done', 'partial'] else 'bg-warning'
|
||||
context['items'] = items
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@@ -222,7 +238,7 @@ class PlanningView(LoginRequiredMixin, TemplateView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||
if role not in ['admin', 'technologist']:
|
||||
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||
return redirect('registry')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -249,7 +265,7 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||
if role not in ['admin', 'technologist']:
|
||||
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||
return redirect('registry')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -282,6 +298,8 @@ class DealPlanningView(LoginRequiredMixin, TemplateView):
|
||||
).order_by('-id')
|
||||
|
||||
tasks = list(tasks_qs)
|
||||
# Рассчитываем показатели прогресса для визуализации:
|
||||
# done_pct/plan_pct — проценты от "Надо"; done_width/plan_width — ширины сегментов бары, ограниченные 0..100
|
||||
for t in tasks:
|
||||
need = int(t.quantity_ordered or 0)
|
||||
done_qty = int(t.done_qty or 0)
|
||||
@@ -313,7 +331,7 @@ class TaskItemsView(LoginRequiredMixin, TemplateView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||
if role not in ['admin', 'technologist']:
|
||||
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||
return redirect('registry')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -329,7 +347,17 @@ class TaskItemsView(LoginRequiredMixin, TemplateView):
|
||||
)
|
||||
context['task'] = task
|
||||
|
||||
items = Item.objects.filter(task=task).select_related('machine').order_by('-date', 'machine__name', '-id')
|
||||
items = list(Item.objects.filter(task=task).select_related('machine').order_by('-date', 'machine__name', '-id'))
|
||||
for it in items:
|
||||
plan = int(it.quantity_plan or 0)
|
||||
fact = int(it.quantity_fact or 0)
|
||||
if plan > 0:
|
||||
fact_pct = int(round(fact * 100 / plan))
|
||||
else:
|
||||
fact_pct = 0
|
||||
it.fact_pct = fact_pct
|
||||
it.fact_width = max(0, min(100, fact_pct))
|
||||
it.fact_bar_class = 'bg-success' if it.status in ['done', 'partial'] else 'bg-warning'
|
||||
context['items'] = items
|
||||
return context
|
||||
|
||||
@@ -340,7 +368,7 @@ class CustomersView(LoginRequiredMixin, TemplateView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||
if role not in ['admin', 'technologist']:
|
||||
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||
return redirect('registry')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -361,7 +389,7 @@ class CustomerDealsView(LoginRequiredMixin, TemplateView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||
if role not in ['admin', 'technologist']:
|
||||
if role not in ['admin', 'technologist', 'master', 'clerk']:
|
||||
return redirect('registry')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -667,6 +695,22 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator')
|
||||
context['user_role'] = role
|
||||
context['machines'] = Machine.objects.all()
|
||||
|
||||
# Вычисляем URL "Назад": приоритетно берём ?next=..., иначе пробуем Referer
|
||||
# Используем только ссылки на текущий хост, чтобы избежать внешних редиректов
|
||||
next_url = (self.request.GET.get('next') or '').strip()
|
||||
back_url = ''
|
||||
if next_url.startswith('/'):
|
||||
back_url = next_url
|
||||
else:
|
||||
ref = (self.request.META.get('HTTP_REFERER') or '').strip()
|
||||
if ref:
|
||||
parts = urlsplit(ref)
|
||||
if parts.netloc == self.request.get_host():
|
||||
back_url = parts.path + (('?' + parts.query) if parts.query else '')
|
||||
if not back_url:
|
||||
back_url = str(reverse_lazy('registry'))
|
||||
context['back_url'] = back_url
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@@ -674,7 +718,24 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
profile = getattr(request.user, 'profile', None)
|
||||
role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator')
|
||||
|
||||
# Поддерживаем "умный" возврат после действия: ?next=... или Referer
|
||||
next_url = (request.POST.get('next') or '').strip()
|
||||
if not next_url:
|
||||
ref = (request.META.get('HTTP_REFERER') or '').strip()
|
||||
if ref:
|
||||
parts = urlsplit(ref)
|
||||
if parts.netloc == request.get_host():
|
||||
next_url = parts.path + (('?' + parts.query) if parts.query else '')
|
||||
|
||||
def redirect_back():
|
||||
# Возвращаемся туда, откуда пришли, иначе в реестр
|
||||
if next_url.startswith('/'):
|
||||
return redirect(next_url)
|
||||
return redirect('registry')
|
||||
|
||||
if role in ['admin', 'technologist']:
|
||||
action = request.POST.get('action', 'save')
|
||||
|
||||
machine_id = request.POST.get('machine')
|
||||
if machine_id and machine_id.isdigit():
|
||||
self.object.machine_id = int(machine_id)
|
||||
@@ -691,11 +752,6 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
if quantity_fact and quantity_fact.isdigit():
|
||||
self.object.quantity_fact = int(quantity_fact)
|
||||
|
||||
status = request.POST.get('status')
|
||||
allowed_statuses = {k for k, _ in self.object.STATUS_CHOICES}
|
||||
if status in allowed_statuses:
|
||||
self.object.status = status
|
||||
|
||||
self.object.is_synced_1c = bool(request.POST.get('is_synced_1c'))
|
||||
self.object.material_taken = request.POST.get('material_taken', self.object.material_taken)
|
||||
self.object.usable_waste = request.POST.get('usable_waste', self.object.usable_waste)
|
||||
@@ -707,35 +763,70 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Действия закрытия для админа/технолога
|
||||
if action == 'close_done' and self.object.status == 'work':
|
||||
self.object.quantity_fact = self.object.quantity_plan
|
||||
self.object.status = 'done'
|
||||
self.object.save()
|
||||
return redirect_back()
|
||||
|
||||
if action == 'close_partial' and self.object.status == 'work':
|
||||
try:
|
||||
fact = int(request.POST.get('quantity_fact', '0'))
|
||||
except ValueError:
|
||||
fact = 0
|
||||
fact = max(0, min(fact, self.object.quantity_plan))
|
||||
residual = self.object.quantity_plan - fact
|
||||
self.object.quantity_fact = fact
|
||||
self.object.status = 'partial'
|
||||
self.object.save()
|
||||
if residual > 0:
|
||||
Item.objects.create(
|
||||
task=self.object.task,
|
||||
date=self.object.date,
|
||||
machine=self.object.machine,
|
||||
quantity_plan=residual,
|
||||
quantity_fact=0,
|
||||
status='leftover',
|
||||
is_synced_1c=False,
|
||||
)
|
||||
return redirect_back()
|
||||
|
||||
self.object.save()
|
||||
return redirect('registry')
|
||||
return redirect_back()
|
||||
|
||||
if role in ['operator', 'master']:
|
||||
action = request.POST.get('action', 'save')
|
||||
material_taken = (request.POST.get('material_taken') or '').strip()
|
||||
usable_waste = (request.POST.get('usable_waste') or '').strip()
|
||||
scrap_weight_raw = (request.POST.get('scrap_weight') or '').strip()
|
||||
status = request.POST.get('status', self.object.status)
|
||||
|
||||
machine_changed = False
|
||||
if role == 'master':
|
||||
machine_id = request.POST.get('machine')
|
||||
if machine_id and machine_id.isdigit():
|
||||
self.object.machine_id = int(machine_id)
|
||||
machine_changed = True
|
||||
if action == 'save':
|
||||
qf = request.POST.get('quantity_fact')
|
||||
if qf and qf.isdigit():
|
||||
self.object.quantity_fact = int(qf)
|
||||
machine_changed = False
|
||||
if role == 'master':
|
||||
machine_id = request.POST.get('machine')
|
||||
if machine_id and machine_id.isdigit():
|
||||
self.object.machine_id = int(machine_id)
|
||||
machine_changed = True
|
||||
fields = ['quantity_fact']
|
||||
if machine_changed:
|
||||
fields.append('machine')
|
||||
self.object.save(update_fields=fields)
|
||||
return redirect_back()
|
||||
|
||||
# Разрешаем мастеру редактировать операторские поля всегда,
|
||||
# оператору — только в процессе закрытия
|
||||
if role == 'operator' and self.object.status != 'work':
|
||||
return redirect('registry')
|
||||
if self.object.status != 'work':
|
||||
return redirect_back()
|
||||
|
||||
errors = []
|
||||
if role == 'operator' and self.object.status == 'work':
|
||||
if not material_taken:
|
||||
errors.append('Заполни поле "Взятый материал"')
|
||||
if not usable_waste:
|
||||
errors.append('Заполни поле "Остаток ДО"')
|
||||
if scrap_weight_raw == '':
|
||||
errors.append('Заполни поле "Лом (кг)" (можно 0)')
|
||||
if not material_taken:
|
||||
errors.append('Заполни поле "Взятый материал"')
|
||||
if not usable_waste:
|
||||
errors.append('Заполни поле "Остаток ДО"')
|
||||
if scrap_weight_raw == '':
|
||||
errors.append('Заполни поле "Лом (кг)" (можно 0)')
|
||||
|
||||
scrap_weight = None
|
||||
if scrap_weight_raw != '':
|
||||
@@ -749,21 +840,18 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
context['errors'] = errors
|
||||
return self.render_to_response(context)
|
||||
|
||||
if material_taken:
|
||||
self.object.material_taken = material_taken
|
||||
if usable_waste:
|
||||
self.object.usable_waste = usable_waste
|
||||
self.object.material_taken = material_taken
|
||||
self.object.usable_waste = usable_waste
|
||||
if scrap_weight is not None:
|
||||
self.object.scrap_weight = scrap_weight
|
||||
|
||||
# Логика закрытия доступна и мастеру, и оператору, но только из 'work'
|
||||
if self.object.status == 'work' and status == 'done':
|
||||
if action == 'close_done':
|
||||
self.object.quantity_fact = self.object.quantity_plan
|
||||
self.object.status = 'done'
|
||||
self.object.save()
|
||||
return redirect('registry')
|
||||
return redirect_back()
|
||||
|
||||
if self.object.status == 'work' and status == 'partial':
|
||||
if action == 'close_partial':
|
||||
try:
|
||||
fact = int(request.POST.get('quantity_fact', '0'))
|
||||
except ValueError:
|
||||
@@ -789,24 +877,18 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
status='leftover',
|
||||
is_synced_1c=False,
|
||||
)
|
||||
return redirect_back()
|
||||
|
||||
return redirect('registry')
|
||||
|
||||
# Если статус не менялся (или не 'work'), просто сохраняем поля
|
||||
update_fields = ['material_taken', 'usable_waste', 'scrap_weight']
|
||||
if machine_changed:
|
||||
update_fields.append('machine')
|
||||
self.object.save(update_fields=update_fields)
|
||||
return redirect('registry')
|
||||
return redirect_back()
|
||||
|
||||
if role == 'clerk':
|
||||
if self.object.status not in ['done', 'partial']:
|
||||
return redirect('registry')
|
||||
return redirect_back()
|
||||
self.object.is_synced_1c = bool(request.POST.get('is_synced_1c'))
|
||||
self.object.save(update_fields=['is_synced_1c'])
|
||||
return redirect('registry')
|
||||
return redirect_back()
|
||||
|
||||
return redirect('registry')
|
||||
return redirect_back()
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('registry')
|
||||
Reference in New Issue
Block a user