diff --git a/apps/authentication/signals.py b/apps/authentication/signals.py index f2fce1e..5a1cf5e 100644 --- a/apps/authentication/signals.py +++ b/apps/authentication/signals.py @@ -3,6 +3,7 @@ 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 rest_framework.exceptions import APIException from django.dispatch import receiver @@ -38,6 +39,12 @@ def update_organization_stats(instance: Organization): @receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction) def organization_stats(sender, instance, **kwargs): if sender == QuotaDistribution: - update_organization_stats(instance.assigned_organization) + if instance.assigned_organization: + update_organization_stats(instance.assigned_organization) + else: + print("assigned_organization is null") elif sender == InventoryQuotaSaleTransaction: - update_organization_stats(instance.inventory_entry.organization) + if instance.inventory_entry: + update_organization_stats(instance.inventory_entry.organization) + else: + print("inventory_entry is null - authentication app signals") diff --git a/apps/product/signals.py b/apps/product/signals.py index 68e42b5..992c5e9 100644 --- a/apps/product/signals.py +++ b/apps/product/signals.py @@ -244,5 +244,8 @@ def update_stats_on_change(sender, instance, **kwargs): update_product_stats(instance.quota.product, instance) update_quota_stats(instance.quota) elif sender == InventoryQuotaSaleTransaction: - update_product_stats(instance.quota_distribution.quota.product, instance.quota_distribution) - update_quota_stats(instance.quota_distribution.quota) + if instance.quota_distribution: + update_product_stats(instance.quota_distribution.quota.product, instance.quota_distribution) + update_quota_stats(instance.quota_distribution.quota) + else: + print("quota distribution is null - product app signal") diff --git a/apps/warehouse/migrations/0017_inventoryquotasaleitem.py b/apps/warehouse/migrations/0017_inventoryquotasaleitem.py new file mode 100644 index 0000000..1367765 --- /dev/null +++ b/apps/warehouse/migrations/0017_inventoryquotasaleitem.py @@ -0,0 +1,38 @@ +# Generated by Django 5.0 on 2025-09-09 08:55 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0072_alter_quota_base_price_cooperative_and_more'), + ('warehouse', '0016_inventoryquotasaletransaction_additional_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='InventoryQuotaSaleItem', + 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)), + ('unit_price', models.PositiveBigIntegerField(default=0)), + ('total_price', models.PositiveBigIntegerField(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)), + ('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_items', to='product.product')), + ('transaction', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='items', to='warehouse.inventoryquotasaletransaction')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/warehouse/migrations/0018_remove_inventoryquotasaletransaction_product_and_more.py b/apps/warehouse/migrations/0018_remove_inventoryquotasaletransaction_product_and_more.py new file mode 100644 index 0000000..4270380 --- /dev/null +++ b/apps/warehouse/migrations/0018_remove_inventoryquotasaletransaction_product_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0 on 2025-09-09 08:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('warehouse', '0017_inventoryquotasaleitem'), + ] + + operations = [ + migrations.RemoveField( + model_name='inventoryquotasaletransaction', + name='product', + ), + migrations.RemoveField( + model_name='inventoryquotasaletransaction', + name='weight', + ), + ] diff --git a/apps/warehouse/migrations/0019_inventoryquotasaleitem_quota_distribution_and_more.py b/apps/warehouse/migrations/0019_inventoryquotasaleitem_quota_distribution_and_more.py new file mode 100644 index 0000000..8f2a88c --- /dev/null +++ b/apps/warehouse/migrations/0019_inventoryquotasaleitem_quota_distribution_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0 on 2025-09-09 11:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0072_alter_quota_base_price_cooperative_and_more'), + ('warehouse', '0018_remove_inventoryquotasaletransaction_product_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryquotasaleitem', + name='quota_distribution', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sale_items', to='product.quotadistribution'), + ), + migrations.AddField( + model_name='inventoryquotasaletransaction', + name='items_total_weight', + field=models.PositiveBigIntegerField(default=0), + ), + ] diff --git a/apps/warehouse/migrations/0020_remove_inventoryquotasaletransaction_items_total_weight.py b/apps/warehouse/migrations/0020_remove_inventoryquotasaletransaction_items_total_weight.py new file mode 100644 index 0000000..610a724 --- /dev/null +++ b/apps/warehouse/migrations/0020_remove_inventoryquotasaletransaction_items_total_weight.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0 on 2025-09-09 11:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('warehouse', '0019_inventoryquotasaleitem_quota_distribution_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='inventoryquotasaletransaction', + name='items_total_weight', + ), + ] diff --git a/apps/warehouse/migrations/0021_inventoryquotasaletransaction_weight.py b/apps/warehouse/migrations/0021_inventoryquotasaletransaction_weight.py new file mode 100644 index 0000000..8d152c5 --- /dev/null +++ b/apps/warehouse/migrations/0021_inventoryquotasaletransaction_weight.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-09-09 11:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('warehouse', '0020_remove_inventoryquotasaletransaction_items_total_weight'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryquotasaletransaction', + name='weight', + field=models.PositiveBigIntegerField(default=0), + ), + ] diff --git a/apps/warehouse/models.py b/apps/warehouse/models.py index 935ac38..5699924 100644 --- a/apps/warehouse/models.py +++ b/apps/warehouse/models.py @@ -88,14 +88,8 @@ class InventoryQuotaSaleTransaction(BaseModel): related_name='inventory_sales', null=True ) - weight = models.DecimalField(max_digits=12, decimal_places=2, null=True) + weight = models.PositiveBigIntegerField(default=0) 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) price_paid = models.PositiveBigIntegerField(default=0) type_of_price = ( @@ -121,17 +115,43 @@ class InventoryQuotaSaleTransaction(BaseModel): payer_cart = models.CharField(max_length=50, null=True) additional = models.JSONField(default=dict) - 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 + @property + def total_weight(self): + """ summation of total sold product weight """ + return sum(item.weight for item in self.items.all) 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) + + +class InventoryQuotaSaleItem(BaseModel): + transaction = models.ForeignKey( + InventoryQuotaSaleTransaction, + on_delete=models.CASCADE, + related_name='items', + null=True + ) + quota_distribution = models.ForeignKey( + product_models.QuotaDistribution, + on_delete=models.CASCADE, + related_name='sale_items', + null=True + ) + product = models.ForeignKey( + product_models.Product, + on_delete=models.CASCADE, + related_name='sale_items', + null=True + ) + weight = models.PositiveBigIntegerField(default=0) + unit_price = models.PositiveBigIntegerField(default=0) + total_price = models.PositiveBigIntegerField(default=0) + + def __str__(self): + return f'Item {self.product} - {self.weight} Kg - {self.total_price}' + + def save(self, *args, **kwargs): + return super(InventoryQuotaSaleItem, self).save(*args, **kwargs) diff --git a/apps/warehouse/pos/api/v1/api.py b/apps/warehouse/pos/api/v1/api.py index 5ad9b29..d54d074 100644 --- a/apps/warehouse/pos/api/v1/api.py +++ b/apps/warehouse/pos/api/v1/api.py @@ -84,7 +84,7 @@ class InventoryEntryViewSet(viewsets.ModelViewSet, DynamicSearchMixin, POSDevice class InventoryQuotaSaleTransactionViewSet(viewsets.ModelViewSet, DynamicSearchMixin, POSDeviceMixin): - queryset = warehouse_models.InventoryQuotaSaleTransaction.objects.all() + queryset = warehouse_models.InventoryQuotaSaleTransaction.objects.all().prefetch_related('items') serializer_class = warehouse_serializers.InventoryQuotaSaleTransactionSerializer permission_classes = [AllowAny] search_fields = [ diff --git a/apps/warehouse/pos/api/v1/serializers.py b/apps/warehouse/pos/api/v1/serializers.py index f28238f..c5f6722 100644 --- a/apps/warehouse/pos/api/v1/serializers.py +++ b/apps/warehouse/pos/api/v1/serializers.py @@ -8,11 +8,12 @@ from apps.product.services.services import ( from apps.pos_device.services.services import pos_organizations_sharing_information from apps.pos_device.pos.api.v1.serializers.device import DeviceSerializer from apps.herd.pos.api.v1.serializers import RancherSerializer +from apps.warehouse import models as warehouse_models from apps.warehouse.exceptions import ( TotalInventorySaleException ) -from apps.warehouse import models as warehouse_models from rest_framework import serializers +from apps.herd.models import Rancher from django.db import models @@ -95,25 +96,62 @@ class InventoryEntrySerializer(serializers.ModelSerializer): class InventoryQuotaSaleTransactionSerializer(serializers.ModelSerializer): + rancher_national_code = serializers.CharField(max_length=50, required=False) class Meta: # noqa model = warehouse_models.InventoryQuotaSaleTransaction fields = '__all__' depth = 0 + def create(self, validated_data): + items_data = self.context['request'].data['items'] + + # get rancher with national code + rancher = Rancher.objects.get(national_code=validated_data.pop('rancher_national_code')) + validated_data.update({'rancher': rancher}) + + # if transaction exists, update transaction status + transaction = self.Meta.model.objects.filter( + transaction_id=validated_data.get('transaction_id') + ) + if transaction.exists(): + obj = transaction.first() + obj.transaction_status = validated_data.get('transaction_status') + obj.save(update_fields=['transaction_status']) + + return obj + + # create transaction record + transaction = warehouse_models.InventoryQuotaSaleTransaction.objects.create( + **validated_data + ) + + # calculate total price of product items in shopping cart + total_price = 0 + for item_data in items_data: + item = warehouse_models.InventoryQuotaSaleItem.objects.create( + transaction=transaction, + **item_data + ) + total_price += item.total_price + transaction.transaction_price = total_price + transaction.save() + + return transaction + 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'] + if 'quota_distribution' in attrs.keys(): + distribution = attrs['quota_distribution'] - total_sale_weight = inventory_entry.inventory_sales.aggregate( - total=models.Sum('weight') - )['total'] or 0 + total_sale_weight = distribution.inventory_sales.aggregate( + total=models.Sum('weight') + )['total'] or 0 - if total_sale_weight + attrs['weight'] > distribution.warehouse_balance: - raise TotalInventorySaleException() + if total_sale_weight + attrs['weight'] > distribution.warehouse_balance: + raise TotalInventorySaleException() return attrs @@ -122,10 +160,29 @@ class InventoryQuotaSaleTransactionSerializer(serializers.ModelSerializer): representation = super().to_representation(instance) - representation['rancher'] = RancherSerializer(instance.rancher).data + if instance.rancher: + representation['rancher'] = RancherSerializer(instance.rancher).data representation['pos_device'] = DeviceSerializer(instance.pos_device).data - representation['seller_organization'] = instance.seller_organization.name - representation['inventory_entry'] = InventoryEntrySerializer(instance.inventory_entry).data + if instance.seller_organization: + representation['seller_organization'] = instance.seller_organization.name + if instance.inventory_entry: + representation['inventory_entry'] = InventoryEntrySerializer(instance.inventory_entry).data return representation + +class InventoryQuotaSaleItemSerializer(serializers.ModelSerializer): + product_name = serializers.CharField(source='product.name', read_only=True) + + class Meta: + model = warehouse_models.InventoryQuotaSaleItem + fields = [ + 'id', + "transaction", + "product", + "product_name", + "weight", + "unit_price", + "total_price", + + ] diff --git a/apps/warehouse/signals.py b/apps/warehouse/signals.py index 95692df..5b5f649 100644 --- a/apps/warehouse/signals.py +++ b/apps/warehouse/signals.py @@ -34,4 +34,7 @@ def update_distribution_warehouse_entry(sender, instance, **kwargs): @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) + if instance.quota_distribution: + warehouse_sold_and_balance(instance.quota_distribution) + else: + print("quota distribution is null - warehouse app signals")