From 44d115b356c37de8b9b6f211ac2f6f20247d7815 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sat, 27 Dec 2025 01:04:41 +0300 Subject: [PATCH] refactor(inventory): remove individual writeoff views and templates, shift to document-based writeoffs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove WriteOffForm from forms.py and add comment directing to WriteOffDocumentForm - Update navigation templates to remove writeoff links and sections - Add 'Сумма' column to sale list with multiplication filter - Delete writeoff-related templates (list, form, confirm delete) - Add 'multiply' filter to inventory_filters.py for calculations - Comment out writeoff URLs in urls.py, keeping WriteOff model for automatic creation - Remove WriteOff views from __init__.py and delete writeoff.py view file This change simplifies writeoff management by removing direct individual writeoff operations and enforcing use of WriteOffDocument for all writeoffs, with WriteOff records created automatically upon document processing. --- myproject/inventory/forms.py | 36 +- .../inventory/base_inventory_minimal.html | 1 - .../inventory/templates/inventory/home.html | 26 -- .../templates/inventory/sale/sale_list.html | 2 + .../writeoff/writeoff_confirm_delete.html | 12 - .../inventory/writeoff/writeoff_form.html | 335 ------------------ .../inventory/writeoff/writeoff_list.html | 51 --- .../templatetags/inventory_filters.py | 56 ++- myproject/inventory/urls.py | 9 +- myproject/inventory/views/__init__.py | 5 +- myproject/inventory/views/writeoff.py | 61 ---- 11 files changed, 59 insertions(+), 535 deletions(-) delete mode 100644 myproject/inventory/templates/inventory/writeoff/writeoff_confirm_delete.html delete mode 100644 myproject/inventory/templates/inventory/writeoff/writeoff_form.html delete mode 100644 myproject/inventory/templates/inventory/writeoff/writeoff_list.html delete mode 100644 myproject/inventory/views/writeoff.py diff --git a/myproject/inventory/forms.py b/myproject/inventory/forms.py index fd2a074..03b03db 100644 --- a/myproject/inventory/forms.py +++ b/myproject/inventory/forms.py @@ -68,41 +68,7 @@ class SaleForm(forms.ModelForm): return sale_price -class WriteOffForm(forms.ModelForm): - class Meta: - model = WriteOff - fields = ['batch', 'quantity', 'reason', 'document_number', 'notes'] - widgets = { - 'batch': forms.Select(attrs={'class': 'form-control'}), - 'quantity': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.001'}), - 'reason': forms.Select(attrs={'class': 'form-control'}), - 'document_number': forms.TextInput(attrs={'class': 'form-control'}), - 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), - } - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Фильтруем партии - показываем только активные - self.fields['batch'].queryset = StockBatch.objects.filter( - is_active=True - ).select_related('product', 'warehouse').order_by('-created_at') - - def clean(self): - cleaned_data = super().clean() - batch = cleaned_data.get('batch') - quantity = cleaned_data.get('quantity') - - if batch and quantity: - if quantity > batch.quantity: - raise ValidationError( - f'Невозможно списать {quantity} шт из партии, ' - f'где только {batch.quantity} шт. ' - f'Недостаток: {quantity - batch.quantity} шт.' - ) - if quantity <= 0: - raise ValidationError('Количество должно быть больше нуля') - - return cleaned_data +# WriteOffForm удалён - используйте WriteOffDocumentForm для работы с документами списания class InventoryForm(forms.ModelForm): diff --git a/myproject/inventory/templates/inventory/base_inventory_minimal.html b/myproject/inventory/templates/inventory/base_inventory_minimal.html index b3179c8..1aab649 100644 --- a/myproject/inventory/templates/inventory/base_inventory_minimal.html +++ b/myproject/inventory/templates/inventory/base_inventory_minimal.html @@ -26,7 +26,6 @@
  • Поступления
  • Продажи
  • Инвентаризация
  • -
  • Списания
  • Документы списания
  • Перемещения
  • diff --git a/myproject/inventory/templates/inventory/home.html b/myproject/inventory/templates/inventory/home.html index 9d99ca1..0f8e9e5 100644 --- a/myproject/inventory/templates/inventory/home.html +++ b/myproject/inventory/templates/inventory/home.html @@ -92,19 +92,6 @@ - -
    - -
    -
    - -
    Приходы
    - Поступление товара -
    -
    -
    -
    -
    @@ -118,19 +105,6 @@
    - -
    - -
    -
    - -
    Списания
    - Списание товара -
    -
    -
    -
    -
    diff --git a/myproject/inventory/templates/inventory/sale/sale_list.html b/myproject/inventory/templates/inventory/sale/sale_list.html index b0e0a28..b843e93 100644 --- a/myproject/inventory/templates/inventory/sale/sale_list.html +++ b/myproject/inventory/templates/inventory/sale/sale_list.html @@ -20,6 +20,7 @@ Склад Количество Цена продажи + Сумма Заказ Статус Дата @@ -33,6 +34,7 @@ {{ sale.warehouse.name }} {{ sale.quantity|smart_quantity }} шт {{ sale.sale_price }} руб. + {{ sale.quantity|multiply:sale.sale_price|format_decimal:2 }} руб. {% if sale.order %} {{ sale.order.order_number }} diff --git a/myproject/inventory/templates/inventory/writeoff/writeoff_confirm_delete.html b/myproject/inventory/templates/inventory/writeoff/writeoff_confirm_delete.html deleted file mode 100644 index c6917d1..0000000 --- a/myproject/inventory/templates/inventory/writeoff/writeoff_confirm_delete.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends 'inventory/base_inventory_minimal.html' %} -{% block inventory_title %}Отмена списания{% endblock %} -{% block breadcrumb_current %}Списания{% endblock %} -{% block inventory_content %} - -{% endblock %} diff --git a/myproject/inventory/templates/inventory/writeoff/writeoff_form.html b/myproject/inventory/templates/inventory/writeoff/writeoff_form.html deleted file mode 100644 index 0fc340d..0000000 --- a/myproject/inventory/templates/inventory/writeoff/writeoff_form.html +++ /dev/null @@ -1,335 +0,0 @@ -{% extends 'inventory/base_inventory_minimal.html' %} -{% load static inventory_filters %} -{% block inventory_title %}{% if form.instance.pk %}Редактирование списания{% else %}Новое списание{% endif %}{% endblock %} -{% block breadcrumb_current %}Списания{% endblock %} - -{% block inventory_content %} - - - -
    - -
    - {% include 'products/components/product_search_picker.html' with container_id='writeoff-product-picker' title='Найти товар для списания' filter_in_stock_only=True categories=categories tags=tags add_button_text='Выбрать товар' multi_select=False content_height='350px' %} -
    - - -
    -
    -
    -
    - - {% if form.instance.pk %}Редактирование{% else %}Создание{% endif %} списания -
    -
    -
    - - {% if form.non_field_errors %} - - {% endif %} - - - - -
    - {% csrf_token %} - - - - - -
    - - {{ form.batch }} - {% if form.batch.errors %} -
    {{ form.batch.errors.0 }}
    - {% endif %} - - Сначала выберите товар слева, затем здесь появятся его партии - - - -
    - - -
    - - {{ form.quantity|smart_quantity }} - {% if form.quantity.errors %} -
    {{ form.quantity.errors.0 }}
    - {% endif %} - - Введите количество товара для списания - - - -
    - - -
    - - {{ form.reason }} - {% if form.reason.errors %} -
    {{ form.reason.errors.0 }}
    - {% endif %} -
    - - -
    - - {{ form.document_number }} - {% if form.document_number.errors %} -
    {{ form.document_number.errors.0 }}
    - {% endif %} -
    - - -
    - - {{ form.notes }} - {% if form.notes.errors %} -
    {{ form.notes.errors.0 }}
    - {% endif %} -
    - - -
    - - - Отмена - -
    -
    -
    -
    -
    -
    - - - - - - -{% endblock %} diff --git a/myproject/inventory/templates/inventory/writeoff/writeoff_list.html b/myproject/inventory/templates/inventory/writeoff/writeoff_list.html deleted file mode 100644 index f15fbb1..0000000 --- a/myproject/inventory/templates/inventory/writeoff/writeoff_list.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends 'inventory/base_inventory_minimal.html' %} -{% load inventory_filters %} -{% block inventory_title %}История списаний{% endblock %} -{% block breadcrumb_current %}Списания{% endblock %} -{% block inventory_content %} -
    -
    -

    Списания товара

    - - Новое списание - -
    -
    - {% if writeoffs %} -
    - - - - - - - - - - - - {% for writeoff in writeoffs %} - - - - - - - - {% endfor %} - -
    ТоварКоличествоПричинаДатаДействия
    {{ writeoff.batch.product.name }}{{ writeoff.quantity|smart_quantity }} шт{{ writeoff.get_reason_display }}{{ writeoff.date|date:"d.m.Y H:i" }} - - - - - - -
    -
    - {% else %} -
    Списаний не найдено.
    - {% endif %} -
    -
    -{% endblock %} diff --git a/myproject/inventory/templatetags/inventory_filters.py b/myproject/inventory/templatetags/inventory_filters.py index e837b4e..d9974fe 100644 --- a/myproject/inventory/templatetags/inventory_filters.py +++ b/myproject/inventory/templatetags/inventory_filters.py @@ -87,12 +87,60 @@ def format_decimal(value, decimal_places=2): quantize_value = Decimal(10) ** -decimal_places rounded = num.quantize(quantize_value) - # Убираем лишние нули - normalized = rounded.normalize() + # Форматируем без научной нотации + result = format(rounded, 'f') - # Форматируем с запятой - result = str(normalized).replace('.', ',') + # Убираем лишние нули справа после десятичной точки + if '.' in result: + result = result.rstrip('0').rstrip('.') + + # Форматируем с запятой вместо точки + result = result.replace('.', ',') return result except (ValueError, TypeError, ArithmeticError): return str(value) + + +@register.filter(name='multiply') +def multiply(value, arg): + """ + Умножает значение на аргумент. + + Использование в шаблонах: + {{ quantity|multiply:price }} + + Args: + value: первое число + arg: второе число (множитель) + + Returns: + Decimal: результат умножения + """ + try: + if value is None or arg is None: + return 0 + + # Преобразуем в Decimal для точности + if isinstance(value, str): + val = Decimal(value) + elif isinstance(value, (int, float)): + val = Decimal(str(value)) + elif isinstance(value, Decimal): + val = value + else: + val = Decimal(str(value)) + + if isinstance(arg, str): + multiplier = Decimal(arg) + elif isinstance(arg, (int, float)): + multiplier = Decimal(str(arg)) + elif isinstance(arg, Decimal): + multiplier = arg + else: + multiplier = Decimal(str(arg)) + + return val * multiplier + + except (ValueError, TypeError, ArithmeticError): + return 0 diff --git a/myproject/inventory/urls.py b/myproject/inventory/urls.py index 0428e57..14e27bb 100644 --- a/myproject/inventory/urls.py +++ b/myproject/inventory/urls.py @@ -9,8 +9,6 @@ from .views import ( InventoryListView, InventoryCreateView, InventoryDetailView, InventoryLineCreateBulkView, InventoryLineAddView, InventoryLineUpdateView, InventoryLineDeleteView, InventoryCompleteView, InventoryDeleteView, - # WriteOff - WriteOffListView, WriteOffCreateView, WriteOffUpdateView, WriteOffDeleteView, # Transfer TransferListView, TransferBulkCreateView, TransferDetailView, TransferDeleteView, GetProductStockView, # Reservation @@ -83,10 +81,9 @@ urlpatterns = [ path('inventory-ops//delete/', InventoryDeleteView.as_view(), name='inventory-delete'), # ==================== WRITEOFF (одиночные записи) ==================== - path('writeoffs/', WriteOffListView.as_view(), name='writeoff-list'), - path('writeoffs/create/', WriteOffCreateView.as_view(), name='writeoff-create'), - path('writeoffs//edit/', WriteOffUpdateView.as_view(), name='writeoff-update'), - path('writeoffs//delete/', WriteOffDeleteView.as_view(), name='writeoff-delete'), + # УДАЛЕНО: пользователи работают только через WriteOffDocument + # Модель WriteOff остаётся - она используется для хранения фактических списаний + # Записи WriteOff создаются автоматически при проведении документов списания # ==================== WRITEOFF DOCUMENT (документы списания) ==================== path('writeoff-documents/', WriteOffDocumentListView.as_view(), name='writeoff-document-list'), diff --git a/myproject/inventory/views/__init__.py b/myproject/inventory/views/__init__.py index 992a626..6fa7abc 100644 --- a/myproject/inventory/views/__init__.py +++ b/myproject/inventory/views/__init__.py @@ -7,7 +7,7 @@ Inventory Views Package - incoming.py: Управление приходами товара - sale.py: Управление продажами - inventory_ops.py: Инвентаризация и её строки -- writeoff.py: Списания товара +- writeoff_document.py: Документы списания товара - transfer.py: Перемещения между складами - reservation.py: Резервирования товара (view-only) - stock.py: Справочник остатков (view-only) @@ -25,7 +25,6 @@ from .inventory_ops import ( InventoryLineCreateBulkView, InventoryLineAddView, InventoryLineUpdateView, InventoryLineDeleteView, InventoryCompleteView, InventoryDeleteView ) -from .writeoff import WriteOffListView, WriteOffCreateView, WriteOffUpdateView, WriteOffDeleteView from .writeoff_document import ( WriteOffDocumentListView, WriteOffDocumentCreateView, WriteOffDocumentDetailView, WriteOffDocumentAddItemView, WriteOffDocumentUpdateItemView, WriteOffDocumentRemoveItemView, @@ -61,8 +60,6 @@ __all__ = [ 'InventoryListView', 'InventoryCreateView', 'InventoryDetailView', 'InventoryLineCreateBulkView', 'InventoryLineAddView', 'InventoryLineUpdateView', 'InventoryLineDeleteView', 'InventoryCompleteView', 'InventoryDeleteView', - # WriteOff - 'WriteOffListView', 'WriteOffCreateView', 'WriteOffUpdateView', 'WriteOffDeleteView', # WriteOffDocument 'WriteOffDocumentListView', 'WriteOffDocumentCreateView', 'WriteOffDocumentDetailView', 'WriteOffDocumentAddItemView', 'WriteOffDocumentUpdateItemView', 'WriteOffDocumentRemoveItemView', diff --git a/myproject/inventory/views/writeoff.py b/myproject/inventory/views/writeoff.py deleted file mode 100644 index 0443889..0000000 --- a/myproject/inventory/views/writeoff.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -""" -WriteOff (Списание товара) views -GROUP 2: MEDIUM PRIORITY -""" -from django.views.generic import ListView, CreateView, UpdateView, DeleteView -from django.urls import reverse_lazy -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib import messages -from ..models import WriteOff -from ..forms import WriteOffForm -from products.models import ProductCategory, ProductTag - - -class WriteOffListView(LoginRequiredMixin, ListView): - model = WriteOff - template_name = 'inventory/writeoff/writeoff_list.html' - context_object_name = 'writeoffs' - paginate_by = 20 - - def get_queryset(self): - return WriteOff.objects.select_related('batch', 'batch__product').order_by('-date') - - -class WriteOffCreateView(LoginRequiredMixin, CreateView): - model = WriteOff - form_class = WriteOffForm - template_name = 'inventory/writeoff/writeoff_form.html' - success_url = reverse_lazy('inventory:writeoff-list') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['categories'] = ProductCategory.objects.filter(is_active=True).order_by('name') - context['tags'] = ProductTag.objects.filter(is_active=True).order_by('name') - return context - - def form_valid(self, form): - messages.success(self.request, f'Списание товара успешно создано.') - return super().form_valid(form) - - -class WriteOffUpdateView(LoginRequiredMixin, UpdateView): - model = WriteOff - form_class = WriteOffForm - template_name = 'inventory/writeoff/writeoff_form.html' - success_url = reverse_lazy('inventory:writeoff-list') - - def form_valid(self, form): - messages.success(self.request, f'Списание товара обновлено.') - return super().form_valid(form) - - -class WriteOffDeleteView(LoginRequiredMixin, DeleteView): - model = WriteOff - template_name = 'inventory/writeoff/writeoff_confirm_delete.html' - success_url = reverse_lazy('inventory:writeoff-list') - - def form_valid(self, form): - writeoff = self.get_object() - messages.success(self.request, f'Списание товара отменено.') - return super().form_valid(form)