feat(products): add support for product sales units
Add new models UnitOfMeasure and ProductSalesUnit to enable selling products in different units (e.g., bunches, kg). Update Product model with base_unit field and methods for unit conversions and availability. Extend Sale, Reservation, and OrderItem models with sales_unit fields and snapshots. Modify SaleProcessor to handle quantity conversions. Include admin interfaces for managing units. Add corresponding database migrations.
This commit is contained in:
@@ -12,6 +12,7 @@ from .models import ProductCategory, ProductTag, Product, ProductKit, KitItem
|
||||
from .models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
|
||||
from .models import ProductVariantGroup, KitItemPriority, SKUCounter, CostPriceHistory
|
||||
from .models import ConfigurableProduct, ConfigurableProductOption, ConfigurableProductAttribute
|
||||
from .models import UnitOfMeasure, ProductSalesUnit
|
||||
from .admin_displays import (
|
||||
format_quality_badge,
|
||||
format_quality_display,
|
||||
@@ -395,7 +396,7 @@ class ProductAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
search_fields = ('name', 'sku', 'description', 'search_keywords')
|
||||
filter_horizontal = ('categories', 'tags', 'variant_groups')
|
||||
readonly_fields = ('photo_preview_large', 'archived_at', 'archived_by', 'cost_price', 'cost_price_details_display')
|
||||
autocomplete_fields = []
|
||||
autocomplete_fields = ['base_unit']
|
||||
actions = [
|
||||
restore_items,
|
||||
delete_selected,
|
||||
@@ -407,7 +408,7 @@ class ProductAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
|
||||
fieldsets = (
|
||||
('Основная информация', {
|
||||
'fields': ('name', 'sku', 'variant_suffix', 'description', 'short_description', 'categories', 'unit', 'price', 'sale_price')
|
||||
'fields': ('name', 'sku', 'variant_suffix', 'description', 'short_description', 'categories', 'base_unit', 'unit', 'price', 'sale_price')
|
||||
}),
|
||||
('Себестоимость', {
|
||||
'fields': ('cost_price_details_display',),
|
||||
@@ -832,7 +833,7 @@ class ProductKitAdminWithItems(ProductKitAdmin):
|
||||
|
||||
# Update admin classes to include photo inlines
|
||||
class ProductAdminWithPhotos(ProductAdmin):
|
||||
inlines = [ProductPhotoInline]
|
||||
inlines = [ProductPhotoInline, ProductSalesUnitInline]
|
||||
|
||||
class ProductKitAdminWithItemsAndPhotos(nested_admin.NestedModelAdmin, ProductKitAdmin):
|
||||
inlines = [KitItemInline, ProductKitPhotoInline]
|
||||
@@ -943,6 +944,88 @@ class CostPriceHistoryAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
return False
|
||||
|
||||
|
||||
# === Админка для единиц измерения ===
|
||||
|
||||
@admin.register(UnitOfMeasure)
|
||||
class UnitOfMeasureAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""Админка для справочника единиц измерения"""
|
||||
list_display = ('code', 'name', 'short_name', 'position', 'is_active')
|
||||
list_filter = ('is_active',)
|
||||
search_fields = ('code', 'name', 'short_name')
|
||||
list_editable = ('position', 'is_active')
|
||||
ordering = ('position', 'code')
|
||||
|
||||
fieldsets = (
|
||||
('Основная информация', {
|
||||
'fields': ('code', 'name', 'short_name')
|
||||
}),
|
||||
('Настройки', {
|
||||
'fields': ('position', 'is_active')
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
class ProductSalesUnitInline(admin.TabularInline):
|
||||
"""Инлайн для единиц продажи товара"""
|
||||
model = ProductSalesUnit
|
||||
extra = 0
|
||||
fields = (
|
||||
'unit', 'name', 'conversion_factor', 'price', 'sale_price',
|
||||
'min_quantity', 'quantity_step', 'is_default', 'is_active'
|
||||
)
|
||||
autocomplete_fields = ['unit']
|
||||
verbose_name = "Единица продажи"
|
||||
verbose_name_plural = "Единицы продажи"
|
||||
|
||||
|
||||
@admin.register(ProductSalesUnit)
|
||||
class ProductSalesUnitAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||||
"""Админка для единиц продажи товаров"""
|
||||
list_display = (
|
||||
'product', 'name', 'unit', 'conversion_factor',
|
||||
'get_price_display', 'min_quantity', 'is_default', 'is_active'
|
||||
)
|
||||
list_filter = ('is_active', 'is_default', 'unit')
|
||||
search_fields = ('product__name', 'product__sku', 'name')
|
||||
autocomplete_fields = ['product', 'unit']
|
||||
list_editable = ('is_default', 'is_active')
|
||||
ordering = ('product', 'position')
|
||||
|
||||
fieldsets = (
|
||||
('Товар и единица', {
|
||||
'fields': ('product', 'unit', 'name')
|
||||
}),
|
||||
('Конверсия', {
|
||||
'fields': ('conversion_factor',),
|
||||
'description': 'Сколько единиц продажи получается из 1 базовой единицы товара. '
|
||||
'Например: 1 банч = 15 веток → conversion_factor = 15'
|
||||
}),
|
||||
('Ценообразование', {
|
||||
'fields': ('price', 'sale_price'),
|
||||
'description': 'Цена за единицу продажи. sale_price - цена со скидкой (опционально).'
|
||||
}),
|
||||
('Ограничения', {
|
||||
'fields': ('min_quantity', 'quantity_step'),
|
||||
'description': 'min_quantity - минимальное количество для заказа. '
|
||||
'quantity_step - шаг изменения количества.'
|
||||
}),
|
||||
('Настройки', {
|
||||
'fields': ('position', 'is_default', 'is_active')
|
||||
}),
|
||||
)
|
||||
|
||||
def get_price_display(self, obj):
|
||||
"""Отображение цены с учетом скидки"""
|
||||
if obj.sale_price:
|
||||
return format_html(
|
||||
'<span style="text-decoration: line-through; color: #999;">{}</span> '
|
||||
'<span style="color: #dc3545; font-weight: bold;">{}</span>',
|
||||
obj.price, obj.sale_price
|
||||
)
|
||||
return obj.price
|
||||
get_price_display.short_description = 'Цена'
|
||||
|
||||
|
||||
admin.site.register(ProductCategory, ProductCategoryAdminWithPhotos)
|
||||
admin.site.register(ProductTag, ProductTagAdmin)
|
||||
admin.site.register(Product, ProductAdminWithPhotos)
|
||||
|
||||
Reference in New Issue
Block a user