Files
RasadDam_Backend/apps/product/models.py

590 lines
19 KiB
Python

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 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': self.id,
'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)
active_quotas_weight = models.PositiveBigIntegerField(default=0)
closed_quotas_weight = models.PositiveBigIntegerField(default=0)
total_quota_weight = models.PositiveBigIntegerField(default=0)
total_remaining = models.PositiveBigIntegerField(default=0)
total_distributed_weight = 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 = models.ForeignKey(
Organization,
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.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 = 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)
has_organization_limit = models.BooleanField(default=False)
limit_by_organizations = models.ManyToManyField(
Organization,
related_name='quota_limits',
blank=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 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)
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.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.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.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()
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)