From ebc79a7dbd3456b3682aae80192cebd878aa3574 Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Mon, 21 Jul 2025 09:39:31 +0330 Subject: [PATCH] some first models of pos & add required to attributes --- Rasaddam_Backend/settings.py | 9 ++ ...organizationstats_total_buyers_and_more.py | 32 +++++ apps/authentication/models.py | 4 +- apps/authentication/signals.py | 36 +++++- apps/authentication/websocket/consumer.py | 39 +++++- apps/authentication/websocket/routing.py | 2 +- ...device_company_device_latitude_and_more.py | 92 ++++++++++++++ apps/pos_device/models.py | 33 ++++- ...emove_productstats_total_quota_and_more.py | 47 +++++++ .../migrations/0053_attribute_required.py | 18 +++ apps/product/models.py | 11 +- apps/product/signals.py | 120 +++++++++++++++++- .../migrations/0012_inventoryentry_balance.py | 18 +++ apps/warehouse/models.py | 2 +- 14 files changed, 446 insertions(+), 17 deletions(-) create mode 100644 apps/authentication/migrations/0027_remove_organizationstats_total_buyers_and_more.py create mode 100644 apps/pos_device/migrations/0002_device_acceptor_device_company_device_latitude_and_more.py create mode 100644 apps/product/migrations/0052_remove_productstats_total_quota_and_more.py create mode 100644 apps/product/migrations/0053_attribute_required.py create mode 100644 apps/warehouse/migrations/0012_inventoryentry_balance.py diff --git a/Rasaddam_Backend/settings.py b/Rasaddam_Backend/settings.py index 74f9dff..c111481 100644 --- a/Rasaddam_Backend/settings.py +++ b/Rasaddam_Backend/settings.py @@ -254,6 +254,15 @@ CACHES = { } } +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("redis://:ydnW4hwzuDRYcTX3FWCHgQ1f@apo.liara.cloud:33740/0")], + }, + }, +} + REST_CAPTCHA = { 'CAPTCHA_CACHE': 'default', 'CAPTCHA_TIMEOUT': 300, # 5 minutes diff --git a/apps/authentication/migrations/0027_remove_organizationstats_total_buyers_and_more.py b/apps/authentication/migrations/0027_remove_organizationstats_total_buyers_and_more.py new file mode 100644 index 0000000..32c9f17 --- /dev/null +++ b/apps/authentication/migrations/0027_remove_organizationstats_total_buyers_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0 on 2025-07-20 08:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0026_organizationstats'), + ] + + operations = [ + migrations.RemoveField( + model_name='organizationstats', + name='total_buyers', + ), + migrations.AddField( + model_name='organizationstats', + name='active_quotas_weight', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='organizationstats', + name='closed_quotas_weight', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='organizationstats', + name='total_quotas_weight', + field=models.PositiveBigIntegerField(default=0), + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 6df97f3..6c00f8b 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -143,10 +143,12 @@ class OrganizationStats(BaseModel): null=True ) total_quota_received = models.PositiveBigIntegerField(default=0) + active_quotas_weight = models.PositiveBigIntegerField(default=0) + closed_quotas_weight = models.PositiveBigIntegerField(default=0) + total_quotas_weight = models.PositiveBigIntegerField(default=0) total_distributed = models.PositiveBigIntegerField(default=0) total_inventory_in = models.PositiveBigIntegerField(default=0) total_sold = models.PositiveBigIntegerField(default=0) - total_buyers = models.PositiveBigIntegerField(default=0) def __str__(self): return f'Organization: {self.organization.name}' diff --git a/apps/authentication/signals.py b/apps/authentication/signals.py index 03ef480..f2fce1e 100644 --- a/apps/authentication/signals.py +++ b/apps/authentication/signals.py @@ -1,11 +1,43 @@ from django.db.models import Sum from apps.product.models import QuotaDistribution from apps.warehouse.models import InventoryQuotaSaleTransaction +from apps.authentication.models import Organization, OrganizationStats from django.db.models.signals import post_save, post_delete from django.dispatch import receiver +def update_organization_stats(instance: Organization): + """ update all stats of organization from quota """ + + if hasattr(instance, 'stats'): + stat = instance.stats + else: + stat = OrganizationStats.objects.create(organization=instance) + + # set organization stats from quotas, distributions transactions & etc + stat.total_quota_received = instance.assigned_quotas.count() + stat.active_quotas_weight = instance.assigned_quotas.filter(is_closed=False).count() + stat.closed_quotas_weight = instance.assigned_quotas.filter(is_closed=True).count() + stat.total_distributed = instance.distributions.count() + stat.total_inventory_in = instance.inventory.count() + stat.total_sold = instance.distributions.all().aggregate( + total_sold=Sum('been_sold') + )['total_sold'] or 0 + + stat.save(update_fields=[ + "total_quota_received", + "active_quotas_weight", + "closed_quotas_weight", + "total_distributed", + "total_inventory_in", + "total_sold", + ]) + + @receiver([post_save, post_delete], sender=QuotaDistribution) @receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction) -def update_organization_stats(sender, instance, **kwargs): - pass +def organization_stats(sender, instance, **kwargs): + if sender == QuotaDistribution: + update_organization_stats(instance.assigned_organization) + elif sender == InventoryQuotaSaleTransaction: + update_organization_stats(instance.inventory_entry.organization) diff --git a/apps/authentication/websocket/consumer.py b/apps/authentication/websocket/consumer.py index b83f826..2a42e10 100644 --- a/apps/authentication/websocket/consumer.py +++ b/apps/authentication/websocket/consumer.py @@ -24,7 +24,42 @@ class MyConsumer(AsyncWebsocketConsumer): unit = await get_unit(int(message)) print("Received:", message) - # پاسخ به کلاینت await self.send(text_data=json.dumps({ - "message": unit + "message": unit.unit })) + + +class SendFromServerConsumer(AsyncWebsocketConsumer): + async def connect(self): + self.group_name = 'mojtaba_group' # noqa + await self.channel_layer.group_add( + self.group_name, + self.channel_name + ) + await self.accept() + await self.send(text_data=json.dumps({"message": "Connected!"})) + + async def disconnect(self, code): + await self.channel_layer.group_discard( + self.group_name, + self.channel_name + ) + + async def receive(self, text_data=None, bytes_data=None): + data = json.loads(text_data) + msg = data.get("message", "") + + await self.channel_layer.group_send( + self.group_name, + { + "type": "chat.message", + "message": msg + } + ) + + async def chat_message(self, event): + message = event['message'] + await self.send(text_data=json.dumps({ + "message": message + })) + diff --git a/apps/authentication/websocket/routing.py b/apps/authentication/websocket/routing.py index 039587f..63d26cd 100644 --- a/apps/authentication/websocket/routing.py +++ b/apps/authentication/websocket/routing.py @@ -2,5 +2,5 @@ from django.urls import re_path from apps.authentication.websocket import consumer websocket_urlpatterns = [ - re_path(r"ws/somepath/$", consumer.MyConsumer.as_asgi()), + re_path(r"ws/somepath/$", consumer.SendFromServerConsumer.as_asgi()), ] \ No newline at end of file diff --git a/apps/pos_device/migrations/0002_device_acceptor_device_company_device_latitude_and_more.py b/apps/pos_device/migrations/0002_device_acceptor_device_company_device_latitude_and_more.py new file mode 100644 index 0000000..d6a4374 --- /dev/null +++ b/apps/pos_device/migrations/0002_device_acceptor_device_company_device_latitude_and_more.py @@ -0,0 +1,92 @@ +# Generated by Django 5.0 on 2025-07-21 06:06 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pos_device', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='acceptor', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='device', + name='company', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='devices', to='pos_device.paymentcompany'), + ), + migrations.AddField( + model_name='device', + name='latitude', + field=models.DecimalField(decimal_places=10, max_digits=20, null=True), + ), + migrations.AddField( + model_name='device', + name='longitude', + field=models.DecimalField(decimal_places=10, max_digits=20, null=True), + ), + migrations.AddField( + model_name='device', + name='multi_device', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='device', + name='password', + field=models.CharField(max_length=25, null=True), + ), + migrations.AddField( + model_name='device', + name='serial', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='device', + name='server_in', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='device', + name='terminal', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='paymentcompany', + name='activation', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='paymentcompany', + name='name_en', + field=models.CharField(max_length=250, null=True), + ), + migrations.AddField( + model_name='paymentcompany', + name='name_fa', + field=models.CharField(max_length=250, null=True), + ), + migrations.CreateModel( + name='DeviceVersion', + 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)), + ('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)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/pos_device/models.py b/apps/pos_device/models.py index 35f30f7..6806e06 100644 --- a/apps/pos_device/models.py +++ b/apps/pos_device/models.py @@ -3,10 +3,41 @@ from apps.core.models import BaseModel class PaymentCompany(BaseModel): - pass + name_fa = models.CharField(max_length=250, null=True) + name_en = models.CharField(max_length=250, null=True) + activation = models.BooleanField(default=False) + + def __str__(self): + return f'Payment Company: {self.name_fa}-{self.id}' + + def save(self, *args, **kwargs): + return super(PaymentCompany, self).save(*args, **kwargs) class Device(BaseModel): + acceptor = models.IntegerField(default=0) + terminal = models.IntegerField(default=0) + serial = models.TextField(null=True) + password = models.CharField(max_length=25, null=True) + multi_device = models.BooleanField(default=False) + server_in = models.BooleanField(default=False) + latitude = models.DecimalField(max_digits=20, decimal_places=10, null=True) + longitude = models.DecimalField(max_digits=20, decimal_places=10, null=True) + company = models.ForeignKey( + PaymentCompany, + on_delete=models.CASCADE, + related_name='devices', + null=True + ) + + def __str__(self): + return f'Device: {self.serial} - {self.id}' + + def save(self, *args, **kwargs): + return super(Device, self).save(*args, **kwargs) + + +class DeviceVersion(BaseModel): pass diff --git a/apps/product/migrations/0052_remove_productstats_total_quota_and_more.py b/apps/product/migrations/0052_remove_productstats_total_quota_and_more.py new file mode 100644 index 0000000..2aabd55 --- /dev/null +++ b/apps/product/migrations/0052_remove_productstats_total_quota_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 5.0 on 2025-07-20 08:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0051_quotabrokervalue_value'), + ] + + operations = [ + migrations.RemoveField( + model_name='productstats', + name='total_quota', + ), + migrations.AddField( + model_name='productstats', + name='active_quotas_weight', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='productstats', + name='closed_quotas_weight', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='productstats', + name='quotas_number', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='productstats', + name='total_distributed_weight', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='productstats', + name='total_quota_weight', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='productstats', + name='total_warehouse_entry', + field=models.PositiveBigIntegerField(default=0), + ), + ] diff --git a/apps/product/migrations/0053_attribute_required.py b/apps/product/migrations/0053_attribute_required.py new file mode 100644 index 0000000..183e05a --- /dev/null +++ b/apps/product/migrations/0053_attribute_required.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-07-21 06:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0052_remove_productstats_total_quota_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='attribute', + name='required', + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index b445707..9d9afbf 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -141,8 +141,13 @@ class ProductStats(BaseModel): related_name='stats', null=True ) - total_quota = models.PositiveBigIntegerField(default=0) + 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) @@ -150,7 +155,7 @@ class ProductStats(BaseModel): return f'Product: {self.product.name}-{self.product.id} stats' def save(self, *args, **kwargs): - return super(ProductStats).save(*args, **kwargs) + return super(ProductStats, self).save(*args, **kwargs) class Attribute(BaseModel): @@ -170,7 +175,7 @@ class Attribute(BaseModel): related_name="attributes", null=True ) - + required = models.BooleanField(default=False) is_global = models.BooleanField(default=False) def __str__(self): diff --git a/apps/product/signals.py b/apps/product/signals.py index 94aeefe..bd824c3 100644 --- a/apps/product/signals.py +++ b/apps/product/signals.py @@ -1,8 +1,18 @@ from django.db.models import Sum from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -from .models import QuotaDistribution, Quota, Product -from apps.warehouse.models import InventoryQuotaSaleTransaction +from .models import ( + QuotaDistribution, + Quota, + Product, + ProductStats, + QuotaStats +) +from apps.warehouse.models import ( + InventoryQuotaSaleTransaction, + InventoryEntry +) +from django.db import models def recalculate_remaining_amount(quota): @@ -22,15 +32,113 @@ def update_quota_remaining(sender, instance, **kwargs): def update_product_stats(instance: Product): - pass + """ update all stats of product """ + + if hasattr(instance, 'stats'): + stat = instance.stats + else: + stat = ProductStats.objects.create(product=instance) + + # number of quotas + quotas_count = instance.quotas.filter(is_closed=False).count() # noqa + + # total weight of product that assigned in quota + active_quotas_weight = instance.quotas.filter(is_closed=False).aggregate( + total=models.Sum('quota_weight') + )['total'] or 0 + + closed_quotas_weight = instance.quotas.filter(is_closed=True).aggregate( # noqa + total=models.Sum('quota_weight') + )['total'] or 0 + + total_quotas_weight = instance.quotas.all().aggregate( # noqa + total=models.Sum('quota_weight') + )['total'] or 0 + + # total remaining weight of product quotas + total_remaining_quotas_weight = instance.quotas.filter(is_closed=False).aggregate( # noqa + total=models.Sum('remaining_weight') + )['total'] or 0 + + # product total distributed weight from quota + total_distributed_weight = QuotaDistribution.objects.filter( + quota__product_id=instance.id, + quota__is_closed=False + ).aggregate(total_weight=models.Sum('weight'))['total_weight'] or 0 + + # total sold of product from quota + total_sold = QuotaDistribution.objects.filter( + quota__product_id=instance.id, + quota__is_closed=False + ).aggregate(total_sold=models.Sum('been_sold'))['total_sold'] or 0 + + # total entry from product to inventory + total_warehouse_entry = QuotaDistribution.objects.filter( + quota__product_id=instance.id, + quota__is_closed=False + ).aggregate(total_entry=models.Sum('warehouse_entry'))['total_entry'] or 0 + + stat.quotas_number = quotas_count + stat.active_quotas_weight = active_quotas_weight + stat.closed_quotas_weight = closed_quotas_weight + stat.total_quota_weight = total_quotas_weight + stat.total_remaining = total_remaining_quotas_weight + stat.total_distributed_weight = total_distributed_weight + stat.total_warehouse_entry = total_warehouse_entry + stat.total_sold = total_sold + stat.save(update_fields=[ + "quotas_number", + "active_quotas_weight", + "closed_quotas_weight", + "total_quota_weight", + "total_remaining", + "total_distributed_weight", + "total_warehouse_entry", + "total_sold", + ]) def update_quota_stats(instance: Quota): - pass + """ update all stats of quota """ + + if hasattr(instance, 'stats'): + stat = instance.stats + else: + stat = QuotaStats.objects.create(quota=instance) + + total_distributed = instance.quota_distributed + total_remaining = instance.remaining_weight + + # total entry to inventory from quota + total_inventory = InventoryEntry.objects.filter( + distribution__quota=instance + ).aggregate( + total_inventory=models.Sum('weight') + )['total_inventory'] or 0 + + # total sale of distributions from quota + total_sale = instance.distributions_assigned.filter( + quota__is_closed=False + ).aggregate(total_sale=models.Sum('been_sold'))['total_sale'] or 0 + + stat.total_distributed = total_distributed + stat.remaining = total_remaining + stat.total_inventory = total_inventory + stat.total_sale = total_sale + stat.save(update_fields=[ + "total_distributed", + "remaining", + "total_inventory", + "total_sale", + ]) @receiver([post_save, post_delete], sender=QuotaDistribution) @receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction) def update_stats_on_change(sender, instance, **kwargs): - update_product_stats(instance) - update_quota_stats(instance) + if sender == QuotaDistribution: + update_product_stats(instance.quota.product) + update_quota_stats(instance.quota) + elif sender == InventoryQuotaSaleTransaction: + update_product_stats(instance.quota_distribution.quota.product) + update_quota_stats(instance.quota_distribution.quota) diff --git a/apps/warehouse/migrations/0012_inventoryentry_balance.py b/apps/warehouse/migrations/0012_inventoryentry_balance.py new file mode 100644 index 0000000..3d2d370 --- /dev/null +++ b/apps/warehouse/migrations/0012_inventoryentry_balance.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-07-20 08:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('warehouse', '0011_inventoryquotasaletransaction'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryentry', + name='balance', + field=models.PositiveBigIntegerField(default=0), + ), + ] diff --git a/apps/warehouse/models.py b/apps/warehouse/models.py index 84c8763..1f6a568 100644 --- a/apps/warehouse/models.py +++ b/apps/warehouse/models.py @@ -18,7 +18,7 @@ class InventoryEntry(BaseModel): null=True ) weight = models.PositiveBigIntegerField(default=0) - balance = models + balance = models.PositiveBigIntegerField(default=0) 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)