feat: упростить создание заказов и рефакторинг единиц измерения
- Добавить inline-редактирование цен в списке товаров - Оптимизировать карточки товаров в POS-терминале - Рефакторинг моделей единиц измерения - Миграция unit -> base_unit в SalesUnit - Улучшить UI форм создания/редактирования товаров Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,6 +122,9 @@ from .catalog_views import CatalogView
|
||||
# Управление единицами измерения
|
||||
from .uom_views import (
|
||||
unit_of_measure_list,
|
||||
unit_of_measure_create,
|
||||
unit_of_measure_update,
|
||||
unit_of_measure_delete,
|
||||
product_sales_unit_list,
|
||||
product_sales_unit_create,
|
||||
product_sales_unit_update,
|
||||
@@ -228,6 +231,9 @@ __all__ = [
|
||||
|
||||
# Управление единицами измерения
|
||||
'unit_of_measure_list',
|
||||
'unit_of_measure_create',
|
||||
'unit_of_measure_update',
|
||||
'unit_of_measure_delete',
|
||||
'product_sales_unit_list',
|
||||
'product_sales_unit_create',
|
||||
'product_sales_unit_update',
|
||||
|
||||
@@ -114,7 +114,7 @@ def search_products_and_variants(request):
|
||||
numeric_id = int(item_id)
|
||||
|
||||
if item_type == 'product':
|
||||
product = Product.objects.prefetch_related('sales_units__unit').get(id=numeric_id, status='active')
|
||||
product = Product.objects.prefetch_related('sales_units').get(id=numeric_id, status='active')
|
||||
|
||||
# Сериализация единиц продажи
|
||||
sales_units_data = []
|
||||
@@ -122,8 +122,6 @@ def search_products_and_variants(request):
|
||||
sales_units_data.append({
|
||||
'id': su.id,
|
||||
'name': su.name,
|
||||
'unit_code': su.unit.code,
|
||||
'unit_short_name': su.unit.short_name,
|
||||
'price': str(su.price),
|
||||
'sale_price': str(su.sale_price) if su.sale_price else None,
|
||||
'actual_price': str(su.actual_price),
|
||||
@@ -216,7 +214,7 @@ def search_products_and_variants(request):
|
||||
|
||||
if search_type in ['all', 'product']:
|
||||
# Показываем последние добавленные активные товары
|
||||
products_qs = Product.objects.filter(status='active').prefetch_related('sales_units__unit')
|
||||
products_qs = Product.objects.filter(status='active').prefetch_related('sales_units')
|
||||
# Применяем фильтры
|
||||
products_qs = _apply_product_filters(products_qs, category_id, tag_id, stock_status, warehouse_id, skip_stock_filter)
|
||||
products = products_qs.order_by('-created_at')[:page_size]
|
||||
@@ -235,8 +233,6 @@ def search_products_and_variants(request):
|
||||
sales_units_data.append({
|
||||
'id': su.id,
|
||||
'name': su.name,
|
||||
'unit_code': su.unit.code,
|
||||
'unit_short_name': su.unit.short_name,
|
||||
'price': str(su.price),
|
||||
'sale_price': str(su.sale_price) if su.sale_price else None,
|
||||
'actual_price': str(su.actual_price),
|
||||
@@ -365,7 +361,7 @@ def search_products_and_variants(request):
|
||||
products_query = _apply_product_filters(products_query, category_id, tag_id, stock_status, warehouse_id, skip_stock_filter)
|
||||
|
||||
# Добавляем prefetch для единиц продажи
|
||||
products_query = products_query.prefetch_related('sales_units__unit')
|
||||
products_query = products_query.prefetch_related('sales_units')
|
||||
|
||||
total_products = products_query.count()
|
||||
start = (page - 1) * page_size
|
||||
@@ -387,8 +383,6 @@ def search_products_and_variants(request):
|
||||
sales_units_data.append({
|
||||
'id': su.id,
|
||||
'name': su.name,
|
||||
'unit_code': su.unit.code,
|
||||
'unit_short_name': su.unit.short_name,
|
||||
'price': str(su.price),
|
||||
'sale_price': str(su.sale_price) if su.sale_price else None,
|
||||
'actual_price': str(su.actual_price),
|
||||
@@ -1422,7 +1416,7 @@ def get_product_sales_units_api(request, product_id):
|
||||
warehouse_id = request.GET.get('warehouse', '').strip()
|
||||
|
||||
# Получаем товар с prefetch единиц продажи
|
||||
product = Product.objects.prefetch_related('sales_units__unit').get(
|
||||
product = Product.objects.prefetch_related('sales_units').get(
|
||||
id=product_id, status='active'
|
||||
)
|
||||
|
||||
@@ -1448,8 +1442,6 @@ def get_product_sales_units_api(request, product_id):
|
||||
sales_units_data.append({
|
||||
'id': su.id,
|
||||
'name': su.name,
|
||||
'unit_code': su.unit.code,
|
||||
'unit_short_name': su.unit.short_name,
|
||||
'price': str(su.price),
|
||||
'sale_price': str(su.sale_price) if su.sale_price else None,
|
||||
'actual_price': str(su.actual_price),
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.db.models.functions import Coalesce
|
||||
from itertools import chain
|
||||
|
||||
from ..models import Product, ProductCategory, ProductTag, ProductKit
|
||||
from ..forms import ProductForm
|
||||
from ..forms import ProductForm, ProductSalesUnitFormSet
|
||||
from .utils import handle_photos
|
||||
from ..models import ProductPhoto
|
||||
from user_roles.mixins import ManagerOwnerRequiredMixin
|
||||
@@ -110,11 +110,36 @@ class ProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateVie
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:products-list')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
context['sales_unit_formset'] = ProductSalesUnitFormSet(
|
||||
self.request.POST,
|
||||
prefix='sales_units'
|
||||
)
|
||||
else:
|
||||
context['sales_unit_formset'] = ProductSalesUnitFormSet(prefix='sales_units')
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
from django.db import IntegrityError
|
||||
|
||||
context = self.get_context_data()
|
||||
sales_unit_formset = context['sales_unit_formset']
|
||||
|
||||
try:
|
||||
response = super().form_valid(form)
|
||||
# Сначала сохраняем товар
|
||||
self.object = form.save()
|
||||
|
||||
# Затем сохраняем единицы продажи
|
||||
if sales_unit_formset.is_valid():
|
||||
sales_unit_formset.instance = self.object
|
||||
sales_unit_formset.save()
|
||||
else:
|
||||
# Если formset невалиден, показываем ошибки
|
||||
for error in sales_unit_formset.errors:
|
||||
if error:
|
||||
messages.warning(self.request, f'Ошибка в единицах продажи: {error}')
|
||||
|
||||
# Обработка загрузки фотографий
|
||||
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
|
||||
@@ -127,7 +152,7 @@ class ProductCreateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, CreateVie
|
||||
messages.error(self.request, error)
|
||||
|
||||
messages.success(self.request, f'Товар "{form.instance.name}" успешно создан!')
|
||||
return response
|
||||
return super().form_valid(form)
|
||||
|
||||
except IntegrityError as e:
|
||||
# Обработка ошибки дублирования slug'а или других unique constraints
|
||||
@@ -161,7 +186,7 @@ class ProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailVie
|
||||
'photos',
|
||||
'categories',
|
||||
'tags',
|
||||
'sales_units__unit'
|
||||
'sales_units'
|
||||
).annotate(
|
||||
total_available=total_available,
|
||||
total_reserved=total_reserved,
|
||||
@@ -199,13 +224,40 @@ class ProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateVie
|
||||
# Добавляем фотографии товара в контекст
|
||||
context['product_photos'] = self.object.photos.all().order_by('order', 'created_at')
|
||||
context['photos_count'] = self.object.photos.count()
|
||||
|
||||
# Добавляем formset единиц продажи
|
||||
if self.request.POST:
|
||||
context['sales_unit_formset'] = ProductSalesUnitFormSet(
|
||||
self.request.POST,
|
||||
instance=self.object,
|
||||
prefix='sales_units'
|
||||
)
|
||||
else:
|
||||
context['sales_unit_formset'] = ProductSalesUnitFormSet(
|
||||
instance=self.object,
|
||||
prefix='sales_units'
|
||||
)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
from django.db import IntegrityError
|
||||
|
||||
context = self.get_context_data()
|
||||
sales_unit_formset = context['sales_unit_formset']
|
||||
|
||||
try:
|
||||
response = super().form_valid(form)
|
||||
# Сначала сохраняем товар
|
||||
self.object = form.save()
|
||||
|
||||
# Затем сохраняем единицы продажи
|
||||
if sales_unit_formset.is_valid():
|
||||
sales_unit_formset.instance = self.object
|
||||
sales_unit_formset.save()
|
||||
else:
|
||||
# Если formset невалиден, показываем ошибки
|
||||
for error in sales_unit_formset.errors:
|
||||
if error:
|
||||
messages.warning(self.request, f'Ошибка в единицах продажи: {error}')
|
||||
|
||||
# Обработка загрузки фотографий
|
||||
photo_errors = handle_photos(self.request, self.object, ProductPhoto, 'product')
|
||||
@@ -218,7 +270,7 @@ class ProductUpdateView(LoginRequiredMixin, ManagerOwnerRequiredMixin, UpdateVie
|
||||
messages.error(self.request, error)
|
||||
|
||||
messages.success(self.request, f'Товар "{form.instance.name}" успешно обновлен!')
|
||||
return response
|
||||
return super().form_valid(form)
|
||||
|
||||
except IntegrityError as e:
|
||||
# Обработка ошибки дублирования slug'а или других unique constraints
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.core.paginator import Paginator
|
||||
from django.urls import reverse
|
||||
|
||||
from products.models import UnitOfMeasure, ProductSalesUnit
|
||||
from products.forms import ProductSalesUnitForm
|
||||
from products.forms import ProductSalesUnitForm, UnitOfMeasureForm
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -26,7 +26,8 @@ def unit_of_measure_list(request):
|
||||
|
||||
# Аннотируем количество использований
|
||||
units = units.annotate(
|
||||
usage_count=Count('productsalesunit')
|
||||
usage_count=Count('productsalesunit'),
|
||||
products_count=Count('products')
|
||||
)
|
||||
|
||||
# Применяем фильтры
|
||||
@@ -58,6 +59,89 @@ def unit_of_measure_list(request):
|
||||
return render(request, 'products/uom/unit_list.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def unit_of_measure_create(request):
|
||||
"""
|
||||
Создание новой единицы измерения
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
form = UnitOfMeasureForm(request.POST)
|
||||
if form.is_valid():
|
||||
unit = form.save()
|
||||
messages.success(request, f'Единица измерения "{unit.name}" успешно создана!')
|
||||
return redirect('products:unit-list')
|
||||
else:
|
||||
form = UnitOfMeasureForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'title': 'Создание единицы измерения',
|
||||
'submit_text': 'Создать'
|
||||
}
|
||||
return render(request, 'products/uom/unit_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def unit_of_measure_update(request, pk):
|
||||
"""
|
||||
Редактирование единицы измерения
|
||||
"""
|
||||
unit = get_object_or_404(UnitOfMeasure, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = UnitOfMeasureForm(request.POST, instance=unit)
|
||||
if form.is_valid():
|
||||
unit = form.save()
|
||||
messages.success(request, f'Единица измерения "{unit.name}" успешно обновлена!')
|
||||
return redirect('products:unit-list')
|
||||
else:
|
||||
form = UnitOfMeasureForm(instance=unit)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'unit': unit,
|
||||
'title': f'Редактирование: {unit.name}',
|
||||
'submit_text': 'Сохранить'
|
||||
}
|
||||
return render(request, 'products/uom/unit_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def unit_of_measure_delete(request, pk):
|
||||
"""
|
||||
Удаление единицы измерения
|
||||
"""
|
||||
unit = get_object_or_404(UnitOfMeasure, pk=pk)
|
||||
|
||||
# Проверяем использование
|
||||
products_using = unit.products.count()
|
||||
sales_units_using = unit.productsalesunit_set.count()
|
||||
|
||||
can_delete = products_using == 0 and sales_units_using == 0
|
||||
|
||||
if request.method == 'POST':
|
||||
if can_delete:
|
||||
name = unit.name
|
||||
unit.delete()
|
||||
messages.success(request, f'Единица измерения "{name}" успешно удалена!')
|
||||
return redirect('products:unit-list')
|
||||
else:
|
||||
messages.error(
|
||||
request,
|
||||
f'Невозможно удалить единицу измерения "{unit.name}". '
|
||||
f'Она используется в {products_using} товарах и {sales_units_using} единицах продажи.'
|
||||
)
|
||||
return redirect('products:unit-list')
|
||||
|
||||
context = {
|
||||
'unit': unit,
|
||||
'can_delete': can_delete,
|
||||
'products_using': products_using,
|
||||
'sales_units_using': sales_units_using,
|
||||
}
|
||||
return render(request, 'products/uom/unit_delete.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def product_sales_unit_list(request):
|
||||
"""
|
||||
@@ -65,14 +149,11 @@ 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()
|
||||
sales_units = ProductSalesUnit.objects.select_related('product').all()
|
||||
|
||||
# Применяем фильтры
|
||||
if search_query:
|
||||
@@ -82,9 +163,6 @@ def product_sales_unit_list(request):
|
||||
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'))
|
||||
|
||||
@@ -99,16 +177,11 @@ def product_sales_unit_list(request):
|
||||
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(),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user