fix: Улучшения системы ценообразования комплектов
Исправлены 4 проблемы: 1. Расчёт цены первого товара - улучшена валидация в getProductPrice и calculateFinalPrice 2. Отображение actual_price в Select2 вместо обычной цены 3. Количество по умолчанию = 1 для новых форм компонентов 4. Auto-select текста при клике на поле количества для удобства редактирования Изменённые файлы: - products/forms.py: добавлен __init__ в KitItemForm для quantity.initial = 1 - products/templates/includes/select2-product-init.html: обновлена formatSelectResult - products/templates/productkit_create.html: добавлен focus handler для auto-select - products/templates/productkit_edit.html: добавлен focus handler для auto-select 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,12 +14,12 @@ from inventory.models import (
|
||||
# ===== WAREHOUSE =====
|
||||
@admin.register(Warehouse)
|
||||
class WarehouseAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'is_active', 'created_at')
|
||||
list_filter = ('is_active', 'created_at')
|
||||
list_display = ('name', 'is_default_display', 'is_active', 'created_at')
|
||||
list_filter = ('is_active', 'is_default', 'created_at')
|
||||
search_fields = ('name',)
|
||||
fieldsets = (
|
||||
('Основная информация', {
|
||||
'fields': ('name', 'description', 'is_active')
|
||||
'fields': ('name', 'description', 'is_active', 'is_default')
|
||||
}),
|
||||
('Даты', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
@@ -28,6 +28,12 @@ class WarehouseAdmin(admin.ModelAdmin):
|
||||
)
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
|
||||
def is_default_display(self, obj):
|
||||
if obj.is_default:
|
||||
return format_html('<span style="color: #ff9900; font-weight: bold;">★ По умолчанию</span>')
|
||||
return '-'
|
||||
is_default_display.short_description = 'По умолчанию'
|
||||
|
||||
|
||||
# ===== STOCK BATCH =====
|
||||
@admin.register(StockBatch)
|
||||
|
||||
@@ -10,11 +10,12 @@ from products.models import Product
|
||||
class WarehouseForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Warehouse
|
||||
fields = ['name', 'description', 'is_active']
|
||||
fields = ['name', 'description', 'is_active', 'is_default']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
|
||||
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'is_default': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +146,19 @@ class InventoryForm(forms.ModelForm):
|
||||
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Фильтруем только активные склады (исключаем скрытые)
|
||||
self.fields['warehouse'].queryset = Warehouse.objects.filter(is_active=True)
|
||||
# Если есть склад по умолчанию и значение не установлено явно - предвыбираем его
|
||||
if not self.initial.get('warehouse'):
|
||||
default_warehouse = Warehouse.objects.filter(
|
||||
is_active=True,
|
||||
is_default=True
|
||||
).first()
|
||||
if default_warehouse:
|
||||
self.initial['warehouse'] = default_warehouse.id
|
||||
|
||||
|
||||
class InventoryLineForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@@ -199,6 +213,17 @@ class IncomingHeaderForm(forms.Form):
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Если есть склад по умолчанию и значение не установлено явно - предвыбираем его
|
||||
if not self.initial.get('warehouse'):
|
||||
default_warehouse = Warehouse.objects.filter(
|
||||
is_active=True,
|
||||
is_default=True
|
||||
).first()
|
||||
if default_warehouse:
|
||||
self.initial['warehouse'] = default_warehouse.id
|
||||
|
||||
def clean_document_number(self):
|
||||
document_number = self.cleaned_data.get('document_number', '')
|
||||
if document_number:
|
||||
@@ -292,6 +317,17 @@ class IncomingForm(forms.Form):
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Если есть склад по умолчанию и значение не установлено явно - предвыбираем его
|
||||
if not self.initial.get('warehouse'):
|
||||
default_warehouse = Warehouse.objects.filter(
|
||||
is_active=True,
|
||||
is_default=True
|
||||
).first()
|
||||
if default_warehouse:
|
||||
self.initial['warehouse'] = default_warehouse.id
|
||||
|
||||
def clean_document_number(self):
|
||||
document_number = self.cleaned_data.get('document_number', '')
|
||||
if document_number:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.4 on 2025-10-28 23:32
|
||||
# Generated by Django 5.0.10 on 2025-10-30 21:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
@@ -101,13 +101,14 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=200, verbose_name='Название')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
|
||||
('is_default', models.BooleanField(default=False, help_text='Автоматически выбирается при создании новых документов', verbose_name='Склад по умолчанию')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Склад',
|
||||
'verbose_name_plural': 'Склады',
|
||||
'indexes': [models.Index(fields=['is_active'], name='inventory_w_is_acti_3ddeac_idx')],
|
||||
'indexes': [models.Index(fields=['is_active'], name='inventory_w_is_acti_3ddeac_idx'), models.Index(fields=['is_default'], name='inventory_w_is_defa_4b7615_idx')],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
|
||||
@@ -12,6 +12,11 @@ class Warehouse(models.Model):
|
||||
name = models.CharField(max_length=200, verbose_name="Название")
|
||||
description = models.TextField(blank=True, null=True, verbose_name="Описание")
|
||||
is_active = models.BooleanField(default=True, verbose_name="Активен")
|
||||
is_default = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Склад по умолчанию",
|
||||
help_text="Автоматически выбирается при создании новых документов"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
|
||||
|
||||
@@ -20,11 +25,19 @@ class Warehouse(models.Model):
|
||||
verbose_name_plural = "Склады"
|
||||
indexes = [
|
||||
models.Index(fields=['is_active']),
|
||||
models.Index(fields=['is_default']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Обеспечиваем что только один склад может быть по умолчанию в рамках одного тенанта"""
|
||||
if self.is_default:
|
||||
# Снимаем флаг is_default со всех других складов этого тенанта
|
||||
Warehouse.objects.filter(is_default=True).exclude(pk=self.pk).update(is_default=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class StockBatch(models.Model):
|
||||
"""
|
||||
|
||||
@@ -386,3 +386,126 @@ def update_product_in_stock_on_stock_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
product_id = instance.product_id
|
||||
_update_product_in_stock(product_id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Сигналы для автоматического обновления себестоимости товара (cost_price)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@receiver(post_save, sender=StockBatch)
|
||||
def update_product_cost_on_batch_change(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Сигнал: При создании или изменении партии (StockBatch) автоматически
|
||||
обновляется себестоимость товара (Product.cost_price).
|
||||
|
||||
Процесс:
|
||||
1. Проверяем, есть ли связанный товар
|
||||
2. Вызываем ProductCostCalculator для пересчета средневзвешенной стоимости
|
||||
3. Обновляем поле cost_price в БД
|
||||
|
||||
Триггеры:
|
||||
- Создание новой партии (поступление товара)
|
||||
- Изменение количества в партии
|
||||
- Изменение стоимости партии
|
||||
"""
|
||||
if not instance.product:
|
||||
return
|
||||
|
||||
# Импортируем здесь чтобы избежать circular import
|
||||
from products.services.cost_calculator import ProductCostCalculator
|
||||
|
||||
try:
|
||||
# Пересчитываем и обновляем себестоимость товара
|
||||
ProductCostCalculator.update_product_cost(instance.product, save=True)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(
|
||||
f"Ошибка при обновлении себестоимости товара {instance.product.sku} "
|
||||
f"после изменения партии {instance.id}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=StockBatch)
|
||||
def update_product_cost_on_batch_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
Сигнал: При удалении партии (StockBatch) автоматически
|
||||
обновляется себестоимость товара.
|
||||
|
||||
Процесс:
|
||||
1. После удаления партии пересчитываем себестоимость
|
||||
2. Если партий не осталось - cost_price становится 0.00
|
||||
"""
|
||||
if not instance.product:
|
||||
return
|
||||
|
||||
# Импортируем здесь чтобы избежать circular import
|
||||
from products.services.cost_calculator import ProductCostCalculator
|
||||
|
||||
try:
|
||||
# Пересчитываем и обновляем себестоимость товара
|
||||
ProductCostCalculator.update_product_cost(instance.product, save=True)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(
|
||||
f"Ошибка при обновлении себестоимости товара после удаления партии: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Сигналы для динамического пересчета цен комплектов
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@receiver(post_save, sender='products.Product')
|
||||
def update_kit_prices_on_product_change(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Сигнал: При изменении цены товара (price или sale_price)
|
||||
автоматически пересчитываются цены всех комплектов, где используется этот товар.
|
||||
|
||||
Процесс:
|
||||
1. Находим все KitItem с этим товаром
|
||||
2. Для каждого комплекта вызываем recalculate_base_price()
|
||||
3. base_price и price обновляются в БД
|
||||
|
||||
Триггеры:
|
||||
- Изменение price (основная цена товара)
|
||||
- Изменение sale_price (цена со скидкой товара)
|
||||
"""
|
||||
from products.models import KitItem
|
||||
|
||||
# Если это создание товара (не обновление), нет комплектов для пересчета
|
||||
if created:
|
||||
return
|
||||
|
||||
# Находим все KitItem с этим товаром
|
||||
kit_items = KitItem.objects.filter(product=instance)
|
||||
|
||||
if not kit_items.exists():
|
||||
return # Товар не используется в комплектах
|
||||
|
||||
# Для каждого комплекта пересчитываем цены
|
||||
kits_to_update = set()
|
||||
for item in kit_items:
|
||||
kits_to_update.add(item.kit_id)
|
||||
|
||||
# Обновляем цены каждого комплекта
|
||||
from products.models import ProductKit
|
||||
for kit_id in kits_to_update:
|
||||
try:
|
||||
kit = ProductKit.objects.get(id=kit_id)
|
||||
kit.recalculate_base_price()
|
||||
except ProductKit.DoesNotExist:
|
||||
pass
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(
|
||||
f"Ошибка при пересчете цены комплекта {kit_id} "
|
||||
f"после изменения цены товара {instance.sku}: {e}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Распределение продаж{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Распределение продаж по партиям (FIFO)</h4></div><div class="card-body">{% if allocations %}<table class="table table-hover table-sm"><thead><tr><th>Продажа</th><th>Товар</th><th>Партия</th><th>Кол-во</th><th>Цена</th><th>Дата</th></tr></thead><tbody>{% for a in allocations %}<tr><td>#{{ a.sale.id }}</td><td>{{ a.sale.product.name }}</td><td>#{{ a.batch.id }}</td><td>{{ a.quantity }}</td><td>{{ a.cost_price }}</td><td>{{ a.sale.date|date:"d.m.Y" }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Распределений не найдено.</div>{% endif %}</div></div>
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Распределение продаж по партиям (FIFO)</h4></div><div class="card-body">{% if allocations %}<table class="table table-hover table-sm"><thead><tr><th>Продажа</th><th>Товар</th><th>Партия</th><th>Кол-во</th><th>Цена</th><th>Дата</th></tr></thead><tbody>{% for a in allocations %}<tr><td>#{{ a.sale.id }}</td><td>{{ a.sale.product.name }}</td><td>#{{ a.batch.id }}</td><td>{{ a.quantity|smart_quantity }}</td><td>{{ a.cost_price }}</td><td>{{ a.sale.date|date:"d.m.Y" }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Распределений не найдено.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Партия товара{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Партия #{{ batch.id }}: {{ batch.product.name }}</h4></div><div class="card-body"><table class="table table-borderless"><tr><th>Товар:</th><td>{{ batch.product.name }}</td></tr><tr><th>Склад:</th><td>{{ batch.warehouse.name }}</td></tr><tr><th>Количество:</th><td><strong>{{ batch.quantity }} шт</strong></td></tr><tr><th>Цена закупки:</th><td>{{ batch.cost_price }} ₽</td></tr><tr><th>Создана:</th><td>{{ batch.created_at|date:"d.m.Y H:i" }}</td></tr><tr><th>Статус:</th><td>{% if batch.is_active %}<span class="badge bg-success">Активна</span>{% else %}<span class="badge bg-secondary">Неактивна</span>{% endif %}</td></tr></table><h5 class="mt-4">История операций</h5><div class="alert alert-info">История продаж и списаний этой партии.</div><a href="{% url 'inventory:batch-list' %}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Вернуться</a></div></div>
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Партия #{{ batch.id }}: {{ batch.product.name }}</h4></div><div class="card-body"><table class="table table-borderless"><tr><th>Товар:</th><td>{{ batch.product.name }}</td></tr><tr><th>Склад:</th><td>{{ batch.warehouse.name }}</td></tr><tr><th>Количество:</th><td><strong>{{ batch.quantity|smart_quantity }} шт</strong></td></tr><tr><th>Цена закупки:</th><td>{{ batch.cost_price }} руб.</td></tr><tr><th>Создана:</th><td>{{ batch.created_at|date:"d.m.Y H:i" }}</td></tr><tr><th>Статус:</th><td>{% if batch.is_active %}<span class="badge bg-success">Активна</span>{% else %}<span class="badge bg-secondary">Неактивна</span>{% endif %}</td></tr></table><h5 class="mt-4">История операций</h5><div class="alert alert-info">История продаж и списаний этой партии.</div><a href="{% url 'inventory:batch-list' %}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Вернуться</a></div></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Партии товаров{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
@@ -28,8 +29,8 @@
|
||||
</td>
|
||||
<td>{{ batch.product.name }}</td>
|
||||
<td>{{ batch.warehouse.name }}</td>
|
||||
<td>{{ batch.quantity }}</td>
|
||||
<td>{{ batch.cost_price }} ₽</td>
|
||||
<td>{{ batch.quantity|smart_quantity }}</td>
|
||||
<td>{{ batch.cost_price }} руб.</td>
|
||||
<td>{{ batch.created_at|date:"d.m.Y" }}</td>
|
||||
<td>
|
||||
<a href="{% url 'inventory:batch-detail' batch.pk %}" class="btn btn-sm btn-outline-info" title="Просмотр партии">
|
||||
|
||||
@@ -3,147 +3,310 @@
|
||||
{% block title %}Склад{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row mb-4">
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h1 class="display-5">Управление складом</h1>
|
||||
<p class="lead text-muted">Здесь будут инструменты для управления инвентаризацией и складским учетом</p>
|
||||
<h1 class="mb-2">Управление складом</h1>
|
||||
<p class="text-muted">Выберите операцию для работы</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Основные операции -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-building"></i> Управление складами
|
||||
</h5>
|
||||
<p class="card-text text-muted">Создание и управление физическими складами</p>
|
||||
<a href="{% url 'inventory:warehouse-list' %}" class="btn btn-outline-primary">Перейти</a>
|
||||
<!-- Основные операции -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h5 class="text-uppercase text-muted mb-3">
|
||||
<small>Основные операции</small>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-5">
|
||||
<!-- Управление складами -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:warehouse-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card primary-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper primary-icon mb-3">
|
||||
<i class="bi bi-building"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Управление складами</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-arrow-down-square"></i> Приход товара
|
||||
</h5>
|
||||
<p class="card-text text-muted">Регистрация поступления товаров на склад</p>
|
||||
<a href="{% url 'inventory:incoming-list' %}" class="btn btn-outline-primary">Перейти</a>
|
||||
<!-- Приход товара -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:incoming-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card success-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper success-icon mb-3">
|
||||
<i class="bi bi-arrow-down-square"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Приход товара</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-arrow-up-square"></i> Реализация товара
|
||||
</h5>
|
||||
<p class="card-text text-muted">Учет проданных товаров с применением FIFO</p>
|
||||
<a href="{% url 'inventory:sale-list' %}" class="btn btn-outline-primary">Перейти</a>
|
||||
<!-- Реализация товара -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:sale-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card warning-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper warning-icon mb-3">
|
||||
<i class="bi bi-arrow-up-square"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Реализация товара</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-clipboard-check"></i> Инвентаризация
|
||||
</h5>
|
||||
<p class="card-text text-muted">Проверка фактических остатков и корректировка</p>
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="btn btn-outline-primary">Перейти</a>
|
||||
<!-- Инвентаризация -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card info-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper info-icon mb-3">
|
||||
<i class="bi bi-clipboard-check"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Инвентаризация</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные операции -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-x-circle"></i> Списание товара
|
||||
</h5>
|
||||
<p class="card-text text-muted">Списание брака, порчи, недостач</p>
|
||||
<a href="{% url 'inventory:writeoff-list' %}" class="btn btn-outline-secondary">Перейти</a>
|
||||
<!-- Списание товара -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:writeoff-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card danger-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper danger-icon mb-3">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Списание товара</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-arrow-left-right"></i> Перемещение товара
|
||||
</h5>
|
||||
<p class="card-text text-muted">Перемещение между складами с сохранением партийности</p>
|
||||
<a href="{% url 'inventory:transfer-list' %}" class="btn btn-outline-secondary">Перейти</a>
|
||||
<!-- Перемещение товара -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:transfer-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card secondary-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper secondary-icon mb-3">
|
||||
<i class="bi bi-arrow-left-right"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Перемещение товара</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Справочная информация -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<h5 class="text-uppercase text-muted mb-3">
|
||||
<small>Справочная информация</small>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Остатки товаров -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:stock-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card stock-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper stock-icon mb-3">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Остатки товаров</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Справочная информация -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-box-seam"></i> Остатки товаров
|
||||
</h5>
|
||||
<p class="card-text text-muted">Просмотр текущих остатков по складам и товарам</p>
|
||||
<a href="{% url 'inventory:stock-list' %}" class="btn btn-outline-info">Перейти</a>
|
||||
<!-- Партии товаров -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:batch-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card batch-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper batch-icon mb-3">
|
||||
<i class="bi bi-diagram-3"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Партии товаров</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-diagram-3"></i> Партии товаров
|
||||
</h5>
|
||||
<p class="card-text text-muted">История партий и их распределение</p>
|
||||
<a href="{% url 'inventory:batch-list' %}" class="btn btn-outline-info">Перейти</a>
|
||||
<!-- Журнал операций -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<a href="{% url 'inventory:movement-list' %}" class="card-link">
|
||||
<div class="card h-100 compact-card journal-card">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="icon-wrapper journal-icon mb-3">
|
||||
<i class="bi bi-journal-check"></i>
|
||||
</div>
|
||||
<h6 class="card-title">Журнал операций</h6>
|
||||
<div class="mt-auto">
|
||||
<span class="btn-text">Перейти →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-journal-check"></i> Журнал операций
|
||||
</h5>
|
||||
<p class="card-text text-muted">Полный журнал всех складских движений</p>
|
||||
<a href="{% url 'inventory:movement-list' %}" class="btn btn-outline-info">Перейти</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
.card-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.compact-card {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s ease;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
.compact-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Цветовые схемы для иконок */
|
||||
.primary-icon {
|
||||
background: rgba(13, 110, 253, 0.15);
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
background: rgba(25, 135, 84, 0.15);
|
||||
color: #198754;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
background: rgba(255, 193, 7, 0.15);
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
background: rgba(23, 162, 184, 0.15);
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.danger-icon {
|
||||
background: rgba(220, 53, 69, 0.15);
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.secondary-icon {
|
||||
background: rgba(108, 117, 125, 0.15);
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.stock-icon {
|
||||
background: rgba(13, 202, 240, 0.15);
|
||||
color: #0dcaf0;
|
||||
}
|
||||
|
||||
.batch-icon {
|
||||
background: rgba(111, 66, 193, 0.15);
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.journal-icon {
|
||||
background: rgba(253, 126, 20, 0.15);
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: #6c757d;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.compact-card:hover .btn-text {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
/* Легкий фон для вызва категорий */
|
||||
.text-uppercase {
|
||||
letter-spacing: 1px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.compact-card {
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Массовое поступление товара{% endblock %}
|
||||
{% block inventory_content %}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}Отмена приходу товара{% endblock %}
|
||||
|
||||
@@ -23,8 +24,8 @@
|
||||
<ul class="mb-0">
|
||||
<li><strong>Товар:</strong> {{ incoming.product.name }}</li>
|
||||
<li><strong>Склад:</strong> {{ incoming.warehouse.name }}</li>
|
||||
<li><strong>Количество:</strong> {{ incoming.quantity }} шт</li>
|
||||
<li><strong>Цена закупки:</strong> {{ incoming.cost_price }} ₽</li>
|
||||
<li><strong>Количество:</strong> {{ incoming.quantity|smart_quantity }} шт</li>
|
||||
<li><strong>Цена закупки:</strong> {{ incoming.cost_price }} руб.</li>
|
||||
{% if incoming.document_number %}
|
||||
<li><strong>Номер документа:</strong> {{ incoming.document_number }}</li>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}
|
||||
{% if form.instance.pk %}
|
||||
@@ -55,7 +56,7 @@
|
||||
<label for="{{ form.quantity.id_for_label }}" class="form-label">
|
||||
{{ form.quantity.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.quantity }}
|
||||
{{ form.quantity|smart_quantity }}
|
||||
{% if form.quantity.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.quantity.errors %}{{ error }}{% endfor %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}История приходов товара{% endblock %}
|
||||
|
||||
@@ -32,8 +33,8 @@
|
||||
<tr>
|
||||
<td><strong>{{ incoming.product.name }}</strong></td>
|
||||
<td>{{ incoming.batch.warehouse.name }}</td>
|
||||
<td>{{ incoming.quantity }} шт</td>
|
||||
<td>{{ incoming.cost_price }} ₽</td>
|
||||
<td>{{ incoming.quantity|smart_quantity }} шт</td>
|
||||
<td>{{ incoming.cost_price }} руб.</td>
|
||||
<td>
|
||||
{% if incoming.batch.document_number %}
|
||||
<code>{{ incoming.batch.document_number }}</code>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Партия {{ batch.document_number }}{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
@@ -71,11 +72,11 @@
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.product.name }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ item.cost_price }} ₽</td>
|
||||
<td>{{ item.quantity|smart_quantity }}</td>
|
||||
<td>{{ item.cost_price }} руб.</td>
|
||||
<td>
|
||||
{% widthratio item.quantity 1 item.cost_price as total_price %}
|
||||
<strong>{{ total_price|floatformat:2 }} ₽</strong>
|
||||
<strong>{{ total_price|floatformat:2 }} руб.</strong>
|
||||
</td>
|
||||
<td>
|
||||
{% if item.stock_batch %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}Детали инвентаризации{% endblock %}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}Внесение результатов инвентаризации{% endblock %}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Новое резервирование{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Резервирование товара</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.product.label }} *</label>{{ form.product }}</div><div class="mb-3"><label class="form-label">{{ form.warehouse.label }} *</label>{{ form.warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.quantity.label }} *</label>{{ form.quantity }}</div><div class="mb-3"><label class="form-label">{{ form.order_item.label }}</label>{{ form.order_item }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:reservation-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Резервирование товара</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.product.label }} *</label>{{ form.product }}</div><div class="mb-3"><label class="form-label">{{ form.warehouse.label }} *</label>{{ form.warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.quantity.label }} *</label>{{ form.quantity|smart_quantity }}</div><div class="mb-3"><label class="form-label">{{ form.order_item.label }}</label>{{ form.order_item }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:reservation-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
|
||||
<style>select,input{width:100%;}</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Резервирования{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Активные резервирования <a href="{% url 'inventory:reservation-create' %}" class="btn btn-primary btn-sm float-end"><i class="bi bi-plus-circle"></i> Новое</a></h4></div><div class="card-body">{% if reservations %}<table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Кол-во</th><th>Склад</th><th>Зарезервировано</th><th class="text-end">Действия</th></tr></thead><tbody>{% for r in reservations %}<tr><td>{{ r.product.name }}</td><td>{{ r.quantity }}</td><td>{{ r.warehouse.name }}</td><td>{{ r.reserved_at|date:"d.m.Y" }}</td><td class="text-end"><a href="{% url 'inventory:reservation-update' r.pk %}" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a></td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Резервирований не найдено.</div>{% endif %}</div></div>
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Активные резервирования <a href="{% url 'inventory:reservation-create' %}" class="btn btn-primary btn-sm float-end"><i class="bi bi-plus-circle"></i> Новое</a></h4></div><div class="card-body">{% if reservations %}<table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Кол-во</th><th>Склад</th><th>Зарезервировано</th><th class="text-end">Действия</th></tr></thead><tbody>{% for r in reservations %}<tr><td>{{ r.product.name }}</td><td>{{ r.quantity|smart_quantity }}</td><td>{{ r.warehouse.name }}</td><td>{{ r.reserved_at|date:"d.m.Y" }}</td><td class="text-end"><a href="{% url 'inventory:reservation-update' r.pk %}" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a></td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Резервирований не найдено.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}Отмена продажи{% endblock %}
|
||||
|
||||
@@ -23,8 +24,8 @@
|
||||
<ul class="mb-0">
|
||||
<li><strong>Товар:</strong> {{ sale.product.name }}</li>
|
||||
<li><strong>Склад:</strong> {{ sale.warehouse.name }}</li>
|
||||
<li><strong>Количество:</strong> {{ sale.quantity }} шт</li>
|
||||
<li><strong>Цена продажи:</strong> {{ sale.sale_price }} ₽</li>
|
||||
<li><strong>Количество:</strong> {{ sale.quantity|smart_quantity }} шт</li>
|
||||
<li><strong>Цена продажи:</strong> {{ sale.sale_price }} руб.</li>
|
||||
<li><strong>Статус:</strong>
|
||||
{% if sale.processed %}
|
||||
Обработана
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}Детали продажи{% endblock %}
|
||||
|
||||
@@ -26,15 +27,15 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Количество:</th>
|
||||
<td><strong>{{ sale.quantity }} шт</strong></td>
|
||||
<td><strong>{{ sale.quantity|smart_quantity }} шт</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Цена продажи:</th>
|
||||
<td><strong>{{ sale.sale_price }} ₽</strong></td>
|
||||
<td><strong>{{ sale.sale_price }} руб.</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Сумма:</th>
|
||||
<td><strong>{{ sale.quantity|add:0|multiply:sale.sale_price }} ₽</strong></td>
|
||||
<td><strong>{{ sale.quantity|add:0|multiply:sale.sale_price }} руб.</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -96,16 +97,16 @@
|
||||
<code>Партия #{{ allocation.batch.id }}</code>
|
||||
</td>
|
||||
<td>{{ allocation.batch.created_at|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ allocation.quantity }} шт</td>
|
||||
<td>{{ allocation.cost_price }} ₽</td>
|
||||
<td><strong>{{ allocation.quantity|add:0|multiply:allocation.cost_price }} ₽</strong></td>
|
||||
<td>{{ allocation.quantity|smart_quantity }} шт</td>
|
||||
<td>{{ allocation.cost_price }} руб.</td>
|
||||
<td><strong>{{ allocation.quantity|add:0|multiply:allocation.cost_price }} руб.</strong></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot class="table-light">
|
||||
<tr>
|
||||
<th colspan="2">Итого:</th>
|
||||
<th>{{ sale.quantity }} шт</th>
|
||||
<th>{{ sale.quantity|smart_quantity }} шт</th>
|
||||
<th colspan="2">
|
||||
<strong>
|
||||
{% comment %} Сумма всех закупочных цен {% endcomment %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}
|
||||
{% if form.instance.pk %}
|
||||
@@ -55,7 +56,7 @@
|
||||
<label for="{{ form.quantity.id_for_label }}" class="form-label">
|
||||
{{ form.quantity.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.quantity }}
|
||||
{{ form.quantity|smart_quantity }}
|
||||
{% if form.quantity.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.quantity.errors %}{{ error }}{% endfor %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
|
||||
{% block inventory_title %}История продаж{% endblock %}
|
||||
|
||||
@@ -32,8 +33,8 @@
|
||||
<tr>
|
||||
<td><strong>{{ sale.product.name }}</strong></td>
|
||||
<td>{{ sale.warehouse.name }}</td>
|
||||
<td>{{ sale.quantity }} шт</td>
|
||||
<td>{{ sale.sale_price }} ₽</td>
|
||||
<td>{{ sale.quantity|smart_quantity }} шт</td>
|
||||
<td>{{ sale.sale_price }} руб.</td>
|
||||
<td>
|
||||
{% if sale.order %}
|
||||
<code>{{ sale.order.order_number }}</code>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Остатки товара{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">{{ stock.product.name }} на {{ stock.warehouse.name }}</h4></div><div class="card-body"><table class="table table-borderless"><tr><th>Товар:</th><td><strong>{{ stock.product.name }}</strong></td></tr><tr><th>Склад:</th><td>{{ stock.warehouse.name }}</td></tr><tr><th>Доступно:</th><td><strong>{{ stock.quantity_available }} шт</strong></td></tr><tr><th>Зарезервировано:</th><td>{{ stock.quantity_reserved }} шт</td></tr><tr><th>Свободно:</th><td><strong>{{ stock.quantity_free }} шт</strong></td></tr><tr><th>Последнее обновление:</th><td>{{ stock.updated_at|date:"d.m.Y H:i" }}</td></tr></table><a href="{% url 'inventory:stock-list' %}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Вернуться</a></div></div>
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">{{ stock.product.name }} на {{ stock.warehouse.name }}</h4></div><div class="card-body"><table class="table table-borderless"><tr><th>Товар:</th><td><strong>{{ stock.product.name }}</strong></td></tr><tr><th>Склад:</th><td>{{ stock.warehouse.name }}</td></tr><tr><th>Доступно:</th><td><strong>{{ stock.quantity_available|smart_quantity }} шт</strong></td></tr><tr><th>Зарезервировано:</th><td>{{ stock.quantity_reserved|smart_quantity }} шт</td></tr><tr><th>Свободно:</th><td><strong>{{ stock.quantity_free|smart_quantity }} шт</strong></td></tr><tr><th>Последнее обновление:</th><td>{{ stock.updated_at|date:"d.m.Y H:i" }}</td></tr></table><a href="{% url 'inventory:stock-list' %}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Вернуться</a></div></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Остатки товаров{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Остатки на складах</h4></div><div class="card-body">{% if stocks %}<table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Склад</th><th>Доступно</th><th>Зарезервировано</th><th>Свободно</th><th>Последний обновления</th></tr></thead><tbody>{% for stock in stocks %}<tr><td><a href="{% url 'inventory:stock-detail' stock.pk %}">{{ stock.product.name }}</a></td><td>{{ stock.warehouse.name }}</td><td>{{ stock.quantity_available }}</td><td>{{ stock.quantity_reserved }}</td><td><strong>{{ stock.quantity_free }}</strong></td><td>{{ stock.updated_at|date:"d.m.Y H:i" }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Остатки не найдены.</div>{% endif %}</div></div>
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Остатки на складах</h4></div><div class="card-body">{% if stocks %}<table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Склад</th><th>Доступно</th><th>Зарезервировано</th><th>Свободно</th><th>Последний обновления</th></tr></thead><tbody>{% for stock in stocks %}<tr><td><a href="{% url 'inventory:stock-detail' stock.pk %}">{{ stock.product.name }}</a></td><td>{{ stock.warehouse.name }}</td><td>{{ stock.quantity_available|smart_quantity }}</td><td>{{ stock.quantity_reserved|smart_quantity }}</td><td><strong>{{ stock.quantity_free|smart_quantity }}</strong></td><td>{{ stock.updated_at|date:"d.m.Y H:i" }}</td></tr>{% endfor %}</tbody></table>{% else %}<div class="alert alert-info">Остатки не найдены.</div>{% endif %}</div></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Перемещение товара{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card"><div class="card-header"><h4 class="mb-0">Перемещение товара</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.batch.label }} *</label>{{ form.batch }}</div><div class="mb-3"><label class="form-label">{{ form.from_warehouse.label }} *</label>{{ form.from_warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.to_warehouse.label }} *</label>{{ form.to_warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.quantity.label }} *</label>{{ form.quantity }}</div><div class="mb-3"><label class="form-label">{{ form.document_number.label }}</label>{{ form.document_number }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:transfer-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
|
||||
<div class="card"><div class="card-header"><h4 class="mb-0">Перемещение товара</h4></div><div class="card-body"><form method="post">{% csrf_token %}<div class="mb-3"><label class="form-label">{{ form.batch.label }} *</label>{{ form.batch }}</div><div class="mb-3"><label class="form-label">{{ form.from_warehouse.label }} *</label>{{ form.from_warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.to_warehouse.label }} *</label>{{ form.to_warehouse }}</div><div class="mb-3"><label class="form-label">{{ form.quantity.label }} *</label>{{ form.quantity|smart_quantity }}</div><div class="mb-3"><label class="form-label">{{ form.document_number.label }}</label>{{ form.document_number }}</div><div class="d-flex gap-2"><button type="submit" class="btn btn-primary">Сохранить</button><a href="{% url 'inventory:transfer-list' %}" class="btn btn-secondary">Отмена</a></div></form></div></div>
|
||||
<style>select,textarea,input{width:100%;}</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}Перемещение товаров{% endblock %}
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Перемещение товаров между складами <a href="{% url 'inventory:transfer-create' %}" class="btn btn-primary btn-sm float-end"><i class="bi bi-plus-circle"></i> Новое</a></h4></div><div class="card-body">{% if transfers %}<div class="table-responsive"><table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Из</th><th>В</th><th>Кол-во</th><th>Дата</th><th class="text-end">Действия</th></tr></thead><tbody>{% for t in transfers %}<tr><td>{{ t.batch.product.name }}</td><td>{{ t.from_warehouse.name }}</td><td>{{ t.to_warehouse.name }}</td><td>{{ t.quantity }}</td><td>{{ t.date|date:"d.m.Y" }}</td><td class="text-end"><a href="{% url 'inventory:transfer-update' t.pk %}" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a><a href="{% url 'inventory:transfer-delete' t.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>
|
||||
{% block inventory_content %}<div class="card"><div class="card-header"><h4 class="mb-0">Перемещение товаров между складами <a href="{% url 'inventory:transfer-create' %}" class="btn btn-primary btn-sm float-end"><i class="bi bi-plus-circle"></i> Новое</a></h4></div><div class="card-body">{% if transfers %}<div class="table-responsive"><table class="table table-hover table-sm"><thead><tr><th>Товар</th><th>Из</th><th>В</th><th>Кол-во</th><th>Дата</th><th class="text-end">Действия</th></tr></thead><tbody>{% for t in transfers %}<tr><td>{{ t.batch.product.name }}</td><td>{{ t.from_warehouse.name }}</td><td>{{ t.to_warehouse.name }}</td><td>{{ t.quantity|smart_quantity }}</td><td>{{ t.date|date:"d.m.Y" }}</td><td class="text-end"><a href="{% url 'inventory:transfer-update' t.pk %}" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a><a href="{% url 'inventory:transfer-delete' t.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 %}
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
|
||||
{% block inventory_title %}Удаление склада{% endblock %}
|
||||
{% block inventory_title %}Архивирование склада{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h4 class="mb-0">Подтверждение удаления</h4>
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<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 class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
Вы собираетесь архивировать склад <strong>"{{ warehouse.name }}"</strong>
|
||||
</div>
|
||||
|
||||
<p class="text-muted">
|
||||
Этот склад будет деактивирован и скрыт из основного списка.
|
||||
<p>
|
||||
<strong>Что произойдет после архивирования:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>✓ Склад исчезнет из списка активных складов</li>
|
||||
<li>✓ Новые документы нельзя будет создавать для этого склада</li>
|
||||
<li>✓ Историю операций можно будет посмотреть в архиве</li>
|
||||
</ul>
|
||||
|
||||
<h5>Склад: <strong>{{ warehouse.name }}</strong></h5>
|
||||
<div class="alert alert-secondary mt-3">
|
||||
<small>Вы всегда сможете вернуть склад, отредактировав его позже.</small>
|
||||
</div>
|
||||
|
||||
{% if warehouse.description %}
|
||||
<p class="text-muted">{{ warehouse.description }}</p>
|
||||
@@ -28,8 +35,8 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Подтвердить удаление
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-archive"></i> Архивировать
|
||||
</button>
|
||||
<a href="{% url 'inventory:warehouse-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Отменить
|
||||
|
||||
@@ -67,6 +67,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input"
|
||||
id="{{ form.is_default.id_for_label }}" name="{{ form.is_default.html_name }}"
|
||||
{% if form.is_default.value %}checked{% endif %}>
|
||||
<label class="form-check-label" for="{{ form.is_default.id_for_label }}">
|
||||
{{ form.is_default.label }}
|
||||
<small class="text-muted d-block">
|
||||
Отмечьте, чтобы использовать этот склад по умолчанию при создании новых документов
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
{% block inventory_title %}Управление складами{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<!-- Скрытое поле для CSRF токена (нужно для AJAX запросов) -->
|
||||
<div style="display: none;">
|
||||
{% csrf_token %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Список складов</h4>
|
||||
@@ -17,6 +21,7 @@
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">✓</th>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th>Статус</th>
|
||||
@@ -26,8 +31,20 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for warehouse in warehouses %}
|
||||
<tr>
|
||||
<td><strong>{{ warehouse.name }}</strong></td>
|
||||
<tr {% if warehouse.is_default %}class="table-warning"{% endif %} data-warehouse-id="{{ warehouse.pk }}">
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="default-warehouse-checkbox"
|
||||
data-warehouse-id="{{ warehouse.pk }}"
|
||||
data-set-default-url="{% url 'inventory:warehouse-set-default' warehouse.pk %}"
|
||||
{% if warehouse.is_default %}checked{% endif %}
|
||||
style="cursor: pointer; width: 18px; height: 18px;">
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ warehouse.name }}</strong>
|
||||
{% if warehouse.is_default %}
|
||||
<span class="badge bg-warning text-dark ms-2">По умолчанию</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ warehouse.description|truncatewords:10 }}</td>
|
||||
<td>
|
||||
{% if warehouse.is_active %}
|
||||
@@ -95,4 +112,145 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Обработчик для галочек "По умолчанию"
|
||||
const checkboxes = document.querySelectorAll('.default-warehouse-checkbox');
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const warehouseId = this.dataset.warehouseId;
|
||||
const setDefaultUrl = this.dataset.setDefaultUrl;
|
||||
|
||||
// Получаем CSRF токен из скрытого input в форме
|
||||
let csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
|
||||
|
||||
// Если токена нет в form, ищем в meta тегах
|
||||
if (!csrfToken) {
|
||||
csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
}
|
||||
|
||||
// Если токена еще нет, ищем его в самой странице через Cookies
|
||||
if (!csrfToken) {
|
||||
const name = 'csrftoken';
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
csrfToken = cookieValue;
|
||||
}
|
||||
|
||||
console.log('CSRF Token:', csrfToken ? 'найден (' + csrfToken.length + ' символов)' : 'не найден');
|
||||
|
||||
// Если галочка установлена, отправляем запрос
|
||||
if (this.checked) {
|
||||
// Визуально обновляем таблицу сразу (оптимистичное обновление)
|
||||
document.querySelectorAll('input.default-warehouse-checkbox').forEach(cb => {
|
||||
cb.checked = false;
|
||||
});
|
||||
document.querySelectorAll('tr[data-warehouse-id]').forEach(tr => {
|
||||
tr.classList.remove('table-warning');
|
||||
tr.querySelector('.badge.bg-warning')?.remove();
|
||||
});
|
||||
|
||||
// Отмечаем текущую строку
|
||||
this.checked = true;
|
||||
const currentRow = document.querySelector(`tr[data-warehouse-id="${warehouseId}"]`);
|
||||
currentRow.classList.add('table-warning');
|
||||
|
||||
// Добавляем бейдж "По умолчанию" если его нет
|
||||
const nameCell = currentRow.querySelector('td:nth-child(2)');
|
||||
if (!nameCell.querySelector('.badge.bg-warning')) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge bg-warning text-dark ms-2';
|
||||
badge.textContent = 'По умолчанию';
|
||||
nameCell.appendChild(badge);
|
||||
}
|
||||
|
||||
// Отправляем AJAX запрос на правильный URL из атрибута data
|
||||
console.log('Отправляем запрос на:', setDefaultUrl);
|
||||
console.log('С заголовками:', {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken ? '***' + csrfToken.slice(-10) : 'не найден'
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// Добавляем CSRF токен если он найден
|
||||
if (csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
}
|
||||
|
||||
fetch(setDefaultUrl, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Ответ сервера:', response.status);
|
||||
if (!response.ok) {
|
||||
return response.text().then(text => {
|
||||
throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Данные:', data);
|
||||
if (data.status === 'success') {
|
||||
console.log(data.message);
|
||||
// Показываем уведомление
|
||||
showNotification(data.message, 'success');
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Ошибка при запросе:', error);
|
||||
// Откатываем визуальные изменения при ошибке
|
||||
showNotification('Ошибка при установке склада по умолчанию: ' + error.message, 'error');
|
||||
// Перезагружаем через 2 секунды
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Функция для показа уведомлений
|
||||
function showNotification(message, type = 'info') {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const cardBody = document.querySelector('.card-body');
|
||||
const alertElement = document.createElement('div');
|
||||
alertElement.innerHTML = alertHtml;
|
||||
cardBody.insertBefore(alertElement.firstElementChild, cardBody.firstChild);
|
||||
|
||||
// Автоматически скрываем через 4 секунды
|
||||
setTimeout(() => {
|
||||
const alert = cardBody.querySelector('.alert');
|
||||
if (alert) {
|
||||
alert.remove();
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}{% if form.instance.pk %}Редактирование списания{% else %}Новое списание{% endif %}{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
@@ -36,7 +37,7 @@
|
||||
<!-- Поле Количество -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.quantity.label }} <span class="text-danger">*</span></label>
|
||||
{{ form.quantity }}
|
||||
{{ form.quantity|smart_quantity }}
|
||||
{% if form.quantity.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.quantity.errors.0 }}</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'inventory/base_inventory.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% block inventory_title %}История списаний{% endblock %}
|
||||
{% block inventory_content %}
|
||||
<div class="card">
|
||||
@@ -25,7 +26,7 @@
|
||||
{% for writeoff in writeoffs %}
|
||||
<tr>
|
||||
<td><strong>{{ writeoff.batch.product.name }}</strong></td>
|
||||
<td>{{ writeoff.quantity }} шт</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">
|
||||
|
||||
1
myproject/inventory/templatetags/__init__.py
Normal file
1
myproject/inventory/templatetags/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Template tags package for inventory app
|
||||
98
myproject/inventory/templatetags/inventory_filters.py
Normal file
98
myproject/inventory/templatetags/inventory_filters.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Custom template filters for inventory app.
|
||||
"""
|
||||
from django import template
|
||||
from decimal import Decimal
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name='smart_quantity')
|
||||
def smart_quantity(value):
|
||||
"""
|
||||
Форматирует количество товара:
|
||||
- Если число целое (например 5.0, 10.000), показывает без дробной части: 5, 10
|
||||
- Если число дробное (например 2.5, 3.125), убирает лишние нули: 2,5 вместо 2,500
|
||||
|
||||
Примеры:
|
||||
5.000 -> 5
|
||||
2.500 -> 2,5
|
||||
3.140 -> 3,14
|
||||
10.0 -> 10
|
||||
|
||||
Args:
|
||||
value: число (int, float, Decimal или строка)
|
||||
|
||||
Returns:
|
||||
str: отформатированное количество
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
|
||||
try:
|
||||
# Преобразуем в Decimal для точности
|
||||
if isinstance(value, str):
|
||||
num = Decimal(value)
|
||||
elif isinstance(value, (int, float)):
|
||||
num = Decimal(str(value))
|
||||
elif isinstance(value, Decimal):
|
||||
num = value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
# Проверяем, является ли число целым
|
||||
if num == num.to_integral_value():
|
||||
# Возвращаем как целое число
|
||||
return f"{int(num)}"
|
||||
else:
|
||||
# Убираем лишние нули справа и форматируем с запятой
|
||||
# Используем normalize() для удаления лишних нулей
|
||||
normalized = num.normalize()
|
||||
# Форматируем с запятой вместо точки (русский формат)
|
||||
result = str(normalized).replace('.', ',')
|
||||
return result
|
||||
|
||||
except (ValueError, TypeError, ArithmeticError):
|
||||
# Если не удалось преобразовать, возвращаем как есть
|
||||
return str(value)
|
||||
|
||||
|
||||
@register.filter(name='format_decimal')
|
||||
def format_decimal(value, decimal_places=2):
|
||||
"""
|
||||
Форматирует decimal число с заданным количеством знаков после запятой.
|
||||
Убирает лишние нули справа.
|
||||
|
||||
Args:
|
||||
value: число для форматирования
|
||||
decimal_places: максимальное количество знаков после запятой
|
||||
|
||||
Returns:
|
||||
str: отформатированное число
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
|
||||
try:
|
||||
if isinstance(value, str):
|
||||
num = Decimal(value)
|
||||
elif isinstance(value, (int, float)):
|
||||
num = Decimal(str(value))
|
||||
elif isinstance(value, Decimal):
|
||||
num = value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
# Округляем до заданного количества знаков
|
||||
quantize_value = Decimal(10) ** -decimal_places
|
||||
rounded = num.quantize(quantize_value)
|
||||
|
||||
# Убираем лишние нули
|
||||
normalized = rounded.normalize()
|
||||
|
||||
# Форматируем с запятой
|
||||
result = str(normalized).replace('.', ',')
|
||||
return result
|
||||
|
||||
except (ValueError, TypeError, ArithmeticError):
|
||||
return str(value)
|
||||
@@ -2,7 +2,7 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
# Warehouse
|
||||
WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView,
|
||||
WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView, SetDefaultWarehouseView,
|
||||
# Incoming
|
||||
IncomingListView, IncomingCreateView, IncomingUpdateView, IncomingDeleteView,
|
||||
# IncomingBatch
|
||||
@@ -39,6 +39,7 @@ urlpatterns = [
|
||||
path('warehouses/create/', WarehouseCreateView.as_view(), name='warehouse-create'),
|
||||
path('warehouses/<int:pk>/edit/', WarehouseUpdateView.as_view(), name='warehouse-update'),
|
||||
path('warehouses/<int:pk>/delete/', WarehouseDeleteView.as_view(), name='warehouse-delete'),
|
||||
path('warehouses/<int:pk>/set-default/', SetDefaultWarehouseView.as_view(), name='warehouse-set-default'),
|
||||
|
||||
# ==================== INCOMING ====================
|
||||
path('incoming/', IncomingListView.as_view(), name='incoming-list'),
|
||||
|
||||
@@ -18,7 +18,7 @@ Inventory Views Package
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from .warehouse import WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView
|
||||
from .warehouse import WarehouseListView, WarehouseCreateView, WarehouseUpdateView, WarehouseDeleteView, SetDefaultWarehouseView
|
||||
from .incoming import IncomingListView, IncomingCreateView, IncomingUpdateView, IncomingDeleteView
|
||||
from .batch import IncomingBatchListView, IncomingBatchDetailView, StockBatchListView, StockBatchDetailView
|
||||
from .sale import SaleListView, SaleCreateView, SaleUpdateView, SaleDeleteView, SaleDetailView
|
||||
@@ -46,7 +46,7 @@ __all__ = [
|
||||
# Home
|
||||
'inventory_home',
|
||||
# Warehouse
|
||||
'WarehouseListView', 'WarehouseCreateView', 'WarehouseUpdateView', 'WarehouseDeleteView',
|
||||
'WarehouseListView', 'WarehouseCreateView', 'WarehouseUpdateView', 'WarehouseDeleteView', 'SetDefaultWarehouseView',
|
||||
# Incoming
|
||||
'IncomingListView', 'IncomingCreateView', 'IncomingUpdateView', 'IncomingDeleteView',
|
||||
# IncomingBatch
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, View
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse, HttpResponseRedirect
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.utils.decorators import method_decorator
|
||||
from ..models import Warehouse
|
||||
from ..forms import WarehouseForm
|
||||
|
||||
@@ -11,6 +14,7 @@ from ..forms import WarehouseForm
|
||||
class WarehouseListView(LoginRequiredMixin, ListView):
|
||||
"""
|
||||
Список всех складов тенанта
|
||||
Сортирует по is_default (по умолчанию первым), потом по названию
|
||||
"""
|
||||
model = Warehouse
|
||||
template_name = 'inventory/warehouse/warehouse_list.html'
|
||||
@@ -18,7 +22,8 @@ class WarehouseListView(LoginRequiredMixin, ListView):
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
return Warehouse.objects.filter(is_active=True).order_by('name')
|
||||
# Сортируем: сначала is_default DESC (по умолчанию первый), потом по названию
|
||||
return Warehouse.objects.filter(is_active=True).order_by('-is_default', 'name')
|
||||
|
||||
|
||||
class WarehouseCreateView(LoginRequiredMixin, CreateView):
|
||||
@@ -51,16 +56,61 @@ class WarehouseUpdateView(LoginRequiredMixin, UpdateView):
|
||||
|
||||
class WarehouseDeleteView(LoginRequiredMixin, DeleteView):
|
||||
"""
|
||||
Удаление склада (мягкое удаление - деактивация)
|
||||
Удаление склада (мягкое удаление - деактивация).
|
||||
Вместо физического удаления из БД, устанавливаем is_active=False
|
||||
"""
|
||||
model = Warehouse
|
||||
template_name = 'inventory/warehouse/warehouse_confirm_delete.html'
|
||||
success_url = reverse_lazy('inventory:warehouse-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
# Мягкое удаление - просто деактивируем
|
||||
warehouse = self.get_object()
|
||||
warehouse.is_active = False
|
||||
warehouse.save()
|
||||
messages.success(self.request, f'Склад "{warehouse.name}" деактивирован.')
|
||||
return super().form_valid(form)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Переопределяем POST метод чтобы использовать мягкое удаление
|
||||
вместо стандартного физического удаления Django
|
||||
"""
|
||||
self.object = self.get_object()
|
||||
warehouse_name = self.object.name
|
||||
|
||||
# Мягкое удаление - просто деактивируем склад
|
||||
self.object.is_active = False
|
||||
self.object.save()
|
||||
|
||||
messages.success(request, f'Склад "{warehouse_name}" архивирован и скрыт из списка.')
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
@method_decorator(require_http_methods(["POST"]), name="dispatch")
|
||||
class SetDefaultWarehouseView(LoginRequiredMixin, View):
|
||||
"""
|
||||
Установка склада по умолчанию
|
||||
Обрабатывает POST запрос от AJAX и возвращает JSON ответ
|
||||
"""
|
||||
|
||||
def post(self, request, pk):
|
||||
"""
|
||||
Установить склад с заданным pk как склад по умолчанию
|
||||
"""
|
||||
try:
|
||||
warehouse = get_object_or_404(Warehouse, pk=pk, is_active=True)
|
||||
|
||||
# Установить этот склад как по умолчанию
|
||||
# (метод save() в модели автоматически снимет флаг с других)
|
||||
warehouse.is_default = True
|
||||
warehouse.save()
|
||||
|
||||
return JsonResponse({
|
||||
'status': 'success',
|
||||
'message': f'Склад "{warehouse.name}" установлен по умолчанию',
|
||||
'warehouse_id': warehouse.id,
|
||||
'warehouse_name': warehouse.name
|
||||
})
|
||||
except Warehouse.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
'message': 'Склад не найден'
|
||||
}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}, status=500)
|
||||
|
||||
Reference in New Issue
Block a user