refactor(inventory): remove individual writeoff views and templates, shift to document-based writeoffs

- 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.
This commit is contained in:
2025-12-27 01:04:41 +03:00
parent 1eaee7de5e
commit 44d115b356
11 changed files with 59 additions and 535 deletions

View File

@@ -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):

View File

@@ -26,7 +26,6 @@
<li><a class="dropdown-item" href="{% url 'inventory:incoming-list' %}">Поступления</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:sale-list' %}">Продажи</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:inventory-list' %}">Инвентаризация</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:writeoff-list' %}">Списания</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:writeoff-document-list' %}">Документы списания</a></li>
<li><a class="dropdown-item" href="{% url 'inventory:transfer-list' %}">Перемещения</a></li>
<li><hr class="dropdown-divider"></li>

View File

@@ -92,19 +92,6 @@
</a>
</div>
<!-- Приходы -->
<div class="col-12 col-md-6 col-lg-4">
<a href="{% url 'inventory:incoming-list' %}" class="card-main-operation h-100 text-decoration-none">
<div class="card-body p-4">
<div class="text-center">
<i class="bi bi-arrow-down-square text-muted mb-3" style="font-size: 2rem;"></i>
<h6 class="mb-2 text-dark fw-medium">Приходы</h6>
<small class="text-muted">Поступление товара</small>
</div>
</div>
</a>
</div>
<!-- Продажи -->
<div class="col-12 col-md-6 col-lg-4">
<a href="{% url 'inventory:sale-list' %}" class="card-main-operation h-100 text-decoration-none">
@@ -118,19 +105,6 @@
</a>
</div>
<!-- Списания -->
<div class="col-12 col-md-6 col-lg-4">
<a href="{% url 'inventory:writeoff-list' %}" class="card-main-operation h-100 text-decoration-none">
<div class="card-body p-4">
<div class="text-center">
<i class="bi bi-x-circle text-muted mb-3" style="font-size: 2rem;"></i>
<h6 class="mb-2 text-dark fw-medium">Списания</h6>
<small class="text-muted">Списание товара</small>
</div>
</div>
</a>
</div>
<!-- Перемещения -->
<div class="col-12 col-md-6 col-lg-4">
<a href="{% url 'inventory:transfer-list' %}" class="card-main-operation h-100 text-decoration-none">

View File

@@ -20,6 +20,7 @@
<th>Склад</th>
<th>Количество</th>
<th>Цена продажи</th>
<th>Сумма</th>
<th>Заказ</th>
<th>Статус</th>
<th>Дата</th>
@@ -33,6 +34,7 @@
<td>{{ sale.warehouse.name }}</td>
<td>{{ sale.quantity|smart_quantity }} шт</td>
<td>{{ sale.sale_price }} руб.</td>
<td><strong>{{ sale.quantity|multiply:sale.sale_price|format_decimal:2 }} руб.</strong></td>
<td>
{% if sale.order %}
<code>{{ sale.order.order_number }}</code>

View File

@@ -1,12 +0,0 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% block inventory_title %}Отмена списания{% endblock %}
{% block breadcrumb_current %}Списания{% endblock %}
{% block inventory_content %}
<div class="card border-danger">
<div class="card-header bg-danger text-white"><h4 class="mb-0">Подтверждение отмены</h4></div>
<div class="card-body">
<div class="alert alert-warning"><i class="bi bi-exclamation-triangle"></i> <strong>Внимание!</strong> Вы собираетесь отменить списание товара.</div>
<form method="post"><{% csrf_token %}<div class="d-flex gap-2"><button type="submit" class="btn btn-danger"><i class="bi bi-trash"></i> Подтвердить</button><a href="{% url 'inventory:writeoff-list' %}" class="btn btn-secondary"><i class="bi bi-x-circle"></i> Отмена</a></div></form>
</div>
</div>
{% endblock %}

View File

@@ -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 %}
<!-- CSS для компонента -->
<link rel="stylesheet" href="{% static 'products/css/product-search-picker.css' %}">
<div class="row">
<!-- Левая колонка: поиск товаров -->
<div class="col-lg-6 mb-4">
{% 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' %}
</div>
<!-- Правая колонка: форма списания -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-clipboard-minus text-danger"></i>
{% if form.instance.pk %}Редактирование{% else %}Создание{% endif %} списания
</h5>
</div>
<div class="card-body">
<!-- Ошибки формы -->
{% if form.non_field_errors %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong><i class="bi bi-exclamation-triangle"></i> Ошибка:</strong>
{% for error in form.non_field_errors %}
<div>{{ error }}</div>
{% endfor %}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<!-- Выбранный товар -->
<div id="selected-product-info" class="alert alert-info mb-3" style="display: none;">
<div class="d-flex align-items-center">
<img id="selected-product-photo" src="" alt="" class="rounded me-3" style="width: 50px; height: 50px; object-fit: cover; display: none;">
<div class="flex-grow-1">
<strong id="selected-product-name"></strong>
<div class="small text-muted" id="selected-product-sku"></div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-selected-product">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<form method="post" novalidate id="writeoff-form">
{% csrf_token %}
<!-- Скрытое поле для хранения выбранного product_id -->
<input type="hidden" id="selected-product-id" name="selected_product_id" value="">
<!-- Поле Партия -->
<div class="mb-3">
<label class="form-label">{{ form.batch.label }} <span class="text-danger">*</span></label>
{{ form.batch }}
{% if form.batch.errors %}
<div class="invalid-feedback d-block">{{ form.batch.errors.0 }}</div>
{% endif %}
<small class="text-muted" id="batch-hint">
<i class="bi bi-info-circle"></i> Сначала выберите товар слева, затем здесь появятся его партии
</small>
<!-- Информация об остатке партии -->
<div id="batch-info" class="mt-2 p-2 bg-light border rounded" style="display:none;">
<small class="text-muted">
Остаток в партии: <strong id="batch-quantity">0</strong> шт
</small>
</div>
</div>
<!-- Поле Количество -->
<div class="mb-3">
<label class="form-label">{{ form.quantity.label }} <span class="text-danger">*</span></label>
{{ form.quantity|smart_quantity }}
{% if form.quantity.errors %}
<div class="invalid-feedback d-block">{{ form.quantity.errors.0 }}</div>
{% endif %}
<small class="text-muted d-block mt-1">
Введите количество товара для списания
</small>
<!-- Предупреждение о превышении остатка -->
<div id="quantity-warning" class="alert alert-warning mt-2" style="display:none;">
<i class="bi bi-exclamation-circle"></i>
<strong>Внимание!</strong> Вы пытаетесь списать <strong id="warning-qty">0</strong> шт,
а в партии только <strong id="warning-batch">0</strong> шт.
Недостаток: <strong id="warning-shortage" class="text-danger">0</strong> шт.
</div>
</div>
<!-- Поле Причина -->
<div class="mb-3">
<label class="form-label">{{ form.reason.label }}</label>
{{ form.reason }}
{% if form.reason.errors %}
<div class="invalid-feedback d-block">{{ form.reason.errors.0 }}</div>
{% endif %}
</div>
<!-- Поле Номер документа -->
<div class="mb-3">
<label class="form-label">{{ form.document_number.label }}</label>
{{ form.document_number }}
{% if form.document_number.errors %}
<div class="invalid-feedback d-block">{{ form.document_number.errors.0 }}</div>
{% endif %}
</div>
<!-- Поле Примечания -->
<div class="mb-3">
<label class="form-label">{{ form.notes.label }}</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="invalid-feedback d-block">{{ form.notes.errors.0 }}</div>
{% endif %}
</div>
<!-- Кнопки действия -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" id="submit-btn">
<i class="bi bi-check-circle"></i> Сохранить
</button>
<a href="{% url 'inventory:writeoff-list' %}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Отмена
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<style>
select, textarea, input[type="text"], input[type="number"] {
width: 100%;
}
.invalid-feedback {
color: #dc3545;
font-size: 0.875em;
}
#selected-product-info {
border-left: 4px solid #0d6efd;
}
</style>
<!-- JS для компонента -->
<script src="{% static 'products/js/product-search-picker.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Элементы формы
const batchSelect = document.querySelector('#id_batch');
const quantityInput = document.querySelector('#id_quantity');
const batchInfo = document.getElementById('batch-info');
const batchQuantitySpan = document.getElementById('batch-quantity');
const quantityWarning = document.getElementById('quantity-warning');
const warningQty = document.getElementById('warning-qty');
const warningBatch = document.getElementById('warning-batch');
const warningShortage = document.getElementById('warning-shortage');
const batchHint = document.getElementById('batch-hint');
// Элементы отображения выбранного товара
const selectedProductInfo = document.getElementById('selected-product-info');
const selectedProductName = document.getElementById('selected-product-name');
const selectedProductSku = document.getElementById('selected-product-sku');
const selectedProductPhoto = document.getElementById('selected-product-photo');
const selectedProductIdInput = document.getElementById('selected-product-id');
const clearSelectedBtn = document.getElementById('clear-selected-product');
// Сохраняем все опции партий для фильтрации
const allBatchOptions = Array.from(batchSelect.options).map(opt => ({
value: opt.value,
text: opt.text,
productName: opt.text.split(' на ')[0] // Извлекаем название товара
}));
// Инициализация компонента поиска товаров
const picker = ProductSearchPicker.init('#writeoff-product-picker', {
onAddSelected: function(products, instance) {
if (products.length > 0) {
selectProduct(products[0]);
instance.clearSelection();
}
}
});
// Функция выбора товара
function selectProduct(product) {
const productId = String(product.id).replace('product_', '');
const productName = product.text || product.name || '';
// Показываем информацию о выбранном товаре
selectedProductName.textContent = productName;
selectedProductSku.textContent = product.sku || '';
selectedProductIdInput.value = productId;
if (product.photo_url) {
selectedProductPhoto.src = product.photo_url;
selectedProductPhoto.style.display = 'block';
} else {
selectedProductPhoto.style.display = 'none';
}
selectedProductInfo.style.display = 'block';
// Фильтруем партии по выбранному товару
filterBatchesByProduct(productName);
// Скрываем подсказку
batchHint.style.display = 'none';
}
// Функция очистки выбора товара
function clearSelectedProduct() {
selectedProductInfo.style.display = 'none';
selectedProductName.textContent = '';
selectedProductSku.textContent = '';
selectedProductPhoto.style.display = 'none';
selectedProductIdInput.value = '';
// Показываем все партии
showAllBatches();
// Показываем подсказку
batchHint.style.display = 'block';
}
// Фильтрация партий по названию товара
function filterBatchesByProduct(productName) {
// Очищаем select
batchSelect.innerHTML = '<option value="">---------</option>';
// Добавляем только партии выбранного товара
const normalizedName = productName.toLowerCase().trim();
allBatchOptions.forEach(function(opt) {
if (opt.value && opt.productName.toLowerCase().trim().includes(normalizedName.split(' (')[0])) {
const option = document.createElement('option');
option.value = opt.value;
option.text = opt.text;
batchSelect.appendChild(option);
}
});
// Если найдена только одна партия, выбираем её автоматически
if (batchSelect.options.length === 2) {
batchSelect.selectedIndex = 1;
updateBatchInfo();
}
}
// Показать все партии
function showAllBatches() {
batchSelect.innerHTML = '<option value="">---------</option>';
allBatchOptions.forEach(function(opt) {
if (opt.value) {
const option = document.createElement('option');
option.value = opt.value;
option.text = opt.text;
batchSelect.appendChild(option);
}
});
}
// Очистка выбора товара
clearSelectedBtn.addEventListener('click', clearSelectedProduct);
// ===== Существующая логика для партий =====
// Функция для получения остатка партии
function getBatchQuantity() {
if (!batchSelect.value) {
batchInfo.style.display = 'none';
quantityWarning.style.display = 'none';
return null;
}
// Получаем текст option и парсим остаток
const selectedOption = batchSelect.options[batchSelect.selectedIndex];
const optionText = selectedOption.text;
// Пытаемся найти количество в скобках (формат: "Product - Остаток: X шт")
const match = optionText.match(/Остаток:\s*(\d+(?:[.,]\d+)?)/);
if (match) {
const qty = parseFloat(match[1].replace(',', '.'));
return qty;
}
return null;
}
// Функция для обновления информации и предупреждений
function updateBatchInfo() {
const batchQty = getBatchQuantity();
if (batchQty !== null) {
batchQuantitySpan.textContent = batchQty;
batchInfo.style.display = 'block';
} else {
batchInfo.style.display = 'none';
quantityWarning.style.display = 'none';
}
}
// Функция для проверки количества
function checkQuantity() {
const batchQty = getBatchQuantity();
const qty = parseFloat(quantityInput.value) || 0;
if (batchQty !== null && qty > 0) {
if (qty > batchQty) {
warningQty.textContent = qty;
warningBatch.textContent = batchQty;
warningShortage.textContent = (qty - batchQty).toFixed(3);
quantityWarning.style.display = 'block';
} else {
quantityWarning.style.display = 'none';
}
} else {
quantityWarning.style.display = 'none';
}
}
// События
batchSelect.addEventListener('change', updateBatchInfo);
quantityInput.addEventListener('input', checkQuantity);
// Инициализация
updateBatchInfo();
});
</script>
{% endblock %}

View File

@@ -1,51 +0,0 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %}
{% block inventory_title %}История списаний{% endblock %}
{% block breadcrumb_current %}Списания{% endblock %}
{% block inventory_content %}
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="mb-0">Списания товара</h4>
<a href="{% url 'inventory:writeoff-create' %}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle"></i> Новое списание
</a>
</div>
<div class="card-body">
{% if writeoffs %}
<div class="table-responsive">
<table class="table table-hover table-sm">
<thead class="table-light">
<tr>
<th>Товар</th>
<th>Количество</th>
<th>Причина</th>
<th>Дата</th>
<th class="text-end">Действия</th>
</tr>
</thead>
<tbody>
{% for writeoff in writeoffs %}
<tr>
<td><strong>{{ writeoff.batch.product.name }}</strong></td>
<td>{{ writeoff.quantity|smart_quantity }} шт</td>
<td><span class="badge bg-warning">{{ writeoff.get_reason_display }}</span></td>
<td>{{ writeoff.date|date:"d.m.Y H:i" }}</td>
<td class="text-end">
<a href="{% url 'inventory:writeoff-update' writeoff.pk %}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</a>
<a href="{% url 'inventory:writeoff-delete' writeoff.pk %}" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info">Списаний не найдено.</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -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

View File

@@ -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/<int:pk>/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/<int:pk>/edit/', WriteOffUpdateView.as_view(), name='writeoff-update'),
path('writeoffs/<int:pk>/delete/', WriteOffDeleteView.as_view(), name='writeoff-delete'),
# УДАЛЕНО: пользователи работают только через WriteOffDocument
# Модель WriteOff остаётся - она используется для хранения фактических списаний
# Записи WriteOff создаются автоматически при проведении документов списания
# ==================== WRITEOFF DOCUMENT (документы списания) ====================
path('writeoff-documents/', WriteOffDocumentListView.as_view(), name='writeoff-document-list'),

View File

@@ -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',

View File

@@ -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)