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 Organization from django.contrib.postgres.fields import ArrayField 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 return {'quotas_count': quotas_count, 'total_quotas_weight': total_quotas_weight} 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 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_choices = ( ('K', 'Per Kilo'), ('', ''), ) type = models.CharField( max_length=10, choices=type_choices, default='empty', help_text='type of attribute like: calculate product by kilogram' ) 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 """ CALCULATION_CHOICES = ( ('K', 'Per Kilo'), ('', ''), ) 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 = models.ForeignKey( Organization, on_delete=models.CASCADE, related_name='product_organization', null=True ) calculation_strategy = models.CharField( max_length=3, choices=CALCULATION_CHOICES, default='empty' ) broker_type = models.CharField(choices=BROKER_TYPES, max_length=20, null=True) required = models.BooleanField(default=False) def __str__(self): return f'{self.organization.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_choices = ( ('10P', '10KG Package'), ('50P', '50KG Package'), ('', ''), ) unit = models.CharField(max_length=10, choices=unit_choices, null=True) variation_coefficient = models.IntegerField(default=0) required = models.BooleanField(default=False) def __str__(self): return f'{self.product.name} - {self.unit} - {self.variation_coefficient}' 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) class Meta: unique_together = ('name', 'registering_organization') 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 month_choices = ArrayField(base_field=models.IntegerField(), null=True) group = models.CharField( max_length=50, choices=[("rural", "روستایی"), ("industrial", "صنعتی"), ("nomadic", "عشایری")] # noqa ) has_distribution_limit = models.BooleanField(default=False) distribution_mode = ArrayField(base_field=models.IntegerField(), blank=True, null=True) base_price_factory = models.DecimalField(max_digits=12, decimal_places=2) base_price_cooperative = models.DecimalField(max_digits=12, decimal_places=2) 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.month_choices 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 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.DecimalField(max_digits=12, decimal_places=2) light_value = models.DecimalField(max_digits=12, decimal_places=2) 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.DecimalField(max_digits=12, decimal_places=2) def __str__(self): return f"Quota ({self.quota.id}) for Broker({self.broker.organization.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.CharField(max_length=20, choices=LivestockType.choices, null=True) livestock_subtype = models.CharField(max_length=20, choices=LivestockSubtype.choices, null=True) quantity_kg = models.DecimalField(max_digits=12, decimal_places=2, null=True) """ @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.CharField(max_length=20, choices=LivestockType.choices, 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): 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) warehouse_entry = models.PositiveBigIntegerField(default=0) warehouse_balance = models.PositiveBigIntegerField(default=0) been_sold = models.PositiveBigIntegerField(default=0) history = HistoricalRecords() 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)