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.BooleanField(default=False) suggested_broker_price = 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)