Files
octopus/myproject/products/views/uom_views.py
Andrey Smakotin e831c4fb6e feat(products): реализована система единиц продажи на фронтенде
Добавлена полноценная интеграция единиц измерения (UoM) для продажи
товаров в разных единицах с автоматическим пересчётом цен и остатков.

## Основные изменения:

### Backend
- Расширен API поиска товаров (api_views.py): добавлена сериализация sales_units
- Создан новый endpoint get_product_sales_units_api для загрузки единиц с остатками
- Добавлено поле sales_unit в OrderItemForm и SaleForm с валидацией
- Созданы CRUD views для управления единицами продажи (uom_views.py)
- Обновлена ProductForm: использует base_unit вместо устаревшего unit

### Frontend
- Создан модуль sales-units.js с функциями для работы с единицами
- Интегрирован в select2-product-search.js: автозагрузка единиц при выборе товара
- Добавлены контейнеры для единиц в order_form.html и sale_form.html
- Реализовано автоматическое обновление цены при смене единицы продажи
- При выборе базовой единицы цена возвращается к базовой цене товара

### UI
- Добавлены страницы управления единицами продажи в навбар
- Созданы шаблоны: sales_unit_list.html, sales_unit_form.html, sales_unit_delete.html
- Добавлены фильтры по товару, единице, активности и дефолтности

## Исправленные ошибки:
- Порядок инициализации: обработчики устанавливаются ДО триггера события change
- Цена корректно обновляется при выборе единицы продажи
- При выборе "Базовая единица" возвращается базовая цена товара

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 12:35:01 +03:00

198 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Views для управления единицами измерения (Unit of Measure)
"""
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.shortcuts import render, redirect, get_object_or_404
from django.db.models import Q, Count
from django.core.paginator import Paginator
from django.urls import reverse
from products.models import UnitOfMeasure, ProductSalesUnit
from products.forms import ProductSalesUnitForm
@login_required
def unit_of_measure_list(request):
"""
Список всех единиц измерения с возможностью фильтрации и поиска
"""
# Получаем параметры фильтрации
search_query = request.GET.get('q', '').strip()
is_active_filter = request.GET.get('is_active', '')
# Базовый queryset
units = UnitOfMeasure.objects.all()
# Аннотируем количество использований
units = units.annotate(
usage_count=Count('productsalesunit')
)
# Применяем фильтры
if search_query:
units = units.filter(
Q(code__icontains=search_query) |
Q(name__icontains=search_query) |
Q(short_name__icontains=search_query)
)
if is_active_filter:
units = units.filter(is_active=(is_active_filter == 'true'))
# Сортировка
units = units.order_by('position', 'code')
# Пагинация
paginator = Paginator(units, 50)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'search_query': search_query,
'is_active_filter': is_active_filter,
'total_units': units.count(),
}
return render(request, 'products/uom/unit_list.html', context)
@login_required
def product_sales_unit_list(request):
"""
Список всех единиц продажи товаров с возможностью фильтрации
"""
# Получаем параметры фильтрации
search_query = request.GET.get('q', '').strip()
unit_filter = request.GET.get('unit', '')
is_active_filter = request.GET.get('is_active', '')
is_default_filter = request.GET.get('is_default', '')
# Базовый queryset
sales_units = ProductSalesUnit.objects.select_related(
'product', 'unit'
).all()
# Применяем фильтры
if search_query:
sales_units = sales_units.filter(
Q(product__name__icontains=search_query) |
Q(product__sku__icontains=search_query) |
Q(name__icontains=search_query)
)
if unit_filter:
sales_units = sales_units.filter(unit_id=unit_filter)
if is_active_filter:
sales_units = sales_units.filter(is_active=(is_active_filter == 'true'))
if is_default_filter:
sales_units = sales_units.filter(is_default=(is_default_filter == 'true'))
# Сортировка
sales_units = sales_units.order_by('product__name', 'position')
# Пагинация
paginator = Paginator(sales_units, 50)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
# Для фильтра единиц
all_units = UnitOfMeasure.objects.filter(is_active=True).order_by('position', 'code')
context = {
'page_obj': page_obj,
'search_query': search_query,
'unit_filter': unit_filter,
'is_active_filter': is_active_filter,
'is_default_filter': is_default_filter,
'all_units': all_units,
'total_sales_units': sales_units.count(),
}
return render(request, 'products/uom/sales_unit_list.html', context)
@login_required
def product_sales_unit_create(request):
"""
Создание новой единицы продажи
"""
if request.method == 'POST':
form = ProductSalesUnitForm(request.POST)
if form.is_valid():
sales_unit = form.save()
messages.success(
request,
f'Единица продажи "{sales_unit.name}" для товара "{sales_unit.product.name}" успешно создана!'
)
return redirect('products:sales-unit-list')
else:
# Предзаполнение товара если передан в параметрах
initial = {}
product_id = request.GET.get('product')
if product_id:
initial['product'] = product_id
form = ProductSalesUnitForm(initial=initial)
context = {
'form': form,
'title': 'Создание единицы продажи',
'submit_text': 'Создать'
}
return render(request, 'products/uom/sales_unit_form.html', context)
@login_required
def product_sales_unit_update(request, pk):
"""
Редактирование единицы продажи
"""
sales_unit = get_object_or_404(ProductSalesUnit, pk=pk)
if request.method == 'POST':
form = ProductSalesUnitForm(request.POST, instance=sales_unit)
if form.is_valid():
sales_unit = form.save()
messages.success(
request,
f'Единица продажи "{sales_unit.name}" успешно обновлена!'
)
return redirect('products:sales-unit-list')
else:
form = ProductSalesUnitForm(instance=sales_unit)
context = {
'form': form,
'sales_unit': sales_unit,
'title': f'Редактирование: {sales_unit.name}',
'submit_text': 'Сохранить'
}
return render(request, 'products/uom/sales_unit_form.html', context)
@login_required
def product_sales_unit_delete(request, pk):
"""
Удаление единицы продажи
"""
sales_unit = get_object_or_404(ProductSalesUnit, pk=pk)
if request.method == 'POST':
product_name = sales_unit.product.name
unit_name = sales_unit.name
sales_unit.delete()
messages.success(
request,
f'Единица продажи "{unit_name}" для товара "{product_name}" успешно удалена!'
)
return redirect('products:sales-unit-list')
context = {
'sales_unit': sales_unit,
}
return render(request, 'products/uom/sales_unit_delete.html', context)