diff --git a/shiftflow/services/sessions.py b/shiftflow/services/sessions.py index 7842011..4e3db54 100644 --- a/shiftflow/services/sessions.py +++ b/shiftflow/services/sessions.py @@ -1,4 +1,5 @@ from django.db import transaction +from django.utils import timezone from manufacturing.models import ProductEntity @@ -76,7 +77,9 @@ def close_cutting_session(session_id: int) -> None: si.quantity = float(si.quantity) - need if si.quantity == 0: - si.delete() + si.is_archived = True + si.archived_at = timezone.now() + si.save(update_fields=['quantity', 'is_archived', 'archived_at']) else: si.save(update_fields=['quantity']) @@ -126,7 +129,9 @@ def close_cutting_session(session_id: int) -> None: raise RuntimeError('Недостаточно сырья для списания.') if used.quantity == 0: - used.delete() + used.is_archived = True + used.archived_at = timezone.now() + used.save(update_fields=['quantity', 'is_archived', 'archived_at']) else: used.save(update_fields=['quantity']) diff --git a/shiftflow/templates/shiftflow/closing.html b/shiftflow/templates/shiftflow/closing.html index bd299ac..6bbb345 100644 --- a/shiftflow/templates/shiftflow/closing.html +++ b/shiftflow/templates/shiftflow/closing.html @@ -89,6 +89,7 @@ Поступление + Сделка Единица Доступно Использовано @@ -98,6 +99,13 @@ {% for s in stock_items %} {% if s.created_at %}{{ s.created_at|date:"d.m.Y H:i" }}{% endif %} + + {% if s.deal_id %} + {{ s.deal.number }} + {% else %} + — + {% endif %} + {{ s }} {{ s.quantity }} @@ -105,7 +113,7 @@ {% empty %} - Нет единиц на складе для выбранного материала + Нет единиц на складе для выбранного материала {% endfor %} diff --git a/shiftflow/templates/shiftflow/item_detail.html b/shiftflow/templates/shiftflow/item_detail.html index 0042d4c..36a9440 100644 --- a/shiftflow/templates/shiftflow/item_detail.html +++ b/shiftflow/templates/shiftflow/item_detail.html @@ -198,14 +198,6 @@ Назад
- {% if item.status == 'work' and user_role == 'admin' %} - - - {% endif %} diff --git a/shiftflow/templates/shiftflow/writeoffs.html b/shiftflow/templates/shiftflow/writeoffs.html new file mode 100644 index 0000000..2a29cfd --- /dev/null +++ b/shiftflow/templates/shiftflow/writeoffs.html @@ -0,0 +1,161 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+
+

Списание / Производство

+
По производственным отчетам
+
+ +
+ {% for card in report_cards %} +
+
+
+ {{ card.report.date|date:"d.m.Y" }} — {{ card.report.machine }} — {{ card.report.operator }} + #{{ card.report.id }} +
+
+ +
+
+
Списано
+ {% if card.consumed %} +
    + {% for k,v in card.consumed.items %} +
  • {{ k }}: {{ v }}
  • + {% endfor %} +
+ {% else %} +
+ {% endif %} +
+ +
+
Произведено
+ {% if card.produced %} +
    + {% for k,v in card.produced.items %} +
  • {{ k }}: {{ v }}
  • + {% endfor %} +
+ {% else %} +
+ {% endif %} +
+ +
+
ДО
+ {% if card.remnants %} +
    + {% for k,v in card.remnants.items %} +
  • {{ k }}: {{ v }}
  • + {% endfor %} +
+ {% else %} +
+ {% endif %} +
+
+
+ {% empty %} +
За выбранный период отчётов нет.
+ {% endfor %} +
+
+ +
+
+

Сменные задания (1С)

+
Отметка «Списано в 1С»
+
+ +
+ {% csrf_token %} + + + +
+ + + + + + + + + + + + + + + {% for it in items %} + + + + + + + + + + + {% empty %} + + {% endfor %} + +
ДатаСделкаСтанокДетальСтатусФакт
+ {% if can_edit and not it.is_synced_1c %} + + {% endif %} + {{ it.date|date:"d.m.Y" }} + {% if it.task.deal_id %} + {{ it.task.deal.number }} + {% else %} + — + {% endif %} + {{ it.machine }} + {{ it.task.drawing_name }} + {{ it.get_status_display }}{{ it.quantity_fact }} + {% if it.is_synced_1c %} + Да + {% else %} + Нет + {% endif %} +
Нет закрытых заданий за период
+
+ + +
+
+{% endblock %} \ No newline at end of file diff --git a/shiftflow/urls.py b/shiftflow/urls.py index 1457631..2233e69 100644 --- a/shiftflow/urls.py +++ b/shiftflow/urls.py @@ -21,6 +21,7 @@ from .views import ( SteelGradeUpsertView, TaskItemsView, ClosingView, + WriteOffsView, WarehouseReceiptCreateView, WarehouseStocksView, WarehouseTransferCreateView, @@ -58,4 +59,5 @@ urlpatterns = [ path('warehouse/receipt/', WarehouseReceiptCreateView.as_view(), name='warehouse_receipt'), path('closing/', ClosingView.as_view(), name='closing'), + path('writeoffs/', WriteOffsView.as_view(), name='writeoffs'), ] \ No newline at end of file diff --git a/shiftflow/views.py b/shiftflow/views.py index fd412fc..38c62de 100644 --- a/shiftflow/views.py +++ b/shiftflow/views.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from urllib.parse import urlsplit import os @@ -32,7 +32,21 @@ from warehouse.services.transfers import receive_transfer from shiftflow.services.closing import apply_closing from .forms import ProductionTaskCreateForm -from .models import Company, Deal, DxfPreviewJob, DxfPreviewSettings, EmployeeProfile, Item, Machine, ProductionTask +from .models import ( + Company, + CuttingSession, + Deal, + DxfPreviewJob, + DxfPreviewSettings, + EmployeeProfile, + Item, + Machine, + ProductionReportConsumption, + ProductionReportRemnant, + ProductionReportStockResult, + ProductionTask, + ShiftItem, +) def _get_dxf_preview_settings() -> DxfPreviewSettings: @@ -1427,7 +1441,7 @@ class WarehouseStocksView(LoginRequiredMixin, TemplateView): ctx['start_date'] = start_date ctx['end_date'] = end_date - qs = StockItem.objects.select_related('location', 'material', 'material__category', 'entity', 'deal').all() + qs = StockItem.objects.select_related('location', 'material', 'material__category', 'entity', 'deal').filter(is_archived=False) if ship_loc_id: qs = qs.exclude(location_id=ship_loc_id) @@ -1681,7 +1695,8 @@ class ClosingView(LoginRequiredMixin, TemplateView): if work_location_id: stock_items = list( StockItem.objects.select_related('location', 'material') - .filter(location_id=work_location_id, material_id=int(material_id), entity__isnull=True) + .filter(location_id=work_location_id, material_id=int(material_id), entity__isnull=True, is_archived=False) + .filter(quantity__gt=0) .order_by('created_at', 'id') ) @@ -1784,6 +1799,116 @@ class ClosingView(LoginRequiredMixin, TemplateView): messages.error(request, 'Выбери хотя бы один пункт сменки и режим закрытия (полностью/частично).') return redirect(f"{reverse_lazy('closing')}?machine_id={machine_id}&material_id={material_id}") + +class WriteOffsView(LoginRequiredMixin, TemplateView): + template_name = 'shiftflow/writeoffs.html' + + 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', 'clerk', 'observer']: + return redirect('registry') + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + profile = getattr(self.request.user, 'profile', None) + role = profile.role if profile else ('admin' if self.request.user.is_superuser else 'operator') + ctx['user_role'] = role + ctx['can_edit'] = role in ['admin', 'clerk'] + + start_date = (self.request.GET.get('start_date') or '').strip() + end_date = (self.request.GET.get('end_date') or '').strip() + reset = self.request.GET.get('reset') + + if not start_date or not end_date or reset: + today = timezone.localdate() + start = today - timedelta(days=21) + start_date = start.strftime('%Y-%m-%d') + end_date = today.strftime('%Y-%m-%d') + + ctx['start_date'] = start_date + ctx['end_date'] = end_date + + reports_qs = ( + CuttingSession.objects.select_related('machine', 'operator') + .filter(is_closed=True, date__gte=start_date, date__lte=end_date) + .order_by('-date', '-id') + ) + + reports = list( + reports_qs.prefetch_related( + 'tasks__task__deal', + 'tasks__task__material', + 'consumptions__material', + 'consumptions__stock_item__material', + 'results__stock_item__material', + 'results__stock_item__entity', + ) + ) + + report_cards = [] + for r in reports: + consumed = {} + for c in list(getattr(r, 'consumptions', []).all() if hasattr(getattr(r, 'consumptions', None), 'all') else []): + 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 + + label = str(mat) if mat else '—' + key = getattr(mat, 'id', None) or label + consumed[key] = consumed.get(key, 0.0) + float(c.quantity) + + produced = {} + remnants = {} + for res in list(getattr(r, 'results', []).all() if hasattr(getattr(r, 'results', None), 'all') else []): + si = res.stock_item + if res.kind == 'finished': + label = str(getattr(si, 'entity', None) or '—') + produced[label] = produced.get(label, 0.0) + float(si.quantity) + elif res.kind == 'remnant': + label = str(getattr(si, 'material', None) or '—') + remnants[label] = remnants.get(label, 0.0) + float(si.quantity) + + report_cards.append({ + 'report': r, + 'consumed': consumed, + 'produced': produced, + 'remnants': remnants, + 'tasks': list(getattr(r, 'tasks', []).all() if hasattr(getattr(r, 'tasks', None), 'all') else []), + }) + + ctx['report_cards'] = report_cards + + items_qs = ( + Item.objects.select_related('task', 'task__deal', 'machine') + .filter(status__in=['done', 'partial'], date__gte=start_date, date__lte=end_date) + .order_by('-date', '-id') + ) + ctx['items'] = list(items_qs) + return ctx + + def post(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', 'clerk']: + return redirect('writeoffs') + + ids = request.POST.getlist('item_ids') + item_ids = [int(x) for x in ids if x.isdigit()] + if not item_ids: + messages.error(request, 'Не выбрано ни одного сменного задания.') + return redirect('writeoffs') + + Item.objects.filter(id__in=item_ids).update(is_synced_1c=True) + messages.success(request, f'Отмечено в 1С: {len(item_ids)}.') + start_date = (request.POST.get('start_date') or '').strip() + end_date = (request.POST.get('end_date') or '').strip() + return redirect(f"{reverse_lazy('writeoffs')}?start_date={start_date}&end_date={end_date}") + if not consumptions: messages.error(request, 'Заполни списание: укажи, какие единицы на складе использованы и в каком количестве.') return redirect(f"{reverse_lazy('closing')}?machine_id={machine_id}&material_id={material_id}") diff --git a/templates/components/_navbar.html b/templates/components/_navbar.html index 068e55c..a6896fb 100644 --- a/templates/components/_navbar.html +++ b/templates/components/_navbar.html @@ -33,6 +33,11 @@ {% endif %} + {% if user_role in 'admin,clerk,observer' %} + + {% endif %} {% if user_role == 'admin' %}