quota, distribution, inventory entry, quota sale transaction, product informations, signals ,....
This commit is contained in:
@@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
import os.path
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
import sentry_sdk
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -27,6 +28,22 @@ SECRET_KEY = 'django-insecure-@0apn-lk85pfw=z00x2ib$w9#rwz8%2v4i_n^^9jz-m9b+y55*
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn="https://e8d8ff4f1bf729370af00b7775be441c@o4509597964697600.ingest.us.sentry.io/4509597966073856",
|
||||
# Add data like request headers and IP for users,
|
||||
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
|
||||
send_default_pii=True,
|
||||
# Set traces_sample_rate to 1.0 to capture 100%
|
||||
# of transactions for tracing.
|
||||
traces_sample_rate=1.0,
|
||||
# Set profile_session_sample_rate to 1.0 to profile 100%
|
||||
# of profile sessions.
|
||||
profile_session_sample_rate=1.0,
|
||||
# Set profile_lifecycle to "trace" to automatically
|
||||
# run the profiler on when there is an active transaction
|
||||
profile_lifecycle="trace",
|
||||
)
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
@@ -131,16 +148,18 @@ AUTH_USER_MODEL = 'authentication.User'
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
'SECURITY_DEFINITIONS': {
|
||||
'Bearer': {
|
||||
'type': 'apiKey',
|
||||
'name': 'Authorization',
|
||||
'in': 'header'
|
||||
"bearer": {
|
||||
"type": "apiKey",
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": 'JWT Authorization header using the Bearer scheme. Example: "Authorization: Bearer <token>"',
|
||||
},
|
||||
'basic': { # <<-- is for djagno authentication
|
||||
'type': 'basic'
|
||||
},
|
||||
},
|
||||
'USE_SESSION_AUTH': True,
|
||||
'USE_SESSION_AUTH': False,
|
||||
"DEFAULT_AUTO_SCHEMA_CLASS": "drf_yasg.inspectors.SwaggerAutoSchema"
|
||||
}
|
||||
|
||||
LOGIN_URL = 'rest_framework:login'
|
||||
@@ -159,7 +178,7 @@ REST_FRAMEWORK = {
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
'rest_framework.filters.SearchFilter',
|
||||
],
|
||||
'EXCEPTION_HANDLER': 'apps.core.error_handler.custom_exception_handler',
|
||||
# 'EXCEPTION_HANDLER': 'apps.core.error_handler.custom_exception_handler',
|
||||
"DEFAULT_PAGINATION_CLASS": 'apps.core.pagination.CustomPageNumberPagination',
|
||||
"PAGE_SIZE": 20,
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema'
|
||||
|
||||
@@ -38,5 +38,6 @@ urlpatterns = [
|
||||
path('tag/', include('apps.tag.urls')),
|
||||
path('search/', include('apps.search.urls')),
|
||||
path('product/', include('apps.product.urls')),
|
||||
path('warehouse/', include('apps.warehouse.urls')),
|
||||
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
]
|
||||
|
||||
@@ -98,9 +98,9 @@ class UserViewSet(ModelViewSet):
|
||||
Customizing update user & bank account info with
|
||||
permission levels
|
||||
"""
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
serializer = self.serializer_class(data=request.data, instance=self.get_object(), partial=True)
|
||||
if serializer.is_valid():
|
||||
user = serializer.update(self.queryset.get(id=pk), validated_data=request.data)
|
||||
user = serializer.save()
|
||||
|
||||
if 'organization' in request.data.keys(): # noqa
|
||||
organization = CustomOperations().custom_update( # update organization for user
|
||||
|
||||
@@ -138,8 +138,8 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
instance.ownership = validated_data.get('ownership')
|
||||
instance.address = validated_data.get('address')
|
||||
instance.photo = validated_data.get('photo')
|
||||
instance.province = Province.objects.get(id=validated_data.get('province'))
|
||||
instance.city = City.objects.get(id=validated_data.get('city'))
|
||||
instance.province = validated_data.get('province', instance.province)
|
||||
instance.city = validated_data.get('province', instance.province)
|
||||
instance.otp_status = validated_data.get('otp_status')
|
||||
instance.save()
|
||||
|
||||
@@ -213,14 +213,10 @@ class OrganizationSerializer(serializers.ModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
""" update user organization information """ # noqa
|
||||
instance.name = validated_data.get('name', instance.name)
|
||||
if validated_data.get('type'):
|
||||
instance.type = validated_data['type']
|
||||
if validated_data.get('province'):
|
||||
instance.province = validated_data['province']
|
||||
if validated_data.get('city'):
|
||||
instance.city = validated_data['city']
|
||||
if validated_data.get('parent_organization'):
|
||||
instance.parent_organization = validated_data['parent_organization']
|
||||
instance.type = validated_data.get('type', instance.type)
|
||||
instance.province = validated_data.get('province', instance.province)
|
||||
instance.city = validated_data.get('city', instance.city)
|
||||
instance.parent_organization = validated_data.get('parent_organization', instance.parent_organization)
|
||||
instance.national_unique_id = validated_data.get('national_unique_id', instance.national_unique_id)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0 on 2025-06-28 11:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0022_alter_user_mobile_alter_user_national_code'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='organization',
|
||||
name='company_code',
|
||||
field=models.CharField(default='', max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='organization',
|
||||
name='field_of_activity',
|
||||
field=models.CharField(choices=[('CO', 'Country'), ('PR', 'Province'), ('CI', 'City')], default='', max_length=2),
|
||||
),
|
||||
]
|
||||
@@ -105,8 +105,8 @@ class Organization(BaseModel):
|
||||
('PR', 'Province'),
|
||||
('CI', 'City')
|
||||
)
|
||||
field_of_activity = models.CharField(max_length=2, choices=activity_fields, default='EM')
|
||||
company_code = models.CharField(max_length=30, default="empty")
|
||||
field_of_activity = models.CharField(max_length=2, choices=activity_fields, default="")
|
||||
company_code = models.CharField(max_length=30, default="")
|
||||
province = models.ForeignKey(
|
||||
Province,
|
||||
on_delete=models.CASCADE,
|
||||
|
||||
@@ -146,10 +146,8 @@ class UserRelationSerializer(serializers.ModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
""" update user relation object """
|
||||
|
||||
if validated_data.get('role'):
|
||||
instance.role = Role.objects.get(id=validated_data.get("role"))
|
||||
if validated_data.get('organization'):
|
||||
instance.organization = Organization.objects.get(id=validated_data.get('organization'))
|
||||
instance.role = validated_data.get('role', instance.role)
|
||||
instance.organization = validated_data.get('organization', instance.organization)
|
||||
instance.save()
|
||||
instance.permissions.clear()
|
||||
instance.permissions.add(*(validated_data.get('permissions', instance.permissions)))
|
||||
|
||||
@@ -32,8 +32,8 @@ class BaseModel(models.Model):
|
||||
self.modified_by = user
|
||||
if not self.creator_info:
|
||||
self.created_by = user
|
||||
self.creator_info = user.first_name + ' ' + user.last_name + '-' + user.national_code
|
||||
self.modifier_info = user.first_name + ' ' + user.last_name + '-' + user.national_code
|
||||
self.creator_info = user.first_name + ' ' + user.last_name
|
||||
self.modifier_info = user.first_name + ' ' + user.last_name
|
||||
super(BaseModel, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ schema_view = get_schema_view(
|
||||
license=openapi.License(name="BSD License"),
|
||||
),
|
||||
public=True,
|
||||
permission_classes=[permissions.BasePermission, permissions.IsAuthenticated]
|
||||
# permission_classes=[permissions.BasePermission, permissions.IsAuthenticated]
|
||||
)
|
||||
|
||||
@@ -8,3 +8,19 @@ class QuotaWeightException(APIException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_detail = "مقدار وارد شده باعث میشود مجموع سهمیهها از مقدار کل سهمیه بیشتر شود." # noqa
|
||||
default_code = 'error'
|
||||
|
||||
|
||||
class QuotaClosedException(APIException):
|
||||
""" if quota is closed, operations can not be done """
|
||||
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_detail = "این سهمیه بسته شده است و قابل توزیع نیست" # noqa
|
||||
default_code = 'error'
|
||||
|
||||
|
||||
class QuotaExpiredTimeException(APIException):
|
||||
"""if quota allowed time for distribute, sale, etc. is expired"""
|
||||
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_detail = "زمان مجوز این سهمیه به پایان رسیده است" # noqa
|
||||
default_code = 'error'
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.0 on 2025-06-28 10:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0028_quota_remaining_weight'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='quota',
|
||||
name='assigned_organizations',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='quota',
|
||||
name='registerer_organization',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='quotadistribution',
|
||||
name='assigned_organization',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='quotadistribution',
|
||||
name='assigner_organization',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.0 on 2025-06-28 11:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0029_remove_quota_assigned_organizations_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='broker',
|
||||
name='organization_relations',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.0 on 2025-06-28 11:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0022_alter_user_mobile_alter_user_national_code'),
|
||||
('product', '0030_remove_broker_organization_relations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='broker',
|
||||
name='organization',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_organization', to='authentication.organization'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='assigned_organizations',
|
||||
field=models.ManyToManyField(blank=True, related_name='assigned_quotas', to='authentication.organization'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='registerer_organization',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='authentication.organization'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quotadistribution',
|
||||
name='assigned_organization',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='distributions', to='authentication.organization'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quotadistribution',
|
||||
name='assigner_organization',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='distributions_assigner', to='authentication.organization'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0 on 2025-06-29 11:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0031_broker_organization_quota_assigned_organizations_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='closed_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='is_closed',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0 on 2025-06-30 11:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0032_quota_closed_at_quota_is_closed'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='quotalivestockagelimitation',
|
||||
name='livestock_subtype',
|
||||
field=models.CharField(choices=[('milking', 'شیری'), ('fattening', 'پرواری')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotalivestockagelimitation',
|
||||
name='livestock_type',
|
||||
field=models.CharField(choices=[('light', 'سبک'), ('heavy', 'سنگین')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotalivestockallocation',
|
||||
name='livestock_group',
|
||||
field=models.CharField(choices=[('rural', 'روستایی'), ('industrial', 'صنعتی'), ('nomadic', 'عشایری')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotalivestockallocation',
|
||||
name='livestock_subtype',
|
||||
field=models.CharField(choices=[('milking', 'شیری'), ('fattening', 'پرواری')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotalivestockallocation',
|
||||
name='livestock_type',
|
||||
field=models.CharField(choices=[('light', 'سبک'), ('heavy', 'سنگین')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quotalivestockallocation',
|
||||
name='quantity_kg',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=12, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.0 on 2025-06-30 11:49
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0033_alter_quotalivestockagelimitation_livestock_subtype_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='quotalivestockallocation',
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
||||
17
apps/product/migrations/0035_remove_quota_quota_balance.py
Normal file
17
apps/product/migrations/0035_remove_quota_quota_balance.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.0 on 2025-07-01 05:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0034_alter_quotalivestockallocation_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='quota',
|
||||
name='quota_balance',
|
||||
),
|
||||
]
|
||||
52
apps/product/migrations/0036_historicalquotadistribution.py
Normal file
52
apps/product/migrations/0036_historicalquotadistribution.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 5.0 on 2025-07-02 05:18
|
||||
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0023_alter_organization_company_code_and_more'),
|
||||
('product', '0035_remove_quota_quota_balance'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HistoricalQuotaDistribution',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||
('create_date', models.DateTimeField(blank=True, editable=False)),
|
||||
('modify_date', models.DateTimeField(blank=True, editable=False)),
|
||||
('creator_info', models.CharField(max_length=100, null=True)),
|
||||
('modifier_info', models.CharField(max_length=100, null=True)),
|
||||
('trash', models.BooleanField(default=False)),
|
||||
('description', models.TextField(max_length=1000, null=True)),
|
||||
('distribution_id', models.CharField(max_length=20, 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_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField(db_index=True)),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('assigned_organization', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='authentication.organization')),
|
||||
('assigner_organization', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='authentication.organization')),
|
||||
('created_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('modified_by', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
('quota', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='product.quota')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical quota distribution',
|
||||
'verbose_name_plural': 'historical quota distributions',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': ('history_date', 'history_id'),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
]
|
||||
@@ -1,7 +1,12 @@
|
||||
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):
|
||||
@@ -74,6 +79,41 @@ class Product(BaseModel):
|
||||
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}'
|
||||
|
||||
@@ -159,8 +199,8 @@ class Broker(BaseModel):
|
||||
related_name='product_broker',
|
||||
null=True
|
||||
)
|
||||
organization_relations = models.ForeignKey(
|
||||
UserRelations,
|
||||
organization = models.ForeignKey(
|
||||
Organization,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='product_organization',
|
||||
null=True
|
||||
@@ -174,7 +214,7 @@ class Broker(BaseModel):
|
||||
required = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.organization_relations.organization.name} - {self.product.name}'
|
||||
return f'{self.organization.name} - {self.product.name}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
return super(Broker, self).save(*args, **kwargs)
|
||||
@@ -245,13 +285,13 @@ class Quota(BaseModel):
|
||||
""" quota for product with some conditions """
|
||||
|
||||
registerer_organization = models.ForeignKey(
|
||||
UserRelations,
|
||||
Organization,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='quotas',
|
||||
null=True
|
||||
)
|
||||
assigned_organizations = models.ManyToManyField(
|
||||
UserRelations,
|
||||
Organization,
|
||||
related_name='assigned_quotas',
|
||||
blank=True
|
||||
)
|
||||
@@ -260,7 +300,6 @@ class Quota(BaseModel):
|
||||
quota_weight = models.PositiveIntegerField(default=0)
|
||||
remaining_weight = models.PositiveBigIntegerField(default=0)
|
||||
quota_distributed = models.PositiveIntegerField(default=0)
|
||||
quota_balance = models.PositiveIntegerField(default=0)
|
||||
product = models.ForeignKey(
|
||||
Product,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -279,6 +318,8 @@ class Quota(BaseModel):
|
||||
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}"
|
||||
@@ -313,6 +354,13 @@ class Quota(BaseModel):
|
||||
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()
|
||||
@@ -364,7 +412,7 @@ class QuotaBrokerValue(BaseModel):
|
||||
value = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
|
||||
def __str__(self):
|
||||
return f"Quota ({self.quota.id}) for Broker({self.broker.organization_relations.organization.name})"
|
||||
return f"Quota ({self.quota.id}) for Broker({self.broker.organization.name})"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
return super(QuotaBrokerValue, self).save(*args, **kwargs)
|
||||
@@ -379,13 +427,16 @@ class QuotaLivestockAllocation(BaseModel):
|
||||
related_name="livestock_allocations",
|
||||
null=True
|
||||
)
|
||||
livestock_group = models.CharField(max_length=20, choices=LivestockGroup.choices)
|
||||
livestock_type = models.CharField(max_length=20, choices=LivestockType.choices)
|
||||
livestock_subtype = models.CharField(max_length=20, choices=LivestockSubtype.choices)
|
||||
quantity_kg = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
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"
|
||||
@@ -401,8 +452,8 @@ class QuotaLiveStockAgeLimitation(BaseModel):
|
||||
related_name='livestock_age_limitations',
|
||||
null=True
|
||||
)
|
||||
livestock_type = models.CharField(max_length=20, choices=LivestockType.choices)
|
||||
livestock_subtype = models.CharField(max_length=20, choices=LivestockSubtype.choices)
|
||||
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):
|
||||
@@ -414,11 +465,17 @@ class QuotaLiveStockAgeLimitation(BaseModel):
|
||||
|
||||
class QuotaDistribution(BaseModel):
|
||||
assigner_organization = models.ForeignKey(
|
||||
UserRelations,
|
||||
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(
|
||||
@@ -427,19 +484,32 @@ class QuotaDistribution(BaseModel):
|
||||
related_name='distributions_assigned',
|
||||
null=True
|
||||
)
|
||||
assigned_organization = models.ForeignKey(
|
||||
UserRelations,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='distributions',
|
||||
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}-{self.assigned_organization.organization.name}"
|
||||
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)
|
||||
|
||||
@@ -10,7 +10,8 @@ def recalculate_remaining_amount(quota):
|
||||
)['total'] or 0
|
||||
|
||||
quota.remaining_weight = quota.quota_weight - total_distributed
|
||||
quota.save(update_fields=["remaining_weight"])
|
||||
quota.quota_distributed = total_distributed
|
||||
quota.save(update_fields=["remaining_weight", "quota_distributed"])
|
||||
|
||||
|
||||
@receiver(post_save, sender=QuotaDistribution)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import datetime
|
||||
|
||||
from apps.product.web.api.v1 import product_serializers as product_serializers
|
||||
from apps.product.exceptions import QuotaExpiredTimeException
|
||||
from rest_framework.exceptions import APIException
|
||||
from apps.product import models as product_models
|
||||
from rest_framework.response import Response
|
||||
@@ -10,6 +11,7 @@ from rest_framework import viewsets
|
||||
from rest_framework import status
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def trash(queryset, pk): # noqa
|
||||
@@ -65,6 +67,10 @@ class ProductViewSet(viewsets.ModelViewSet):
|
||||
queryset = product_models.Product.objects.all()
|
||||
serializer_class = product_serializers.ProductSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
product = self.queryset.get(id=1)
|
||||
return Response(product.quota_information(), status.HTTP_200_OK)
|
||||
|
||||
@action(
|
||||
methods=['put'],
|
||||
detail=True,
|
||||
@@ -346,24 +352,26 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa
|
||||
user_relation = request.user.user_relation.all().first()
|
||||
|
||||
# add user relation to data
|
||||
request.data['registerer_organization'] = user_relation.id
|
||||
request.data['registerer_organization'] = user_relation.organization.id
|
||||
|
||||
# create quota
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid():
|
||||
quota = serializer.save()
|
||||
quota.remaining_quota_weight = quota.quota_weight
|
||||
quota.remaining_weight = quota.quota_weight
|
||||
|
||||
# create incentive plan
|
||||
plans_list = []
|
||||
if 'incentive_plan_data' in request.data.keys():
|
||||
for plan in request.data['incentive_plan_data']:
|
||||
plan.update({'quota': quota.id})
|
||||
incentive_plan = CustomOperations().custom_create(
|
||||
request=request,
|
||||
view=QuotaIncentiveAssignmentViewSet(),
|
||||
data_key='incentive_plan_data',
|
||||
additional_data={'quota': quota.id}
|
||||
data=plan
|
||||
)
|
||||
else:
|
||||
incentive_plan = {}
|
||||
plans_list.append(incentive_plan)
|
||||
|
||||
# create product price attributes for quota
|
||||
attributes_value_list = []
|
||||
@@ -376,7 +384,6 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa
|
||||
data=attr
|
||||
)
|
||||
attributes_value_list.append(attributes)
|
||||
|
||||
# create product broker values for quota
|
||||
broker_data_list = []
|
||||
if 'broker_data' in request.data.keys():
|
||||
@@ -400,7 +407,6 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa
|
||||
data=ls_alloc
|
||||
)
|
||||
allocations_list.append(allocations)
|
||||
|
||||
# create livestock age limits for quota
|
||||
livestock_age_limits = []
|
||||
if 'livestock_age_limitations' in request.data.keys():
|
||||
@@ -415,19 +421,133 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa
|
||||
|
||||
data = {
|
||||
'quota': serializer.data,
|
||||
'incentive_plan': incentive_plan,
|
||||
'incentive_plan': plans_list, # noqa
|
||||
'attribute_values': attributes_value_list,
|
||||
'broker_values': broker_data_list,
|
||||
'live_stock_allocations': allocations_list,
|
||||
'livestock_age_limitations': livestock_age_limits
|
||||
}
|
||||
|
||||
# call save method to generate id & calculate quota final price
|
||||
quota.save(calculate_final_price=True)
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, request, *args, **kwargs):
|
||||
pass
|
||||
def update(self, request, pk=None, *args, **kwargs):
|
||||
# get user relations data like organization
|
||||
user_relation = request.user.user_relation.all().first()
|
||||
|
||||
# add user relation to data
|
||||
request.data['registerer_organization'] = user_relation.organization.id
|
||||
|
||||
# create quota
|
||||
serializer = self.serializer_class(data=request.data, instance=self.get_object(), partial=True)
|
||||
if serializer.is_valid():
|
||||
quota = serializer.save()
|
||||
|
||||
# create incentive plan
|
||||
plans_list = []
|
||||
if 'incentive_plan_data' in request.data.keys():
|
||||
for plan in request.data['incentive_plan_data']:
|
||||
plan.update({'quota': quota.id})
|
||||
incentive_plan = CustomOperations().custom_update(
|
||||
request=request,
|
||||
view=QuotaIncentiveAssignmentViewSet(),
|
||||
data_key='incentive_plan_data',
|
||||
obj_id=plan['id'],
|
||||
data=plan
|
||||
)
|
||||
plans_list.append(incentive_plan)
|
||||
|
||||
# create product price attributes for quota
|
||||
attributes_value_list = [] # noqa
|
||||
if 'price_attributes_data' in request.data.keys():
|
||||
for attr in request.data['price_attributes_data']:
|
||||
attr.update({'quota': quota.id})
|
||||
attributes = CustomOperations().custom_update(
|
||||
request=request,
|
||||
view=AttributeValueViewSet(),
|
||||
obj_id=attr['id'],
|
||||
data=attr
|
||||
)
|
||||
attributes_value_list.append(attributes)
|
||||
# create product broker values for quota
|
||||
broker_data_list = []
|
||||
if 'broker_data' in request.data.keys():
|
||||
for broker in request.data['broker_data']:
|
||||
broker.update({'quota': quota.id})
|
||||
broker_value = CustomOperations().custom_update(
|
||||
request=request,
|
||||
view=QuotaBrokerValueViewSet(),
|
||||
obj_id=broker['id'],
|
||||
data=broker
|
||||
)
|
||||
broker_data_list.append(broker_value)
|
||||
|
||||
# create livestock allocations to quota
|
||||
allocations_list = []
|
||||
if 'livestock_allocation_data' in request.data.keys():
|
||||
for ls_alloc in request.data['livestock_allocation_data']:
|
||||
ls_alloc.update({'quota': quota.id})
|
||||
allocations = CustomOperations().custom_update(
|
||||
request=request,
|
||||
view=QuotaLiveStockAllocationViewSet(),
|
||||
obj_id=ls_alloc['id'],
|
||||
data=ls_alloc
|
||||
)
|
||||
allocations_list.append(allocations)
|
||||
# create livestock age limits for quota
|
||||
livestock_age_limits = []
|
||||
if 'livestock_age_limitations' in request.data.keys():
|
||||
for age_limit in request.data['livestock_age_limitations']:
|
||||
age_limit.update({'quota': quota.id})
|
||||
age_limit_creation_object = CustomOperations().custom_update(
|
||||
request=request,
|
||||
view=QuotaLiveStockAgeLimitation(),
|
||||
obj_id=age_limit['id'],
|
||||
data=age_limit
|
||||
)
|
||||
livestock_age_limits.append(age_limit_creation_object)
|
||||
|
||||
data = {
|
||||
'quota': serializer.data,
|
||||
'incentive_plan': plans_list, # noqa
|
||||
'attribute_values': attributes_value_list,
|
||||
'broker_values': broker_data_list,
|
||||
'live_stock_allocations': allocations_list,
|
||||
'livestock_age_limitations': livestock_age_limits
|
||||
}
|
||||
|
||||
# call save method to generate id & calculate quota final price
|
||||
quota.save(calculate_final_price=True)
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@action(
|
||||
methods=['patch'],
|
||||
detail=True,
|
||||
url_path='close',
|
||||
url_name='close',
|
||||
name='close'
|
||||
)
|
||||
@transaction.atomic
|
||||
def close(self, request, pk=None):
|
||||
""" to close quota """
|
||||
quota = self.get_object()
|
||||
|
||||
# check quota expired time
|
||||
if not quota.is_in_valid_time():
|
||||
raise QuotaExpiredTimeException()
|
||||
|
||||
if quota.is_closed:
|
||||
raise APIException("این سهمیه قبلا بسته شده است", status.HTTP_400_BAD_REQUEST) # noqa
|
||||
|
||||
quota.is_closed = True
|
||||
quota.closed_at = datetime.now()
|
||||
quota.save()
|
||||
|
||||
return Response(status.HTTP_200_OK)
|
||||
|
||||
@action(
|
||||
methods=['get'],
|
||||
@@ -440,7 +560,7 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa
|
||||
""" list of quotas for creator """
|
||||
assigner = product_models.UserRelations.objects.filter(user=request.user).first()
|
||||
serializers = self.serializer_class(
|
||||
self.queryset.filter(registerer_organization=assigner),
|
||||
self.queryset.filter(registerer_organization=assigner.organization),
|
||||
many=True
|
||||
).data
|
||||
return Response(serializers.data, status=status.HTTP_200_OK)
|
||||
@@ -456,7 +576,7 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa
|
||||
""" list of quotas for assigned organizations """
|
||||
assigned = product_models.UserRelations.objects.filter(user=request.user).first()
|
||||
serializer = self.serializer_class(
|
||||
self.queryset.filter(assigned_organizations=assigned),
|
||||
self.queryset.filter(assigned_organizations=assigned.organization),
|
||||
many=True
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
from apps.product import models as product_models
|
||||
from apps.authorization.api.v1 import serializers as authorize_serializers
|
||||
from apps.authentication.api.v1.serializers.serializer import OrganizationSerializer
|
||||
|
||||
|
||||
class ProductCategorySerializer(serializers.ModelSerializer):
|
||||
@@ -53,16 +54,14 @@ class AttributeValueSerializer(serializers.ModelSerializer):
|
||||
model = product_models.AttributeValue
|
||||
fields = '__all__'
|
||||
|
||||
def to_representation(self, instance):
|
||||
""" Custom output """
|
||||
def update(self, instance, validated_data):
|
||||
|
||||
representation = super().to_representation(instance)
|
||||
if instance.quota:
|
||||
representation['quota'] = QuotaSerializer(instance.quota).data
|
||||
if instance.attribute:
|
||||
representation['attribute'] = AttributeSerializer(instance.attribute).data
|
||||
instance.quota = validated_data.get('quota', instance.quota)
|
||||
instance.attribute = validated_data.get('attribute', instance.attribute)
|
||||
instance.value = validated_data.get('value', instance.value)
|
||||
instance.save()
|
||||
|
||||
return representation
|
||||
return instance
|
||||
|
||||
|
||||
class BrokerSerializer(serializers.ModelSerializer):
|
||||
@@ -74,9 +73,9 @@ class BrokerSerializer(serializers.ModelSerializer):
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
if instance.organization_relations:
|
||||
representation['organization_relations'] = authorize_serializers.UserRelationSerializer(
|
||||
instance.organization_relations
|
||||
if instance.organization:
|
||||
representation['organization'] = OrganizationSerializer(
|
||||
instance.organization
|
||||
).data
|
||||
|
||||
return representation
|
||||
@@ -109,6 +108,66 @@ class QuotaSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = product_models.Quota
|
||||
fields = '__all__'
|
||||
depth = 0
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
if isinstance(instance, product_models.Quota):
|
||||
representation['incentive_plan'] = QuotaIncentiveAssignmentSerializer(
|
||||
instance.incentive_assignments.all(),
|
||||
many=True
|
||||
).data
|
||||
|
||||
representation['attribute_values'] = AttributeValueSerializer(
|
||||
instance.attribute_values.all(),
|
||||
many=True
|
||||
).data
|
||||
|
||||
representation['brokers'] = QuotaBrokerValueSerializer(
|
||||
instance.broker_values.all(),
|
||||
many=True
|
||||
).data
|
||||
|
||||
representation['livestock_allocations'] = QuotaLiveStockAllocationSerializer(
|
||||
instance.livestock_allocations.all(),
|
||||
many=True
|
||||
).data
|
||||
|
||||
representation['livestock_limitations'] = QuotaLiveStockAgeLimitationSerializer(
|
||||
instance.livestock_age_limitations.all(),
|
||||
many=True
|
||||
).data
|
||||
|
||||
return representation
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
""" Custom Update """
|
||||
|
||||
instance.quota_id = validated_data.get('quota_id', instance.quota_id)
|
||||
instance.quota_code = validated_data.get('quota_code', instance.quota_code)
|
||||
instance.quota_weight = validated_data.get('quota_weight', instance.quota_weight)
|
||||
instance.remaining_weight = validated_data.get('remaining_weight', instance.remaining_weight)
|
||||
instance.quota_distributed = validated_data.get('quota_distributed', instance.quota_distributed)
|
||||
instance.quota_balance = validated_data.get('quota_balance', instance.quota_balance)
|
||||
instance.product = validated_data.get('product', instance.product)
|
||||
instance.sale_type = validated_data.get('sale_type', instance.sale_type)
|
||||
instance.month_choices = validated_data.get('month_choices', instance.month_choices)
|
||||
instance.group = validated_data.get('group', instance.group)
|
||||
instance.has_distribution_limit = validated_data.get('has_distribution_limit', instance.has_distribution_limit)
|
||||
instance.distribution_mode = validated_data.get('distribution_mode', instance.distribution_mode)
|
||||
instance.base_price_factory = validated_data.get('base_price_factory', instance.base_price_factory)
|
||||
instance.base_price_cooperative = validated_data.get('base_price_cooperative', instance.base_price_cooperative)
|
||||
instance.final_price = validated_data.get('final_price', instance.final_price)
|
||||
instance.is_closed = validated_data.get('is_closed', instance.is_closed)
|
||||
instance.closed_at = validated_data.get('closed_at', instance.closed_at)
|
||||
instance.save()
|
||||
|
||||
instance.assigned_organizations.clear()
|
||||
instance.assigned_organizations.add(
|
||||
*(validated_data.get('assigned_organizations', instance.assigned_organizations))
|
||||
)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class QuotaIncentiveAssignmentSerializer(serializers.ModelSerializer):
|
||||
@@ -116,20 +175,74 @@ class QuotaIncentiveAssignmentSerializer(serializers.ModelSerializer):
|
||||
model = product_models.QuotaIncentiveAssignment
|
||||
fields = '__all__'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
""" Custom Update """
|
||||
|
||||
instance.quota = validated_data.get('quota', instance.quota)
|
||||
instance.incentive_plan = validated_data.get('incentive_plan', instance.incentive_plan)
|
||||
instance.heavy_value = validated_data.get('heavy_value', instance.heavy_value)
|
||||
instance.light_value = validated_data.get('light_value', instance.light_value)
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class QuotaBrokerValueSerializer(serializers.ModelSerializer): # noqa
|
||||
class Meta:
|
||||
model = product_models.QuotaBrokerValue
|
||||
fields = '__all__'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
""" Custom Update """
|
||||
|
||||
instance.quota = validated_data.get('quota', instance.quota)
|
||||
instance.broker = validated_data.get('broker', instance.broker)
|
||||
instance.value = validated_data.get('value', instance.value)
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class QuotaLiveStockAllocationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = product_models.QuotaLivestockAllocation
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'livestock_group': {
|
||||
'required': False
|
||||
},
|
||||
'livestock_type': {
|
||||
'required': False
|
||||
},
|
||||
'livestock_subtype': {
|
||||
'required': False
|
||||
}
|
||||
}
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
""" Custom Update """
|
||||
|
||||
instance.quota = validated_data.get('quota', instance.quota)
|
||||
instance.livestock_group = validated_data.get('livestock_group', instance.livestock_group)
|
||||
instance.livestock_type = validated_data.get('livestock_type', instance.livestock_type)
|
||||
instance.livestock_subtype = validated_data.get('livestock_subtype', instance.livestock_subtype)
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class QuotaLiveStockAgeLimitationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = product_models.QuotaLiveStockAgeLimitation
|
||||
fields = '__all__'
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
""" Custom Update """
|
||||
|
||||
instance.quota = validated_data.get('quota', instance.quota)
|
||||
instance.livestock_type = validated_data.get('livestock_type', instance.livestock_type)
|
||||
instance.livestock_subtype = validated_data.get('livestock_subtype', instance.livestock_subtype)
|
||||
instance.age_month = validated_data.get('age_month', instance.age_month)
|
||||
instance.save()
|
||||
|
||||
return instance
|
||||
|
||||
@@ -42,7 +42,7 @@ class QuotaDistributionViewSet(viewsets.ModelViewSet):
|
||||
except APIException as e:
|
||||
raise APIException("unauthorized", code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
request.data.update({'assigner_organization': assigner_user.id})
|
||||
request.data.update({'assigner_organization': assigner_user.organization.id})
|
||||
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
@@ -77,6 +77,14 @@ class QuotaDistributionViewSet(viewsets.ModelViewSet):
|
||||
@transaction.atomic
|
||||
def trash(self, request, pk=None):
|
||||
""" Sent quota distribution to trash """
|
||||
quota_distribution = self.get_object()
|
||||
|
||||
# check if distribution has inventory entry
|
||||
if quota_distribution.inventory_entry.exists():
|
||||
raise APIException(
|
||||
"امکان حذف این توزیع وجود ندارد. ورود به انبار برای آن ثبت شده است", # noqa
|
||||
status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
try:
|
||||
trash(self.queryset, pk)
|
||||
except APIException as e:
|
||||
@@ -92,6 +100,14 @@ class QuotaDistributionViewSet(viewsets.ModelViewSet):
|
||||
@transaction.atomic
|
||||
def delete(self, request, pk=None):
|
||||
""" Full delete of quota distribution object """
|
||||
quota_distribution = self.get_object()
|
||||
|
||||
# check if distribution has inventory entry
|
||||
if quota_distribution.inventory_entry.exists():
|
||||
raise APIException(
|
||||
"امکان حذف این توزیع وجود ندارد. ورود به انبار برای آن ثبت شده است", # noqa
|
||||
status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
try:
|
||||
delete(self.queryset, pk)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@@ -2,7 +2,11 @@ from rest_framework import serializers
|
||||
from apps.product import models as product_models
|
||||
from apps.product.web.api.v1.product_serializers import QuotaSerializer
|
||||
from django.db import models
|
||||
from apps.product.exceptions import QuotaWeightException
|
||||
from apps.product.exceptions import (
|
||||
QuotaWeightException,
|
||||
QuotaClosedException,
|
||||
QuotaExpiredTimeException
|
||||
)
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
@@ -17,13 +21,25 @@ class QuotaDistributionSerializer(serializers.ModelSerializer):
|
||||
}
|
||||
|
||||
def validate(self, data):
|
||||
""" to validate if distribution weight
|
||||
more than quota weight raise exception """
|
||||
"""
|
||||
to validate if distribution weight
|
||||
more than quota weight raise exception
|
||||
or if quota is closed raise exception
|
||||
"""
|
||||
|
||||
quota = data['quota']
|
||||
amount = data['weight']
|
||||
instance_id = self.instance.id if self.instance else None
|
||||
|
||||
# check quota expired time
|
||||
if not quota.is_in_valid_time():
|
||||
raise QuotaExpiredTimeException()
|
||||
|
||||
# check if quota is closed
|
||||
if quota.is_closed:
|
||||
raise QuotaClosedException()
|
||||
|
||||
# total quota distributions weight
|
||||
total = product_models.QuotaDistribution.objects.filter(
|
||||
quota=quota
|
||||
).exclude(id=instance_id).aggregate(
|
||||
|
||||
@@ -4,3 +4,6 @@ from django.apps import AppConfig
|
||||
class WarehouseConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.warehouse'
|
||||
|
||||
def ready(self):
|
||||
import apps.warehouse.signals
|
||||
|
||||
14
apps/warehouse/exceptions.py
Normal file
14
apps/warehouse/exceptions.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class InventoryEntryWeightException(APIException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_detail = "مقدار وارد شده برای ورودی به انبار از مقدار کل سهمیه توزیع داده شده بیشتر است" # noqa
|
||||
default_code = 'error'
|
||||
|
||||
|
||||
class TotalInventorySaleException(APIException):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
default_detail = "مقدار وارد شده برای فروش از انبار از موجودی انبار بیشتر میباشد" # noqa
|
||||
default_code = 'error'
|
||||
59
apps/warehouse/migrations/0001_initial.py
Normal file
59
apps/warehouse/migrations/0001_initial.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Generated by Django 5.0 on 2025-06-28 10:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('authorization', '0019_page_is_active_permissions_is_active'),
|
||||
('product', '0029_remove_quota_assigned_organizations_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WareHouse',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('create_date', models.DateTimeField(auto_now_add=True)),
|
||||
('modify_date', models.DateTimeField(auto_now=True)),
|
||||
('creator_info', models.CharField(max_length=100, null=True)),
|
||||
('modifier_info', models.CharField(max_length=100, null=True)),
|
||||
('trash', models.BooleanField(default=False)),
|
||||
('name', models.CharField(max_length=250, null=True)),
|
||||
('address', models.TextField(blank=True, null=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)),
|
||||
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)),
|
||||
('user_relation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='warehouse', to='authorization.userrelations')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InventoryEntry',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('create_date', models.DateTimeField(auto_now_add=True)),
|
||||
('modify_date', models.DateTimeField(auto_now=True)),
|
||||
('creator_info', models.CharField(max_length=100, null=True)),
|
||||
('modifier_info', models.CharField(max_length=100, null=True)),
|
||||
('trash', models.BooleanField(default=False)),
|
||||
('document', models.CharField(max_length=250, null=True)),
|
||||
('is_confirmed', models.BooleanField(default=False)),
|
||||
('notes', models.TextField(blank=True, null=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)),
|
||||
('distribution', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_entry', to='product.quotadistribution')),
|
||||
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)),
|
||||
('warehouse', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory', to='warehouse.warehouse')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.0 on 2025-06-28 12:03
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('warehouse', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='warehouse',
|
||||
name='address',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='warehouse',
|
||||
name='name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='warehouse',
|
||||
name='user_relation',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.0 on 2025-06-28 12:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0023_alter_organization_company_code_and_more'),
|
||||
('warehouse', '0002_remove_warehouse_address_remove_warehouse_name_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryentry',
|
||||
name='distribution',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='warehouse',
|
||||
name='organization',
|
||||
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='warehouse', to='authentication.organization'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.0 on 2025-06-28 12:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('product', '0031_broker_organization_quota_assigned_organizations_and_more'),
|
||||
('warehouse', '0003_remove_inventoryentry_distribution_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inventoryentry',
|
||||
name='distribution',
|
||||
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_entry', to='product.quotadistribution'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.0 on 2025-06-29 05:44
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0023_alter_organization_company_code_and_more'),
|
||||
('product', '0031_broker_organization_quota_assigned_organizations_and_more'),
|
||||
('warehouse', '0004_inventoryentry_distribution'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='inventoryentry',
|
||||
name='warehouse',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryentry',
|
||||
name='delivery_address',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryentry',
|
||||
name='lading_number',
|
||||
field=models.CharField(max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryentry',
|
||||
name='organization',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory', to='authentication.organization'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventoryentry',
|
||||
name='weight',
|
||||
field=models.PositiveBigIntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='inventoryentry',
|
||||
name='distribution',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_entry', to='product.quotadistribution'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='WareHouse',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
# Generated by Django 5.0 on 2025-06-29 11:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0023_alter_organization_company_code_and_more'),
|
||||
('authorization', '0019_page_is_active_permissions_is_active'),
|
||||
('product', '0032_quota_closed_at_quota_is_closed'),
|
||||
('warehouse', '0005_remove_inventoryentry_warehouse_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InventoryQuotaSale',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('create_date', models.DateTimeField(auto_now_add=True)),
|
||||
('modify_date', models.DateTimeField(auto_now=True)),
|
||||
('creator_info', models.CharField(max_length=100, null=True)),
|
||||
('modifier_info', models.CharField(max_length=100, null=True)),
|
||||
('trash', models.BooleanField(default=False)),
|
||||
('weight', models.PositiveBigIntegerField(default=0)),
|
||||
('herd_owners_number', models.PositiveBigIntegerField(default=0)),
|
||||
('transactions_number', models.PositiveBigIntegerField(default=0)),
|
||||
('sale_status', models.BooleanField(default=False)),
|
||||
('is_active', models.BooleanField(default=0)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)),
|
||||
('inventory_entry', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_sales', to='warehouse.inventoryentry')),
|
||||
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)),
|
||||
('quota_distribution', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_sales', to='product.quotadistribution')),
|
||||
('seller_organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_sales', to='authentication.organization')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InventorySaleTransaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('create_date', models.DateTimeField(auto_now_add=True)),
|
||||
('modify_date', models.DateTimeField(auto_now=True)),
|
||||
('creator_info', models.CharField(max_length=100, null=True)),
|
||||
('modifier_info', models.CharField(max_length=100, null=True)),
|
||||
('trash', models.BooleanField(default=False)),
|
||||
('transaction_id', models.CharField(max_length=50, null=True)),
|
||||
('weight', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('delivery_address', models.TextField(blank=True, null=True)),
|
||||
('total_price', models.PositiveBigIntegerField(default=0)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('buyer_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='buyer_sale_transactions', to='authorization.userrelations')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)),
|
||||
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)),
|
||||
('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_transactions', to='product.product')),
|
||||
('quota_sale', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_transactions', to='warehouse.inventoryquotasale')),
|
||||
('seller_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seller_sale_transactions', to='authorization.userrelations')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.0 on 2025-06-29 12:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('warehouse', '0006_inventoryquotasale_inventorysaletransaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='buyer_user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='seller_user',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.0 on 2025-06-29 12:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('warehouse', '0007_remove_inventorysaletransaction_buyer_user_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='buyer_user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='buyer_sale_transactions', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='seller_user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seller_sale_transactions', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.0 on 2025-06-29 12:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0023_alter_organization_company_code_and_more'),
|
||||
('warehouse', '0008_inventorysaletransaction_buyer_user_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='buyer_organization',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_transactions', to='authentication.organization'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.0 on 2025-07-01 07:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('warehouse', '0009_inventorysaletransaction_buyer_organization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='quota_sale',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='buyer_organization',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='buyer_user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='created_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='modified_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='product',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='inventorysaletransaction',
|
||||
name='seller_user',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='InventoryQuotaSale',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='InventorySaleTransaction',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.0 on 2025-07-01 07:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0023_alter_organization_company_code_and_more'),
|
||||
('product', '0035_remove_quota_quota_balance'),
|
||||
('warehouse', '0010_remove_inventorysaletransaction_quota_sale_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InventoryQuotaSaleTransaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('create_date', models.DateTimeField(auto_now_add=True)),
|
||||
('modify_date', models.DateTimeField(auto_now=True)),
|
||||
('creator_info', models.CharField(max_length=100, null=True)),
|
||||
('modifier_info', models.CharField(max_length=100, null=True)),
|
||||
('trash', models.BooleanField(default=False)),
|
||||
('transaction_id', models.CharField(max_length=50, null=True)),
|
||||
('weight', models.DecimalField(decimal_places=2, max_digits=12, null=True)),
|
||||
('delivery_address', models.TextField(blank=True, null=True)),
|
||||
('transaction_price', models.PositiveBigIntegerField(default=0)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('herd_owners_number', models.PositiveBigIntegerField(default=0)),
|
||||
('transactions_number', models.PositiveBigIntegerField(default=0)),
|
||||
('sale_status', models.BooleanField(default=False)),
|
||||
('is_active', models.BooleanField(default=0)),
|
||||
('buyer_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='buyer_sale_transactions', to=settings.AUTH_USER_MODEL)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)),
|
||||
('inventory_entry', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_sales', to='warehouse.inventoryentry')),
|
||||
('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)),
|
||||
('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_transactions', to='product.product')),
|
||||
('quota_distribution', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_sales', to='product.quotadistribution')),
|
||||
('seller_organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_sales', to='authentication.organization')),
|
||||
('seller_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seller_sale_transactions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,103 @@
|
||||
from apps.product import models as product_models
|
||||
from apps.authentication.models import User
|
||||
from apps.core.models import BaseModel
|
||||
from django.db import models
|
||||
|
||||
|
||||
class InventoryEntry(BaseModel):
|
||||
distribution = models.ForeignKey(
|
||||
product_models.QuotaDistribution,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='inventory_entry',
|
||||
null=True
|
||||
)
|
||||
organization = models.ForeignKey(
|
||||
product_models.Organization,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="inventory",
|
||||
null=True
|
||||
)
|
||||
weight = models.PositiveBigIntegerField(default=0)
|
||||
balance = models
|
||||
lading_number = models.CharField(max_length=50, null=True)
|
||||
delivery_address = models.TextField(blank=True, null=True)
|
||||
document = models.CharField(max_length=250, null=True)
|
||||
is_confirmed = models.BooleanField(default=False)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
|
||||
@property
|
||||
def total_sold(self):
|
||||
return self.inventory_sales.aggregate(total=models.Sum('weight'))['total'] or 0
|
||||
|
||||
@property
|
||||
def remaining_weight(self):
|
||||
return self.weight - self.total_sold
|
||||
|
||||
def __str__(self):
|
||||
return f"distribution: {self.distribution.distribution_id}-{self.organization.name}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(InventoryEntry, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class InventoryQuotaSaleTransaction(BaseModel):
|
||||
transaction_id = models.CharField(max_length=50, null=True)
|
||||
seller_organization = models.ForeignKey(
|
||||
product_models.Organization,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='inventory_sales',
|
||||
null=True
|
||||
)
|
||||
quota_distribution = models.ForeignKey(
|
||||
product_models.QuotaDistribution,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='inventory_sales',
|
||||
null=True
|
||||
)
|
||||
inventory_entry = models.ForeignKey(
|
||||
InventoryEntry,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='inventory_sales',
|
||||
null=True
|
||||
)
|
||||
buyer_user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='buyer_sale_transactions',
|
||||
null=True
|
||||
)
|
||||
seller_user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='seller_sale_transactions',
|
||||
null=True
|
||||
)
|
||||
weight = models.DecimalField(max_digits=12, decimal_places=2, null=True)
|
||||
delivery_address = models.TextField(blank=True, null=True)
|
||||
product = models.ForeignKey(
|
||||
product_models.Product,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='sale_transactions',
|
||||
null=True
|
||||
)
|
||||
transaction_price = models.PositiveBigIntegerField(default=0)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
herd_owners_number = models.PositiveBigIntegerField(default=0)
|
||||
transactions_number = models.PositiveBigIntegerField(default=0)
|
||||
sale_status = models.BooleanField(default=False)
|
||||
is_active = models.BooleanField(default=0)
|
||||
|
||||
def buyers_count(self):
|
||||
""" number of buyers from specific inventory """
|
||||
|
||||
unique_buyers_count = self.objects.filter(
|
||||
inventory_entry=self.inventory_entry
|
||||
).values('buyer_user').distinct().count()
|
||||
|
||||
return unique_buyers_count
|
||||
|
||||
def __str__(self):
|
||||
return f"Inventory Sale: {self.transaction_id}-{self.quota_distribution.distribution_id}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(InventoryQuotaSaleTransaction, self).save(*args, **kwargs)
|
||||
|
||||
36
apps/warehouse/signals.py
Normal file
36
apps/warehouse/signals.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from django.db.models import Sum
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from apps.product.models import QuotaDistribution
|
||||
from .models import InventoryEntry, InventoryQuotaSaleTransaction
|
||||
|
||||
|
||||
def calculate_warehouse_entry(quota_distribution):
|
||||
total_entry = quota_distribution.inventory_entry.aggregate(
|
||||
total=Sum('weight')
|
||||
)['total'] or 0
|
||||
|
||||
quota_distribution.warehouse_entry = total_entry
|
||||
quota_distribution.save(update_fields=['warehouse_entry'])
|
||||
|
||||
|
||||
def warehouse_sold_and_balance(quota_distribution):
|
||||
total_sold = quota_distribution.inventory_sales.aggregate(
|
||||
total=Sum('weight')
|
||||
)['total'] or 0
|
||||
|
||||
quota_distribution.been_sold = total_sold
|
||||
quota_distribution.warehouse_balance = quota_distribution.warehouse_entry - total_sold
|
||||
quota_distribution.save(update_fields=['been_sold', 'warehouse_balance'])
|
||||
|
||||
|
||||
@receiver(post_save, sender=InventoryEntry)
|
||||
@receiver(post_delete, sender=InventoryEntry)
|
||||
def update_distribution_warehouse_entry(sender, instance, **kwargs):
|
||||
calculate_warehouse_entry(instance.distribution)
|
||||
|
||||
|
||||
@receiver(post_save, sender=InventoryQuotaSaleTransaction)
|
||||
@receiver(post_delete, sender=InventoryQuotaSaleTransaction)
|
||||
def update_distribution_warehouse_sold_and_balance(sender, instance, **kwargs):
|
||||
warehouse_sold_and_balance(instance.quota_distribution)
|
||||
@@ -1 +1,7 @@
|
||||
# Your urls go here
|
||||
from django.urls import path, include
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('web/api/', include('apps.warehouse.web.api.v1.urls'))
|
||||
]
|
||||
|
||||
17
apps/warehouse/web/api/v1/api.py
Normal file
17
apps/warehouse/web/api/v1/api.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from apps.warehouse.web.api.v1 import serializers as warehouse_serializers
|
||||
from apps.warehouse import models as warehouse_models
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets
|
||||
from django.db import transaction
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class InventoryEntryViewSet(viewsets.ModelViewSet):
|
||||
queryset = warehouse_models.InventoryEntry.objects.all()
|
||||
serializer_class = warehouse_serializers.InventoryEntrySerializer
|
||||
|
||||
|
||||
class InventoryQuotaSaleTransactionViewSet(viewsets.ModelViewSet):
|
||||
queryset = warehouse_models.InventoryQuotaSaleTransaction.objects.all()
|
||||
serializer_class = warehouse_serializers.InventoryQuotaSaleTransactionSerializer
|
||||
@@ -0,0 +1,90 @@
|
||||
from apps.warehouse.exceptions import (
|
||||
InventoryEntryWeightException,
|
||||
TotalInventorySaleException
|
||||
)
|
||||
from apps.product.exceptions import QuotaExpiredTimeException
|
||||
from apps.warehouse import models as warehouse_models
|
||||
from apps.authorization.models import UserRelations
|
||||
from rest_framework import serializers
|
||||
from django.db import models
|
||||
|
||||
|
||||
class InventoryEntrySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = warehouse_models.InventoryEntry
|
||||
fields = '__all__'
|
||||
|
||||
def create(self, validated_data):
|
||||
""" Custom create & set organization """
|
||||
|
||||
distribution = validated_data['distribution']
|
||||
organization = distribution.assigned_organization
|
||||
|
||||
return warehouse_models.InventoryEntry.objects.create(
|
||||
organization=organization,
|
||||
**validated_data
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
check if inventory entries weight is not more than
|
||||
distribution weight & check quota expired time
|
||||
"""
|
||||
|
||||
distribution = attrs['distribution']
|
||||
|
||||
# check for quota expired time
|
||||
if not distribution.quota.is_in_valid_time():
|
||||
raise QuotaExpiredTimeException()
|
||||
|
||||
# total inventory entries weight
|
||||
total_entered = distribution.inventory_entry.filter(is_confirmed=True).aggregate(
|
||||
total=models.Sum('weight')
|
||||
)['total'] or 0
|
||||
|
||||
if total_entered + attrs['weight'] > distribution.weight:
|
||||
raise InventoryEntryWeightException()
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class InventoryQuotaSaleTransactionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = warehouse_models.InventoryQuotaSaleTransaction
|
||||
fields = '__all__'
|
||||
depth = 0
|
||||
|
||||
def validate(self, attrs):
|
||||
"""
|
||||
validate total inventory sale should be fewer than
|
||||
inventory entry from distribution
|
||||
"""
|
||||
inventory_entry = attrs['inventory_entry']
|
||||
distribution = attrs['quota_distribution']
|
||||
|
||||
total_sale_weight = inventory_entry.inventory_sales.aggregate(
|
||||
total=models.Sum('weight')
|
||||
)['total'] or 0
|
||||
|
||||
if total_sale_weight + attrs['weight'] > distribution.warehouse_balance:
|
||||
raise TotalInventorySaleException()
|
||||
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
""" Custom create & set some parameters like seller & buyer """
|
||||
|
||||
distribution = validated_data['quota_distribution']
|
||||
seller_organization = distribution.assigned_organization
|
||||
|
||||
user = self.context['request'].user
|
||||
buyer_user = user
|
||||
seller_user = validated_data['inventory_entry'].created_by
|
||||
|
||||
return warehouse_models.InventoryQuotaSaleTransaction.objects.create(
|
||||
seller_organization=seller_organization,
|
||||
seller_user=seller_user,
|
||||
buyer_user=buyer_user,
|
||||
**validated_data
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from apps.warehouse.web.api.v1 import api
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'inventory_entry', api.InventoryEntryViewSet, basename='inventory_entry')
|
||||
router.register(
|
||||
r'inventory_sale_transaction',
|
||||
api.InventoryQuotaSaleTransactionViewSet,
|
||||
basename='inventory_sale_transaction'
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('v1/', include(router.urls)),
|
||||
]
|
||||
|
||||
@@ -35,7 +35,6 @@ class CustomOperations:
|
||||
serializer = view.serializer_class(data=data) # noqa
|
||||
serializer.is_valid(raise_exception=True) # noqa
|
||||
view.perform_create(serializer) # noqa
|
||||
|
||||
headers = view.get_success_headers(serializer.data) # noqa
|
||||
return serializer.data
|
||||
|
||||
@@ -50,16 +49,17 @@ class CustomOperations:
|
||||
additional_data: dict = None
|
||||
) -> typing.Any:
|
||||
view_data = request.data # included needed data for view set # noqa
|
||||
queryset = view.queryset.get(id=obj_id)
|
||||
if user:
|
||||
view_data[data_key].update({'user': user.id}) # noqa
|
||||
if additional_data:
|
||||
view_data[data_key].update(additional_data)
|
||||
if data_key:
|
||||
serializer = view.serializer_class(data=view_data[data_key]) # noqa
|
||||
serializer = view.serializer_class(data=view_data[data_key], instance=queryset, partial=True) # noqa
|
||||
if data:
|
||||
serializer = view.serializer_class(data=data) # noqa
|
||||
serializer = view.serializer_class(data=data, instance=queryset, partial=True) # noqa
|
||||
serializer.is_valid(raise_exception=True) # noqa
|
||||
serializer.update(view.queryset.get(id=obj_id), view_data[data_key]) # noqa
|
||||
# view.perform_update(serializer) # noqa
|
||||
object_data = data if data else view_data[data_key]
|
||||
serializer.save() # noqa
|
||||
headers = view.get_success_headers(serializer.data) # noqa
|
||||
return serializer.data
|
||||
|
||||
@@ -75,3 +75,4 @@ django-crum
|
||||
django-rest-swagger
|
||||
drf-yasg
|
||||
tinydb
|
||||
django-simple-history
|
||||
Reference in New Issue
Block a user