Files
RasadDam_Backend/apps/product/models.py
2025-11-09 14:57:18 +03:30

773 lines
25 KiB
Python

from datetime import datetime
import jdatetime
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db import transaction
from simple_history.models import HistoricalRecords
from apps.authentication.models import OrganizationType, Organization
from apps.authorization.models import UserRelations
from apps.core.models import BaseModel
from apps.herd.models import Rancher
from apps.livestock.models import LiveStockType
from apps.product.services.distribution_child import get_all_distribution_child
class LivestockGroup(models.TextChoices):
ROOSTAEI = "rural", "روستایی" # noqa
SANATI = "industrial", "صنعتی" # noqa
ASHAYERI = "nomadic", "عشایری" # noqa
class LivestockType(models.TextChoices):
LIGHT = "light", "سبک"
HEAVY = "heavy", "سنگین" # noqa
class LivestockSubtype(models.TextChoices):
MILKING = "milking", "شیری" # noqa
FATTENING = "fattening", "پرواری" # noqa
# Create your models here.
class ProductCategory(BaseModel):
""" Category for products """
name = models.CharField(max_length=250, default='empty') # noqa
type_choices = (
('free', 'Free'), # free product
('gov', 'Governmental') # government product
)
type = models.CharField(max_length=5, choices=type_choices, default='empty')
img = models.CharField(max_length=100, default='empty')
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
related_name='parents',
null=True
)
def __str__(self):
return f'name: {self.name} - type: {self.type}'
def save(self, *args, **kwargs):
super(ProductCategory, self).save(*args, **kwargs)
class Product(BaseModel):
""" Child of reference product - like: brown rice """
name = models.CharField(max_length=250, default='empty') # noqa
product_id = models.BigIntegerField(default=0)
type_choices = (
('free', 'FREE'), # free product
('gov', 'GOVERNMENTAL') # government product
)
type = models.CharField(max_length=5, choices=type_choices)
img = models.CharField(max_length=100, default='empty')
category = models.ForeignKey(
ProductCategory,
on_delete=models.CASCADE,
related_name='products',
null=True
)
def generate_product_id(self): # noqa
""" generate id for product from 10 """
last = Product.objects.filter(product_id__gte=10, product_id__lte=1999).order_by('-product_id').first()
if last:
next_code = last.product_id + 1
else:
next_code = 10
return next_code
def quota_information(self):
"""
quotas information of product
"""
# number of quotas
quotas_count = self.quotas.filter(is_closed=False).count()
# total weight of product that assigned in quota
total_quotas_weight = self.quotas.filter(is_closed=False).aggregate(
total=models.Sum('quota_weight')
)['total'] or 0
# total remaining weight of product quotas
total_remaining_quotas_weight = self.quotas.filter(is_closed=False).aggregate(
total=models.Sum('remaining_weight')
)['total'] or 0
total_distributed_weight = QuotaDistribution.objects.filter(
quota__product_id=self.id,
quota__is_closed=False
).aggregate(total_weight=models.Sum('weight'))['total_weight'] or 0
total_sold = QuotaDistribution.objects.filter(
quota__product_id=self.id,
quota__is_closed=False
).aggregate(total_sold=models.Sum('been_sold'))['total_sold'] or 0
total_warehouse_entry = QuotaDistribution.objects.filter(
quota__product_id=self.id,
quota__is_closed=False
).aggregate(total_entry=models.Sum('warehouse_entry'))['total_entry'] or 0
data = {
'product_id': self.id,
'product_name': self.name,
'quotas_count': quotas_count,
'total_quotas_weight': total_quotas_weight,
'total_remaining_quotas_weight': total_remaining_quotas_weight,
'total_distributed_weight': total_distributed_weight,
'total_sold': total_sold,
'total_warehouse_entry': total_warehouse_entry,
}
return data
def __str__(self):
return f'name: {self.name} - type: {self.type}'
def save(self, *args, **kwargs):
if not self.product_id:
self.product_id = self.generate_product_id() # set product id
super(Product, self).save(*args, **kwargs)
class ProductStats(BaseModel):
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='stats',
null=True
)
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='product_stats',
null=True
)
quotas_number = models.PositiveBigIntegerField(default=0)
sale_unit = models.CharField(max_length=25, null=True)
active_quotas_weight = models.PositiveBigIntegerField(default=0)
closed_quotas_weight = models.PositiveBigIntegerField(default=0)
total_quota_weight = models.PositiveBigIntegerField(default=0)
total_quota_remaining = models.PositiveBigIntegerField(default=0)
total_remaining_distribution_weight = models.PositiveBigIntegerField(default=0)
received_distribution_weight = models.PositiveBigIntegerField(default=0)
given_distribution_weight = models.PositiveBigIntegerField(default=0)
received_distribution_number = models.PositiveBigIntegerField(default=0)
given_distribution_number = models.PositiveBigIntegerField(default=0)
total_warehouse_entry = models.PositiveBigIntegerField(default=0)
total_sold = models.PositiveBigIntegerField(default=0)
total_transactions = models.PositiveBigIntegerField(default=0)
def __str__(self):
return f'Product: {self.product.name}-{self.product.id} stats'
def save(self, *args, **kwargs):
return super(ProductStats, self).save(*args, **kwargs)
class Attribute(BaseModel):
"""
every reference product have multiple attributes
"""
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='attributes',
null=True
)
name = models.CharField(max_length=100, default='empty')
type = models.ForeignKey(
'SaleUnit',
on_delete=models.CASCADE,
related_name="attributes",
null=True
)
required = models.BooleanField(default=False)
is_global = models.BooleanField(default=False)
def __str__(self):
return f'{self.product.name} - {self.name}'
def save(self, *args, **kwargs):
return super(Attribute, self).save(*args, **kwargs)
class AttributeValue(BaseModel):
"""
every child product should have attribute value for
reference product attribute
"""
quota = models.ForeignKey(
'Quota',
on_delete=models.CASCADE,
related_name='attribute_values',
null=True
)
attribute = models.ForeignKey(
Attribute,
on_delete=models.CASCADE,
related_name='values',
null=True
)
value = models.IntegerField(default=0)
def __str__(self):
return f'Quota({self.quota.id}) - {self.attribute.name} - {self.value}'
def save(self, *args, **kwargs):
return super(AttributeValue, self).save(*args, **kwargs)
class Broker(BaseModel):
""" Broker for product """
BROKER_TYPES = (
('public', 'PUBLIC'),
('exclusive', 'EXCLUSIVE')
)
name = models.CharField(max_length=255, null=True)
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='product_broker',
null=True
)
organization_type = models.ForeignKey(
OrganizationType,
on_delete=models.CASCADE,
related_name='product_organization',
null=True
)
calculation_strategy = models.ForeignKey(
'SaleUnit',
on_delete=models.CASCADE,
related_name='brokers',
null=True
)
broker_type = models.CharField(choices=BROKER_TYPES, max_length=20, null=True)
fix_broker_price = models.PositiveBigIntegerField(default=0)
fix_broker_price_state = models.PositiveBigIntegerField(default=0)
required = models.BooleanField(default=False)
def __str__(self):
return f'{self.organization_type.name} - {self.product.name}'
def save(self, *args, **kwargs):
return super(Broker, self).save(*args, **kwargs)
class SaleUnit(BaseModel):
""" Units of product for sale """
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='sale_unit',
null=True
)
unit = models.CharField(max_length=250, null=True, unique=True)
required = models.BooleanField(default=False)
def __str__(self):
return f'{self.product.name} - {self.unit}'
def save(self, *args, **kwargs):
return super(SaleUnit, self).save(*args, **kwargs)
class IncentivePlan(BaseModel):
""" incentive plan for every quota """
PLAN_TYPE_CHOICES = (
('ILQ', 'increasing livestock quotas'),
('SM', 'statistical/monitoring')
)
GROUP_CHOICES = (
('industrial', 'Industrial'),
('rural', 'Rural'),
('nomadic', 'Nomadic')
)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
registering_organization = models.ForeignKey(
UserRelations,
on_delete=models.CASCADE,
related_name='incentive_plans',
null=True
)
plan_type = models.CharField(choices=PLAN_TYPE_CHOICES, max_length=5, null=True)
group = models.CharField(choices=GROUP_CHOICES, max_length=15, null=True)
is_time_unlimited = models.BooleanField(default=False)
start_date_limit = models.DateField(null=True, blank=True)
end_date_limit = models.DateField(null=True, blank=True)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
return super(IncentivePlan, self).save(*args, **kwargs)
class IncentivePlanRancher(BaseModel):
plan = models.ForeignKey(
IncentivePlan,
on_delete=models.CASCADE,
related_name='rancher_plans',
null=True
)
rancher = models.ForeignKey(
Rancher,
on_delete=models.CASCADE,
related_name='plans',
null=True
)
livestock_type = models.ForeignKey(
LiveStockType,
on_delete=models.CASCADE,
related_name='rancher_plans',
null=True
)
allowed_quantity = models.PositiveBigIntegerField(default=0)
used_quantity = models.PositiveBigIntegerField(default=0)
def __str__(self):
return f'{self.plan.name}-{self.rancher.first_name}-{self.livestock_type.name}'
def save(self, *args, **kwargs):
return super(IncentivePlanRancher, self).save(*args, **kwargs)
class Quota(BaseModel):
""" quota for product with some conditions """
registerer_organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='quotas',
null=True
)
assigned_organizations = models.ManyToManyField(
Organization,
related_name='assigned_quotas',
blank=True
)
quota_id = models.PositiveBigIntegerField(null=True, blank=True)
quota_code = models.CharField(max_length=15, null=True)
quota_weight = models.PositiveIntegerField(default=0)
remaining_weight = models.PositiveBigIntegerField(default=0)
quota_distributed = models.PositiveIntegerField(default=0)
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='quotas',
null=True
)
sale_type = models.CharField(max_length=50, choices=[("free", "آزاد"), ("gov", "دولتی")]) # noqa
sale_unit = models.ForeignKey(
SaleUnit,
on_delete=models.CASCADE,
related_name='quotas',
null=True
)
month_choices = ArrayField(base_field=models.IntegerField(), null=True)
sale_license = ArrayField(base_field=models.IntegerField(), null=True)
group = ArrayField(base_field=models.CharField(
max_length=50,
choices=[("rural", "روستایی"), ("industrial", "صنعتی"), ("nomadic", "عشایری")], # noqa
), null=True)
has_distribution_limit = models.BooleanField(default=False)
distribution_mode = ArrayField(base_field=models.IntegerField(), blank=True, null=True)
has_organization_limit = models.BooleanField(default=False)
limit_by_organizations = models.ManyToManyField(
Organization,
related_name='quota_limits',
blank=True
)
base_price_factory = models.PositiveBigIntegerField(default=0)
base_price_cooperative = models.PositiveBigIntegerField(default=0)
final_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
limit_by_herd_size = models.BooleanField(default=True)
is_closed = models.BooleanField(default=False)
closed_at = models.DateTimeField(null=True, blank=True)
pos_sale_type_choices = (
('weight', 'WEIGHT'),
('count', 'COUNT'),
('all', 'ALL'),
)
pos_sale_type = models.CharField(choices=pos_sale_type_choices, max_length=25, default='all')
def __str__(self):
return f"Quota ({self.id}) for {self.product.name}"
def generate_quota_id(self): # noqa
""" generate id for quota from 1001 """
with transaction.atomic():
last = Quota.objects.filter(
quota_id__gte=10001,
quota_id__lte=19999
).select_for_update().order_by('-quota_id').first()
if last:
next_code = last.quota_id + 1
else:
next_code = 10001
return next_code
def calculate_final_price(self):
""" calculate final price of quota """
factor_total = sum([
f.value for f in self.attribute_values.all()
])
broker_total = sum([
b.value for b in self.broker_values.all()
])
coop = self.base_price_cooperative or 0
factory = self.base_price_factory or 0
return factor_total + broker_total + coop + factory
@property
def remaining_quota_weight(self):
""" calculate remaining quota weight after distribution """
distributed_weight = self.distributions_assigned.aggregate(total=models.Sum("weight"))["total"] or 0
return self.quota_weight - distributed_weight
def is_in_valid_time(self):
""" check if quota allowed time for distribute, sale and... is expired """
now = datetime.now()
persian_date = jdatetime.datetime.fromgregorian(datetime=now)
if self.has_distribution_limit:
return persian_date.month in self.distribution_mode
return True
def is_in_sale_licence_time(self):
""" check if quota allowed time for sale and... is expired """
now = datetime.now()
persian_date = jdatetime.datetime.fromgregorian(datetime=now)
return persian_date.month in self.sale_license
def soft_delete(self):
self.trash = True
self.save(update_fields=['trash'])
for dist in self.distributions_assigned.all():
dist.soft_delete()
def save(self, calculate_final_price=None, *args, **kwargs):
if not self.quota_id:
self.quota_id = self.generate_quota_id()
if calculate_final_price:
if not self.final_price:
self.final_price = self.calculate_final_price()
return super(Quota, self).save(*args, **kwargs)
class QuotaFinalPriceTypes(BaseModel):
name = models.CharField(max_length=250)
en_name = models.CharField(max_length=250, null=True)
def __str__(self):
return f'{self.name}'
def save(self, *args, **kwargs):
return super(QuotaFinalPriceTypes, self).save(*args, **kwargs)
class QuotaPriceCalculationItems(BaseModel):
quota = models.ForeignKey(
Quota,
on_delete=models.CASCADE,
related_name='pricing_items',
null=True
)
pricing_type = models.ForeignKey(
'QuotaFinalPriceTypes',
on_delete=models.CASCADE,
related_name='pricing_items',
null=True
)
name = models.CharField(max_length=250)
value = models.PositiveBigIntegerField(default=0)
def __str__(self):
return f'{self.quota.quota_id}-{self.pricing_type.name}-{self.name}'
def save(self, *args, **kwargs):
return super(QuotaPriceCalculationItems, self).save(*args, **kwargs)
class QuotaStats(BaseModel):
quota = models.OneToOneField(
Quota,
on_delete=models.CASCADE,
related_name='stats',
null=True,
)
total_distributed = models.PositiveBigIntegerField(default=0)
remaining = models.PositiveBigIntegerField(default=0)
total_inventory = models.PositiveBigIntegerField(default=0)
total_sale = models.PositiveBigIntegerField(default=0)
def __str__(self):
return f'Quota: {self.quota.quota_id} stats'
def save(self, *args, **kwargs):
return super(QuotaStats, self).save(*args, **kwargs)
class QuotaUsage(BaseModel):
distribution = models.ForeignKey(
"QuotaDistribution",
on_delete=models.CASCADE,
related_name='usages',
null=True
)
rancher = models.ForeignKey(
Rancher,
on_delete=models.CASCADE,
related_name='usages',
null=True
)
livestock_type = models.ForeignKey(
LiveStockType,
on_delete=models.CASCADE,
related_name='usages',
null=True
)
incentive_plan = models.ForeignKey(
IncentivePlan,
null=True,
blank=True,
on_delete=models.SET_NULL
)
count = models.PositiveIntegerField()
base_quota_used = models.IntegerField(default=0)
incentive_quota_used = models.IntegerField(default=0)
usage_type_choices = (
('base', 'BASE'),
('incentive', 'INCENTIVE'),
)
usage_type = models.CharField(max_length=150, choices=usage_type_choices, null=True)
def __str__(self):
return f'rancher: {self.rancher.ranching_farm} - plan: {self.incentive_plan.name}'
def save(self, *args, **kwargs):
return super(QuotaUsage, self).save(*args, **kwargs)
class QuotaIncentiveAssignment(BaseModel):
""" assign incentive plan to quota """
quota = models.ForeignKey(
Quota,
on_delete=models.CASCADE,
related_name="incentive_assignments",
null=True
)
incentive_plan = models.ForeignKey(
IncentivePlan,
on_delete=models.CASCADE,
related_name='quota_assignment',
null=True
)
heavy_value = models.PositiveBigIntegerField(default=0)
light_value = models.PositiveBigIntegerField(default=0)
livestock_type = models.ForeignKey(
LiveStockType,
on_delete=models.CASCADE,
related_name='incentive_plans',
null=True
)
quantity_kg = models.PositiveBigIntegerField(default=0)
def calculate_heavy_value(self):
""" calculate total livestock heavy value of incentive plans """
heavy_weight = QuotaIncentiveAssignment.objects.filter(
quota=self.quota, livestock_type__weight_type='H'
).aggregate(total=models.Sum('quantity_kg'))['total'] or 0
self.heavy_value = heavy_weight
self.save()
def calculate_light_value(self):
""" calculate total livestock light value of incentive plans """
heavy_weight = QuotaIncentiveAssignment.objects.filter(
quota=self.quota, livestock_type__weight_type='L'
).aggregate(total=models.Sum('quantity_kg'))['total'] or 0
self.heavy_value = heavy_weight
self.save()
def __str__(self):
return f"Quota ({self.quota.id}) for {self.incentive_plan.name}"
def save(self, *args, **kwargs):
return super(QuotaIncentiveAssignment, self).save(*args, **kwargs)
class QuotaBrokerValue(BaseModel):
""" broker attributes value for quota """
quota = models.ForeignKey(
Quota,
on_delete=models.CASCADE,
related_name="broker_values",
null=True
)
broker = models.ForeignKey(
Broker,
on_delete=models.CASCADE,
related_name='values'
)
value = models.PositiveBigIntegerField(default=0)
def __str__(self):
return f"Quota ({self.quota.id}) for Broker({self.broker.organization_type.name})"
def save(self, *args, **kwargs):
return super(QuotaBrokerValue, self).save(*args, **kwargs)
class QuotaLivestockAllocation(BaseModel):
""" livestock allocation to quota """
quota = models.ForeignKey(
"Quota",
on_delete=models.CASCADE,
related_name="livestock_allocations",
null=True
)
livestock_group = models.CharField(max_length=20, choices=LivestockGroup.choices, null=True)
livestock_type = models.ForeignKey(
LiveStockType,
on_delete=models.CASCADE,
related_name='allocations',
null=True
)
livestock_subtype = models.CharField(max_length=20, choices=LivestockSubtype.choices, null=True)
quantity_kg = models.PositiveBigIntegerField(default=0)
"""
@using for set unique values between fields
class Meta:
unique_together = ('quota', 'livestock_group', 'livestock_type', 'livestock_subtype')
"""
def __str__(self):
return f"{self.livestock_group} - {self.livestock_type}/{self.livestock_subtype}: {self.quantity_kg}kg"
def save(self, *args, **kwargs):
return super(QuotaLivestockAllocation, self).save(*args, **kwargs)
class QuotaLiveStockAgeLimitation(BaseModel):
quota = models.ForeignKey(
Quota,
on_delete=models.CASCADE,
related_name='livestock_age_limitations',
null=True
)
livestock_type = models.ForeignKey(
LiveStockType,
on_delete=models.CASCADE,
related_name='quota_limitations',
null=True
)
livestock_subtype = models.CharField(max_length=20, choices=LivestockSubtype.choices, null=True)
age_month = models.PositiveIntegerField(default=0)
def __str__(self):
return f"{self.livestock_type}/{self.livestock_subtype}: {self.age_month} month"
def save(self, *args, **kwargs):
return super(QuotaLiveStockAgeLimitation, self).save(*args, **kwargs)
class QuotaDistribution(BaseModel):
parent_distribution = models.ForeignKey(
'self',
on_delete=models.CASCADE,
related_name='children',
null=True
)
assigner_organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='distributions_assigner',
null=True
)
assigned_organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='distributions',
null=True
)
description = models.TextField(max_length=1000, null=True, blank=True)
distribution_id = models.CharField(max_length=20, null=True)
quota = models.ForeignKey(
Quota,
on_delete=models.CASCADE,
related_name='distributions_assigned',
null=True
)
weight = models.PositiveBigIntegerField(default=0)
remaining_weight = models.PositiveBigIntegerField(default=0)
distributed = models.PositiveBigIntegerField(default=0)
warehouse_entry = models.PositiveBigIntegerField(default=0)
warehouse_balance = models.PositiveBigIntegerField(default=0)
been_sold = models.PositiveBigIntegerField(default=0)
history = HistoricalRecords()
pre_sale = models.BooleanField(default=False)
pre_sale_balance = models.PositiveBigIntegerField(default=0)
free_sale = models.BooleanField(default=False)
free_sale_balance = models.PositiveBigIntegerField(default=0)
def generate_distribution_id(self):
""" generate special id for quota distribution """
year = jdatetime.datetime.now().year
month = jdatetime.datetime.now().month
day = jdatetime.datetime.now().day
product_id = self.quota.product.product_id
quota_id = self.quota.quota_id
base_code = f"{str(year)[3]}{month}{day}{product_id}{quota_id}"
similar_codes = QuotaDistribution.objects.filter(distribution_id__startswith=base_code).count()
counter = str(similar_codes + 1).zfill(4)
return f"{base_code}{counter}"
def __str__(self):
return f"{self.distribution_id}-"
def soft_delete(self):
self.trash = True
self.save(update_fields=['trash'])
childs = get_all_distribution_child(self) # noqa
for child in childs:
child.soft_delete()
for entry in self.inventory_entry.all():
entry.soft_delete()
def save(self, *args, **kwargs):
if not self.distribution_id:
self.distribution_id = self.generate_distribution_id()
return super(QuotaDistribution, self).save(*args, **kwargs)