import datetime from simple_history.models import HistoricalRecords from django.db import models from apps.core.models import BaseModel from apps.authorization.models import UserRelations from apps.authentication.models import OrganizationType, Organization from django.contrib.postgres.fields import ArrayField from apps.livestock.models import LiveStockType from datetime import datetime import jdatetime 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) 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 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) is_closed = models.BooleanField(default=False) closed_at = models.DateTimeField(null=True, blank=True) def __str__(self): return f"Quota ({self.id}) for {self.product.name}" def generate_quota_id(self): # noqa """ generate id for quota from 1001 """ last = Quota.objects.filter(quota_id__gte=1001, quota_id__lte=1999).order_by('-quota_id').first() if last: next_code = last.quota_id + 1 else: next_code = 1001 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, etc is expired """ now = datetime.now() persian_date = jdatetime.datetime.fromgregorian(datetime=now) return persian_date.month in self.sale_license 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 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 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) 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) free_sale = models.BooleanField(default=False) 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 save(self, *args, **kwargs): if not self.distribution_id: self.distribution_id = self.generate_distribution_id() return super(QuotaDistribution, self).save(*args, **kwargs)