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:
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user