diff --git a/shiftflow/migrations/0008_alter_item_date.py b/shiftflow/migrations/0008_alter_item_date.py new file mode 100644 index 0000000..e0bc8d9 --- /dev/null +++ b/shiftflow/migrations/0008_alter_item_date.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.3 on 2026-03-29 21:15 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shiftflow', '0007_machine_machine_type'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='date', + field=models.DateField(default=django.utils.timezone.localdate, verbose_name='Дата смены'), + ), + ] diff --git a/shiftflow/models.py b/shiftflow/models.py index 2eabeeb..79251bd 100644 --- a/shiftflow/models.py +++ b/shiftflow/models.py @@ -90,7 +90,7 @@ class Item(models.Model): task = models.ForeignKey(ProductionTask, on_delete=models.CASCADE, related_name='items', verbose_name="Задание", null=True, blank=True) # --- Смена (заполняет мастер) --- - date = models.DateField("Дата смены", default=timezone.now) + date = models.DateField("Дата смены", default=timezone.localdate) machine = models.ForeignKey(Machine, on_delete=models.PROTECT, verbose_name="Станок") quantity_plan = models.PositiveIntegerField("План на смену, шт") diff --git a/shiftflow/templates/shiftflow/item_detail.html b/shiftflow/templates/shiftflow/item_detail.html index 6927229..397f7c8 100644 --- a/shiftflow/templates/shiftflow/item_detail.html +++ b/shiftflow/templates/shiftflow/item_detail.html @@ -12,6 +12,14 @@
{% csrf_token %} + + {% if errors %} +
+ {% for e in errors %} +
{{ e }}
+ {% endfor %} +
+ {% endif %}
@@ -56,15 +64,15 @@
- +
- - + +
- +
@@ -74,10 +82,27 @@ {% endif %} {% endif %} - {% if user_role == 'admin' %} + {% if user_role in 'admin,technologist' %}
+
+ + +
+
+ + +
+
+ + +
+
- +
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
{% endif %} - {% if user_role in 'admin,clerk' %} - {% if item.status == 'done' or item.quantity_fact > 0 %} + {% if user_role == 'clerk' %} +
+
+ Взятый материал + {{ item.material_taken|default:"-" }} +
+
+ Остаток ДО + {{ item.usable_waste|default:"-" }} +
+
+ Лом (кг) + {{ item.scrap_weight }} +
+
+ + {% if item.status == 'done' or item.status == 'partial' %}
{% else %} -
Списание будет доступно после выполнения.
+
Списание будет доступно после закрытия (Выполнено/Частично).
{% endif %} - {% if user_role == 'clerk' %}{% endif %} + {% endif %}
diff --git a/shiftflow/templates/shiftflow/partials/_filter.html b/shiftflow/templates/shiftflow/partials/_filter.html index e31caf0..471a8b0 100644 --- a/shiftflow/templates/shiftflow/partials/_filter.html +++ b/shiftflow/templates/shiftflow/partials/_filter.html @@ -22,8 +22,11 @@
Статус:
{% if user_role == 'operator' %} - - В работе + + + + + {% else %} @@ -72,7 +75,10 @@ document.addEventListener('DOMContentLoaded', function(){ const s = document.querySelector('input[name="start_date"]'); const e = document.querySelector('input[name="end_date"]'); - const today = new Date().toISOString().slice(0,10); + const now = new Date(); + const mm = String(now.getMonth() + 1).padStart(2, '0'); + const dd = String(now.getDate()).padStart(2, '0'); + const today = `${now.getFullYear()}-${mm}-${dd}`; if (s && !s.value) s.value = today; if (e && !e.value) e.value = today; }); diff --git a/shiftflow/templates/shiftflow/registry.html b/shiftflow/templates/shiftflow/registry.html index f50f69b..8e13a45 100644 --- a/shiftflow/templates/shiftflow/registry.html +++ b/shiftflow/templates/shiftflow/registry.html @@ -63,7 +63,7 @@ {% endif %} - + {{ item.get_status_display }} diff --git a/shiftflow/views.py b/shiftflow/views.py index 8591b0b..908fbf8 100644 --- a/shiftflow/views.py +++ b/shiftflow/views.py @@ -55,8 +55,13 @@ class RegistryView(LoginRequiredMixin, ListView): start_date = self.request.GET.get('start_date') end_date = self.request.GET.get('end_date') if not filtered: - today = timezone.now().date() - queryset = queryset.filter(date=today, status__in=['work', 'leftover']) + 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']) else: if start_date: queryset = queryset.filter(date__gte=start_date) @@ -71,7 +76,9 @@ class RegistryView(LoginRequiredMixin, ListView): # Ограничения по ролям if role == 'operator': user_machines = profile.machines.all() if profile else Machine.objects.none() - queryset = queryset.filter(machine__in=user_machines, status='work') + queryset = queryset.filter(machine__in=user_machines) + if not filtered: + queryset = queryset.filter(status='work') elif role == 'master' and not filtered: queryset = queryset.filter(status='work') @@ -89,10 +96,15 @@ class RegistryView(LoginRequiredMixin, ListView): filtered = self.request.GET.get('filtered') if not filtered: - today_str = timezone.now().date().strftime('%Y-%m-%d') + today_str = timezone.localdate().strftime('%Y-%m-%d') context['start_date'] = today_str context['end_date'] = today_str - context['selected_statuses'] = ['work', 'leftover'] + if role == 'clerk': + context['selected_statuses'] = ['closed'] + elif role in ['operator', 'master']: + context['selected_statuses'] = ['work'] + else: + context['selected_statuses'] = ['work', 'leftover'] context['selected_machines'] = [m.id for m in machines] context['all_selected_machines'] = True else: @@ -147,7 +159,7 @@ class RegistryPrintView(LoginRequiredMixin, TemplateView): start_date = self.request.GET.get('start_date') end_date = self.request.GET.get('end_date') if not filtered: - today = timezone.now().date() + today = timezone.localdate() queryset = queryset.filter(date=today, status__in=['work', 'leftover']) start_date = today.strftime('%Y-%m-%d') end_date = start_date @@ -210,38 +222,110 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - # Обязательно добавляем роль в контекст этого шаблона! - if hasattr(self.request.user, 'profile'): - context['user_role'] = self.request.user.profile.role + profile = getattr(self.request.user, 'profile', None) + role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator') + context['user_role'] = role + context['machines'] = Machine.objects.all() return context def post(self, request, *args, **kwargs): self.object = self.get_object() profile = getattr(request.user, 'profile', None) - role = profile.role if profile else 'operator' + role = profile.role if profile else ('admin' if request.user.is_superuser else 'operator') - # Общие поля - 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) - self.object.scrap_weight = request.POST.get('scrap_weight', self.object.scrap_weight or 0) + if role in ['admin', 'technologist']: + machine_id = request.POST.get('machine') + if machine_id and machine_id.isdigit(): + self.object.machine_id = int(machine_id) - status = request.POST.get('status', self.object.status) + date_value = request.POST.get('date') + if date_value: + self.object.date = date_value + + quantity_plan = request.POST.get('quantity_plan') + if quantity_plan and quantity_plan.isdigit(): + self.object.quantity_plan = int(quantity_plan) + + quantity_fact = request.POST.get('quantity_fact') + 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) + + scrap_weight = request.POST.get('scrap_weight') + if scrap_weight is not None and scrap_weight != '': + try: + self.object.scrap_weight = float(scrap_weight) + except ValueError: + pass + + self.object.save() + return redirect('registry') if role in ['operator', 'master']: + if self.object.status != 'work': + return redirect('registry') + + 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() + + errors = [] + 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 != '': + try: + scrap_weight = float(scrap_weight_raw) + except ValueError: + errors.append('Поле "Лом (кг)" должно быть числом') + + status = request.POST.get('status', self.object.status) + + if errors: + context = self.get_context_data() + context['errors'] = errors + return self.render_to_response(context) + + self.object.material_taken = material_taken + self.object.usable_waste = usable_waste + if scrap_weight is not None: + self.object.scrap_weight = scrap_weight + if status == 'done': self.object.quantity_fact = self.object.quantity_plan self.object.status = 'done' self.object.save() - elif status == 'partial': + return redirect('registry') + + if status == 'partial': try: fact = int(request.POST.get('quantity_fact', '0')) except ValueError: fact = 0 + if fact <= 0: + context = self.get_context_data() + context['errors'] = ['При частичном закрытии укажи, сколько сделано (больше 0)'] + return self.render_to_response(context) 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, @@ -252,15 +336,17 @@ class ItemUpdateView(LoginRequiredMixin, UpdateView): status='leftover', is_synced_1c=False, ) - else: - # Просто сохранить без спец-логики - return super().post(request, *args, **kwargs) - elif role == 'clerk': - # Учетчик может отмечать списание 1С + + return redirect('registry') + + return redirect('registry') + + if role == 'clerk': + if self.object.status not in ['done', 'partial']: + return redirect('registry') self.object.is_synced_1c = bool(request.POST.get('is_synced_1c')) - self.object.save() - else: - return super().post(request, *args, **kwargs) + self.object.save(update_fields=['is_synced_1c']) + return redirect('registry') return redirect('registry') diff --git a/warehouse/migrations/0003_alter_material_full_name.py b/warehouse/migrations/0003_alter_material_full_name.py new file mode 100644 index 0000000..8d7adea --- /dev/null +++ b/warehouse/migrations/0003_alter_material_full_name.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.3 on 2026-03-29 21:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('warehouse', '0002_materialcategory_gost_standard'), + ] + + operations = [ + migrations.AlterField( + model_name='material', + name='full_name', + field=models.CharField(blank=True, help_text='Генерируется автоматически', max_length=500, verbose_name='Полное наименование'), + ), + ]