diff --git a/.trae/rules/main.md b/.trae/rules/main.md index beb7714..2255cc5 100644 --- a/.trae/rules/main.md +++ b/.trae/rules/main.md @@ -62,4 +62,13 @@ - На изменяемые складские остатки используй select_for_update() чтобы избежать гонок. - Для массовых операций избегай N+1: - select_related / prefetch_related - - bulk update/create там, где это безопасно. \ No newline at end of file + - bulk update/create там, где это безопасно. + + + Правило для новых внутренних функций (как договор): + +- Всегда берём логгер logger = logging.getLogger('mes') +- Перед выполнением — logger.info('fn:start ...', ключевые параметры) +- После успешного выполнения — logger.info('fn:done ...', ключевые результаты) +- На важных шагах — logger.info('fn:step ...', детали) +- Исключение — с context: logger.exception('fn:error ...') — не глотаем, пробрасываем дальше \ No newline at end of file diff --git a/TODO.md b/TODO.md index a22313d..d6ddf7c 100644 --- a/TODO.md +++ b/TODO.md @@ -10,4 +10,7 @@ - Доработать страницу «Списание»: фильтры, удобная сводка по материалам/изделиям и отметка «внесено в 1С». ## Потребность (Материалы) -- Пересмотреть расчёт потребности: уйти от м²/мм, формировать пачки DXF по материалам/толщинам и прокат по длинам (для nesting/ручного расчёта). \ No newline at end of file +- Пересмотреть расчёт потребности: уйти от м²/мм, формировать пачки DXF по материалам/толщинам и прокат по длинам (для nesting/ручного расчёта). + +## Изделия (Сборка) +- Проработать интерфейс сборки изделия: редактирование состава, паспорт узла, маршруты, сварные швы, быстрые переходы по уровням. \ No newline at end of file diff --git a/core/settings.py b/core/settings.py index db1e37e..3f92579 100644 --- a/core/settings.py +++ b/core/settings.py @@ -13,6 +13,8 @@ https://docs.djangoproject.com/en/6.0/ref/settings/ import os from pathlib import Path import environ +import logging +from logging.handlers import RotatingFileHandler # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -178,6 +180,46 @@ STATICFILES_DIRS = [BASE_DIR / 'static'] MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'media' +# Логирование (единый файл на сервер) +LOG_DIR = BASE_DIR / 'logs' +try: + os.makedirs(LOG_DIR, exist_ok=True) +except Exception: + pass + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'simple': { + 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' + }, + }, + 'handlers': { + 'mes_file': { + 'class': 'logging.handlers.RotatingFileHandler', + 'level': 'INFO', + 'filename': str(LOG_DIR / 'mes.log'), + 'maxBytes': 5 * 1024 * 1024, + 'backupCount': 3, + 'encoding': 'utf-8', + 'formatter': 'simple', + }, + 'console': { + 'class': 'logging.StreamHandler', + 'level': 'INFO', + 'formatter': 'simple', + }, + }, + 'loggers': { + 'mes': { + 'handlers': ['mes_file', 'console'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} + # Куда переходим после логина LOGIN_REDIRECT_URL = '/' # Куда идем после входа LOGOUT_REDIRECT_URL = '/' # Куда идем после выхода diff --git a/shiftflow/services/closing.py b/shiftflow/services/closing.py index 32c9e9a..5f41d81 100644 --- a/shiftflow/services/closing.py +++ b/shiftflow/services/closing.py @@ -1,5 +1,6 @@ from django.db import transaction from django.utils import timezone +import logging from shiftflow.models import ( CuttingSession, @@ -9,7 +10,8 @@ from shiftflow.models import ( ShiftItem, ) from shiftflow.services.sessions import close_cutting_session -from warehouse.models import StockItem + +logger = logging.getLogger('mes') @transaction.atomic @@ -22,12 +24,15 @@ def apply_closing( consumptions: dict[int, float], remnants: list[dict], ) -> None: + logger.info('apply_closing:start user=%s machine=%s material=%s items=%s consumptions=%s remnants=%s', user_id, machine_id, material_id, list(item_actions.keys()), list(consumptions.keys()), len(remnants)) + items = list( Item.objects.select_for_update(of=('self',)) .select_related('task', 'task__deal', 'task__material', 'machine') .filter(id__in=list(item_actions.keys()), machine_id=machine_id, status='work', task__material_id=material_id) ) if not items: + logger.error('apply_closing:no_items machine=%s material=%s', machine_id, material_id) raise RuntimeError('Не найдено пунктов сменки для закрытия.') report = CuttingSession.objects.create( @@ -38,6 +43,8 @@ def apply_closing( is_closed=False, ) + logger.info('apply_closing:report_created id=%s', report.id) + logger.info('apply_closing:update_items start items=%s', [it.id for it in items]) for it in items: spec = item_actions.get(it.id) or {} action = (spec.get('action') or '').strip() @@ -59,6 +66,7 @@ def apply_closing( ShiftItem.objects.create(session=report, task=it.task, quantity_fact=fact) + logger.info('apply_closing:consumption_count=%s', len(consumptions)) for stock_item_id, qty in consumptions.items(): if qty <= 0: continue @@ -69,6 +77,7 @@ def apply_closing( quantity=float(qty), ) + logger.info('apply_closing:remnants_count=%s', len(remnants)) for r in remnants: qty = float(r.get('quantity') or 0) if qty <= 0: @@ -82,6 +91,7 @@ def apply_closing( unique_id=None, ) + logger.info('apply_closing:close_session id=%s', report.id) close_cutting_session(report.id) for it in items: @@ -117,4 +127,6 @@ def apply_closing( quantity_fact=0, status='leftover', is_synced_1c=False, - ) \ No newline at end of file + ) + + logger.info('apply_closing:done report=%s', report.id) diff --git a/shiftflow/services/sessions.py b/shiftflow/services/sessions.py index 4e3db54..c950ec1 100644 --- a/shiftflow/services/sessions.py +++ b/shiftflow/services/sessions.py @@ -1,4 +1,5 @@ from django.db import transaction +import logging from django.utils import timezone from manufacturing.models import ProductEntity @@ -12,6 +13,8 @@ from shiftflow.models import ( ) from warehouse.models import StockItem +logger = logging.getLogger('mes') + @transaction.atomic def close_cutting_session(session_id: int) -> None: @@ -26,6 +29,7 @@ def close_cutting_session(session_id: int) -> None: - для каждого ShiftItem создаём StockItem(entity=..., location=machine.location, quantity=quantity_fact) - если использованный материал не совпадает с planned_material КД -> material_substitution=True """ + logger.info('close_cutting_session:start id=%s', session_id) session = ( CuttingSession.objects.select_for_update(of=('self',)) .select_related( @@ -66,6 +70,8 @@ def close_cutting_session(session_id: int) -> None: if c.stock_item_id: si = StockItem.objects.select_for_update(of=('self',)).select_related('material', 'location').get(pk=c.stock_item_id) + logger.info('close_cutting_session:consume stock_item=%s qty=%s before=%s', si.id, c.quantity, si.quantity) + if not si.material_id: raise RuntimeError('В списании сырья указана позиция склада без material.') @@ -80,6 +86,7 @@ def close_cutting_session(session_id: int) -> None: si.is_archived = True si.archived_at = timezone.now() si.save(update_fields=['quantity', 'is_archived', 'archived_at']) + logger.info('close_cutting_session:archived stock_item=%s', si.id) else: si.save(update_fields=['quantity']) @@ -107,7 +114,10 @@ def close_cutting_session(session_id: int) -> None: need -= take if si.quantity == 0: - si.delete() + si.is_archived = True + si.archived_at = timezone.now() + si.save(update_fields=['quantity', 'is_archived', 'archived_at']) + logger.info('close_cutting_session:archived stock_item=%s', si.id) else: si.save(update_fields=['quantity']) @@ -118,6 +128,7 @@ def close_cutting_session(session_id: int) -> None: raise RuntimeError('Не заполнено списание сырья: добавь строки «Списание сырья» или укажи legacy поле «Взятый материал».') used = StockItem.objects.select_for_update(of=('self',)).select_related('material', 'location').get(pk=session.used_stock_item_id) + logger.info('close_cutting_session:used stock_item=%s before=%s', used.id, used.quantity) if not used.material_id: raise RuntimeError('Взятый материал должен ссылаться на сырьё (material), а не на готовую деталь (entity).') @@ -132,6 +143,7 @@ def close_cutting_session(session_id: int) -> None: used.is_archived = True used.archived_at = timezone.now() used.save(update_fields=['quantity', 'is_archived', 'archived_at']) + logger.info('close_cutting_session:archived used=%s', used.id) else: used.save(update_fields=['quantity']) @@ -193,4 +205,5 @@ def close_cutting_session(session_id: int) -> None: ProductionReportStockResult.objects.create(report=session, stock_item=created, kind='remnant') session.is_closed = True - session.save(update_fields=["is_closed"]) \ No newline at end of file + session.save(update_fields=["is_closed"]) + logger.info('close_cutting_session:done id=%s', session_id) diff --git a/shiftflow/templates/shiftflow/closing.html b/shiftflow/templates/shiftflow/closing.html index 6bbb345..92b2f0c 100644 --- a/shiftflow/templates/shiftflow/closing.html +++ b/shiftflow/templates/shiftflow/closing.html @@ -91,6 +91,7 @@ Поступление Сделка Единица + Размеры Доступно Использовано @@ -107,6 +108,15 @@ {% endif %} {{ s }} + + {% if s.current_length and s.current_width %} + {{ s.current_length|floatformat:"-g" }} × {{ s.current_width|floatformat:"-g" }} мм + {% elif s.current_length %} + {{ s.current_length|floatformat:"-g" }} мм + {% else %} + — + {% endif %} + {{ s.quantity }} diff --git a/shiftflow/templates/shiftflow/maintenance.html b/shiftflow/templates/shiftflow/maintenance.html index ee6d95c..503e8a1 100644 --- a/shiftflow/templates/shiftflow/maintenance.html +++ b/shiftflow/templates/shiftflow/maintenance.html @@ -12,6 +12,7 @@ Здесь настраиваем и обслуживаем генерацию превью DXF (PNG) на сервере. +
DXF @@ -70,8 +71,8 @@ -
@@ -84,6 +85,25 @@
+
+
+ Логи +
+
+
Файл: {{ log_path|default:"—" }}
+
{{ log_tail|default:"" }}
+
+ {% csrf_token %} + + +
+
+
+ {% if messages %}
{% for message in messages %} diff --git a/shiftflow/templates/shiftflow/registry_print.html b/shiftflow/templates/shiftflow/registry_print.html index 29c0ed2..ef8b27d 100644 --- a/shiftflow/templates/shiftflow/registry_print.html +++ b/shiftflow/templates/shiftflow/registry_print.html @@ -30,9 +30,9 @@ - - Назад - +
{{ printed_at|date:"d.m.Y H:i" }}
diff --git a/shiftflow/templates/shiftflow/task_create.html b/shiftflow/templates/shiftflow/task_create.html index e2b2dc3..cc59e03 100644 --- a/shiftflow/templates/shiftflow/task_create.html +++ b/shiftflow/templates/shiftflow/task_create.html @@ -268,10 +268,18 @@
-
+
+
+ + +