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:
@@ -1,8 +1,9 @@
|
||||
"""
|
||||
Модель Product - базовый товар (цветок, упаковка, аксессуар).
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, Sum
|
||||
|
||||
from .base import BaseProductEntity
|
||||
from .categories import ProductCategory, ProductTag
|
||||
@@ -56,7 +57,19 @@ class Product(BaseProductEntity):
|
||||
max_length=10,
|
||||
choices=UNIT_CHOICES,
|
||||
default='шт',
|
||||
verbose_name="Единица измерения"
|
||||
verbose_name="Единица измерения (deprecated)"
|
||||
)
|
||||
|
||||
# Новое поле: ссылка на справочник единиц измерения
|
||||
base_unit = models.ForeignKey(
|
||||
'UnitOfMeasure',
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='products',
|
||||
verbose_name="Базовая единица",
|
||||
help_text="Единица хранения и закупки (банч, кг, шт). "
|
||||
"Если указана, используется вместо поля 'unit'."
|
||||
)
|
||||
|
||||
# ЦЕНООБРАЗОВАНИЕ - переименованные поля
|
||||
@@ -153,6 +166,99 @@ class Product(BaseProductEntity):
|
||||
variant_groups__in=self.variant_groups.all()
|
||||
).exclude(id=self.id).distinct()
|
||||
|
||||
# === МЕТОДЫ ДЛЯ РАБОТЫ С ЕДИНИЦАМИ ПРОДАЖИ ===
|
||||
|
||||
@property
|
||||
def unit_display(self):
|
||||
"""
|
||||
Отображаемое название единицы измерения.
|
||||
Приоритет: base_unit.code > unit
|
||||
"""
|
||||
if self.base_unit:
|
||||
return self.base_unit.code
|
||||
return self.unit
|
||||
|
||||
@property
|
||||
def has_sales_units(self):
|
||||
"""Есть ли у товара настроенные единицы продажи"""
|
||||
return self.sales_units.filter(is_active=True).exists()
|
||||
|
||||
def get_default_sales_unit(self):
|
||||
"""
|
||||
Получить единицу продажи по умолчанию.
|
||||
Возвращает первую единицу с is_default=True или первую активную.
|
||||
"""
|
||||
default = self.sales_units.filter(is_active=True, is_default=True).first()
|
||||
if default:
|
||||
return default
|
||||
return self.sales_units.filter(is_active=True).first()
|
||||
|
||||
def get_total_available(self, warehouse=None):
|
||||
"""
|
||||
Получить общий доступный остаток в базовых единицах.
|
||||
|
||||
Args:
|
||||
warehouse: (опционально) конкретный склад
|
||||
|
||||
Returns:
|
||||
Decimal: количество в базовых единицах
|
||||
"""
|
||||
from inventory.models import Stock
|
||||
|
||||
qs = Stock.objects.filter(product=self)
|
||||
if warehouse:
|
||||
qs = qs.filter(warehouse=warehouse)
|
||||
|
||||
result = qs.aggregate(
|
||||
total=Sum('quantity_available')
|
||||
)['total']
|
||||
|
||||
return result or Decimal('0')
|
||||
|
||||
def get_available_in_unit(self, sales_unit, warehouse=None):
|
||||
"""
|
||||
Получить остаток в указанной единице продажи.
|
||||
|
||||
Args:
|
||||
sales_unit: объект ProductSalesUnit
|
||||
warehouse: (опционально) конкретный склад
|
||||
|
||||
Returns:
|
||||
Decimal: количество в единицах продажи
|
||||
"""
|
||||
base_qty = self.get_total_available(warehouse)
|
||||
return sales_unit.convert_from_base(base_qty)
|
||||
|
||||
def get_all_units_availability(self, warehouse=None):
|
||||
"""
|
||||
Получить остатки во всех единицах продажи.
|
||||
|
||||
Args:
|
||||
warehouse: (опционально) конкретный склад
|
||||
|
||||
Returns:
|
||||
list[dict]: список с информацией по каждой единице продажи
|
||||
[
|
||||
{
|
||||
'sales_unit': ProductSalesUnit,
|
||||
'available': Decimal,
|
||||
'price': Decimal
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
base_qty = self.get_total_available(warehouse)
|
||||
result = []
|
||||
|
||||
for su in self.sales_units.filter(is_active=True).select_related('unit'):
|
||||
result.append({
|
||||
'sales_unit': su,
|
||||
'available': su.convert_from_base(base_qty),
|
||||
'price': su.actual_price
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class CostPriceHistory(models.Model):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user