From 72bcb40f69aaaa8bc1ee866e0132012dcc11c991 Mon Sep 17 00:00:00 2001 From: Deepthi Damruwan Date: Fri, 9 May 2025 04:37:09 +0530 Subject: [PATCH 1/4] v 2.0 ( updated model ) --- .../management/commands/populate_all_data.py | 68 +-- warehouse_managment/warehouse/admin.py | 9 +- ...warehouseinventory_supplier_id_and_more.py | 30 ++ warehouse_managment/warehouse/models.py | 11 +- warehouse_managment/warehouse/serializers.py | 7 +- warehouse_managment/warehouse/urls.py | 10 + .../warehouse/utils/mock_orders.py | 23 + .../warehouse/utils/order_accept_Req.py | 10 + .../warehouse/{ => utils}/supplier_names.py | 0 warehouse_managment/warehouse/views.py | 415 ++++++++++++++---- 10 files changed, 466 insertions(+), 117 deletions(-) create mode 100644 warehouse_managment/warehouse/migrations/0003_remove_warehouseinventory_supplier_id_and_more.py create mode 100644 warehouse_managment/warehouse/utils/mock_orders.py create mode 100644 warehouse_managment/warehouse/utils/order_accept_Req.py rename warehouse_managment/warehouse/{ => utils}/supplier_names.py (100%) diff --git a/warehouse_managment/product/management/commands/populate_all_data.py b/warehouse_managment/product/management/commands/populate_all_data.py index b79146d..b6b69bf 100644 --- a/warehouse_managment/product/management/commands/populate_all_data.py +++ b/warehouse_managment/product/management/commands/populate_all_data.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand from product.models import ProductCategory, Product, SupplierProduct -from warehouse.models import Warehouse, WarehouseInventory, InventoryTransaction +from warehouse.models import Warehouse, WarehouseInventory, InventoryTransaction, WarehouseSupplier from decimal import Decimal from django.utils import timezone import random @@ -13,12 +13,13 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): self.stdout.write("๐Ÿงน Deleting existing data...") - InventoryTransaction.objects.all().delete() - WarehouseInventory.objects.all().delete() - SupplierProduct.objects.all().delete() - Product.objects.all().delete() - ProductCategory.objects.all().delete() - Warehouse.objects.all().delete() + # InventoryTransaction.objects.all().delete() + # WarehouseInventory.objects.all().delete() + # SupplierProduct.objects.all().delete() + # Product.objects.all().delete() + # ProductCategory.objects.all().delete() + # WarehouseSupplier.objects.all().delete() + # Warehouse.objects.all().delete() self.stdout.write("๐Ÿงช Populating new data...") @@ -34,7 +35,7 @@ def handle(self, *args, **kwargs): # Products products = [] - for i in range(10): # more products + for i in range(10): category = random.choice(categories) product = Product.objects.create( product_SKU=f"SKU{i + 1:03}", @@ -44,7 +45,7 @@ def handle(self, *args, **kwargs): ) products.append(product) - # Warehouses + # Warehouses (Randomly assigning warehouses to suppliers) warehouse_data = [ ("Colombo Central", "6.9271ยฐ N", "79.8612ยฐ E"), ("Kandy Depot", "7.2906ยฐ N", "80.6337ยฐ E"), @@ -56,39 +57,52 @@ def handle(self, *args, **kwargs): warehouse_name=name, location_x=x, location_y=y, - capacity=Decimal("1000000.00") + capacity=Decimal("100000000.00") ) warehouses.append(warehouse) - # SupplierProduct, WarehouseInventory, and InventoryTransaction + # SupplierProduct, WarehouseInventory, InventoryTransaction, WarehouseSupplier + created_pairs = set() + for product in products: - for supplier_id in [101, 102, 103]: - # SupplierProduct - max_capacity = random.randint(300000, 600000) - lead_time = random.randint(3, 10) - SupplierProduct.objects.create( - supplier_id=supplier_id, - product=product, - maximum_capacity=max_capacity, - supplier_price=round(random.uniform(80, 1500), 2), - lead_time_days=lead_time - ) + # Randomly assign suppliers (not for all suppliers) + suppliers = random.sample([101, 102, 103], k=random.randint(1, 3)) # Randomly pick 1 to 3 suppliers + for supplier_id in suppliers: + # SupplierProduct (Randomly create it for some suppliers) + if random.choice([True, False]): + max_capacity = random.randint(300000, 600000) + lead_time = random.randint(3, 10) + SupplierProduct.objects.create( + supplier_id=supplier_id, + product=product, + maximum_capacity=max_capacity, + supplier_price=round(random.uniform(80, 1500), 2), + lead_time_days=lead_time + ) + + # WarehouseSupplier (Randomly assign warehouses for this supplier) + warehouse = random.choice(warehouses) # Randomly pick one warehouse for this supplier + if (warehouse.id, supplier_id) not in created_pairs: + WarehouseSupplier.objects.create( + warehouse=warehouse, + supplier_id=supplier_id + ) + created_pairs.add((warehouse.id, supplier_id)) - # For each warehouse, create inventory & transactions - for warehouse in warehouses: + # Inventory - ensure unique product_id and warehouse_id combination + if not WarehouseInventory.objects.filter(warehouse=warehouse, product=product).exists(): quantity = Decimal(random.uniform(100000, 400000)) last_restocked = timezone.now() - timedelta(days=random.randint(1, 60)) inventory = WarehouseInventory.objects.create( warehouse=warehouse, product=product, - supplier_id=supplier_id, quantity=quantity, last_restocked=last_restocked, minimum_stock_level=Decimal("100000.00") ) - # Create 1-2 incoming and 1 outgoing transactions per inventory + # Transactions for _ in range(random.randint(1, 2)): qty_in = Decimal(random.uniform(10000, 50000)) InventoryTransaction.objects.create( @@ -111,4 +125,4 @@ def handle(self, *args, **kwargs): created_by="System" ) - self.stdout.write(self.style.SUCCESS("โœ… Successfully seeded all data including inventory and transactions!")) + self.stdout.write(self.style.SUCCESS("โœ… Successfully seeded all data!!!")) diff --git a/warehouse_managment/warehouse/admin.py b/warehouse_managment/warehouse/admin.py index 49d5c8c..faea785 100644 --- a/warehouse_managment/warehouse/admin.py +++ b/warehouse_managment/warehouse/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Warehouse, WarehouseInventory, InventoryTransaction +from .models import Warehouse, WarehouseInventory, InventoryTransaction, WarehouseSupplier @admin.register(Warehouse) class WarehouseAdmin(admin.ModelAdmin): @@ -7,10 +7,15 @@ class WarehouseAdmin(admin.ModelAdmin): @admin.register(WarehouseInventory) class WarehouseInventoryAdmin(admin.ModelAdmin): - list_display = ('id', 'warehouse', 'product', 'supplier_id', 'quantity', 'last_restocked', 'minimum_stock_level') + list_display = ('id', 'warehouse', 'product', 'quantity', 'last_restocked', 'minimum_stock_level') list_filter = ('warehouse', 'product') @admin.register(InventoryTransaction) class InventoryTransactionAdmin(admin.ModelAdmin): list_display = ('id', 'inventory', 'transaction_type', 'quantity_change', 'created_at', 'created_by') list_filter = ('transaction_type', 'created_by') + +@admin.register(WarehouseSupplier) +class WarehouseSupplierAdmin(admin.ModelAdmin): + list_display = ('id', 'warehouse', 'supplier_id') + list_filter = ('warehouse',) diff --git a/warehouse_managment/warehouse/migrations/0003_remove_warehouseinventory_supplier_id_and_more.py b/warehouse_managment/warehouse/migrations/0003_remove_warehouseinventory_supplier_id_and_more.py new file mode 100644 index 0000000..3b890fa --- /dev/null +++ b/warehouse_managment/warehouse/migrations/0003_remove_warehouseinventory_supplier_id_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2 on 2025-05-08 19:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('warehouse', '0002_warehouse_capacity'), + ] + + operations = [ + migrations.RemoveField( + model_name='warehouseinventory', + name='supplier_id', + ), + migrations.CreateModel( + name='WarehouseSupplier', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('supplier_id', models.IntegerField()), + ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='warehouse.warehouse')), + ], + options={ + 'db_table': 'warehouse_supplier', + 'unique_together': {('warehouse', 'supplier_id')}, + }, + ), + ] diff --git a/warehouse_managment/warehouse/models.py b/warehouse_managment/warehouse/models.py index 2fbd761..afae8e2 100644 --- a/warehouse_managment/warehouse/models.py +++ b/warehouse_managment/warehouse/models.py @@ -1,5 +1,3 @@ -from django.db import models - from django.db import models from product.models import Product @@ -16,7 +14,6 @@ class Meta: class WarehouseInventory(models.Model): warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE) - supplier_id = models.IntegerField() quantity = models.DecimalField(max_digits=10, decimal_places=2) last_restocked = models.DateTimeField(blank=True, null=True) minimum_stock_level = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) @@ -49,3 +46,11 @@ class InventoryTransaction(models.Model): class Meta: db_table = 'inventory_transactions' +class WarehouseSupplier(models.Model): + warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE) + supplier_id = models.IntegerField() + + class Meta: + db_table = 'warehouse_supplier' + unique_together = (('warehouse', 'supplier_id'),) + diff --git a/warehouse_managment/warehouse/serializers.py b/warehouse_managment/warehouse/serializers.py index 019bb97..bdad607 100644 --- a/warehouse_managment/warehouse/serializers.py +++ b/warehouse_managment/warehouse/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Warehouse, WarehouseInventory, InventoryTransaction +from .models import Warehouse, WarehouseInventory, InventoryTransaction, WarehouseSupplier class WarehouseSerializer(serializers.ModelSerializer): class Meta: @@ -15,3 +15,8 @@ class InventoryTransactionSerializer(serializers.ModelSerializer): class Meta: model = InventoryTransaction fields = '__all__' + +class WarehouseSupplierSerializer(serializers.ModelSerializer): + class Meta: + model = WarehouseSupplier + fields = ['id', 'name', 'location'] diff --git a/warehouse_managment/warehouse/urls.py b/warehouse_managment/warehouse/urls.py index ce5ad16..dfb89fd 100644 --- a/warehouse_managment/warehouse/urls.py +++ b/warehouse_managment/warehouse/urls.py @@ -7,6 +7,8 @@ supplier_dashboard, get_suppliers_by_category, warehouse_inventory_list, + order_inventory_summary, + handle_order_status, ) urlpatterns = [ @@ -34,4 +36,12 @@ # GET suppliers by category path('suppliers-by-category', get_suppliers_by_category, name='suppliers-by-category'), + + # GET Product counts and names by orders + path('order-inventory-summary/', order_inventory_summary), + + # POST endpoint to accept or reject an order request and update inventory + path('order/handle/', handle_order_status, name='handle_order_status'), ] + + diff --git a/warehouse_managment/warehouse/utils/mock_orders.py b/warehouse_managment/warehouse/utils/mock_orders.py new file mode 100644 index 0000000..5ba1587 --- /dev/null +++ b/warehouse_managment/warehouse/utils/mock_orders.py @@ -0,0 +1,23 @@ +ORDERS = [ + { + "order_id": "ORD001", + "products": [ + {"product_id": 1, "product_count": 100}, + {"product_id": 3, "product_count": 50} + ] + }, + { + "order_id": "ORD002", + "products": [ + {"product_id": 5, "product_count": 200} + ] + }, + { + "order_id": "ORD003", + "products": [ + {"product_id": 2, "product_count": 150}, + {"product_id": 1, "product_count": 60}, + {"product_id": 6, "product_count": 30} + ] + } +] diff --git a/warehouse_managment/warehouse/utils/order_accept_Req.py b/warehouse_managment/warehouse/utils/order_accept_Req.py new file mode 100644 index 0000000..be700b9 --- /dev/null +++ b/warehouse_managment/warehouse/utils/order_accept_Req.py @@ -0,0 +1,10 @@ +ORDER_REQUEST = { + "warehouse_id": 1, + "order_id": "ORD109", + "status": "accepted", # or "rejected" + "products": [ + {"product_name": "Chili Product 1", "product_count": 4460}, + {"product_name": "Cinnamon Product 2", "product_count": 68}, + {"product_name": "Chili Product 5", "product_count": 81}, + ] +} diff --git a/warehouse_managment/warehouse/supplier_names.py b/warehouse_managment/warehouse/utils/supplier_names.py similarity index 100% rename from warehouse_managment/warehouse/supplier_names.py rename to warehouse_managment/warehouse/utils/supplier_names.py diff --git a/warehouse_managment/warehouse/views.py b/warehouse_managment/warehouse/views.py index 538457e..fcc5a58 100644 --- a/warehouse_managment/warehouse/views.py +++ b/warehouse_managment/warehouse/views.py @@ -3,14 +3,16 @@ from rest_framework import status from django.utils import timezone from decimal import Decimal -import requests +from collections import defaultdict from product.models import Product, SupplierProduct, ProductCategory -from django.db.models import Sum -from .models import Warehouse, WarehouseInventory, InventoryTransaction -from .supplier_names import SUPPLIER_NAME_MAP +from django.db.models import Sum, F +from django.db import transaction +from .models import Warehouse, WarehouseInventory, InventoryTransaction, WarehouseSupplier +from .utils.supplier_names import SUPPLIER_NAME_MAP +from .utils.mock_orders import ORDERS +from .utils.order_accept_Req import ORDER_REQUEST from .serializers import ( WarehouseSerializer, - WarehouseInventorySerializer, InventoryTransactionSerializer, ) @@ -20,10 +22,10 @@ def warehouse_list(request): serializer = WarehouseSerializer(warehouses, many=True) return Response(serializer.data) + @api_view(['GET']) def warehouse_inventory_list(request): warehouse_id = request.query_params.get('warehouse_id') - if not warehouse_id: return Response({"error": "warehouse_id is required"}, status=400) @@ -32,34 +34,60 @@ def warehouse_inventory_list(request): except Warehouse.DoesNotExist: return Response({"error": "Warehouse not found"}, status=404) - inventory = WarehouseInventory.objects.filter(warehouse_id=warehouse_id) - - current_stock_level = inventory.aggregate(total=Sum('quantity'))['total'] or 0 - minimum_stock_level = inventory.aggregate(min_level=Sum('minimum_stock_level'))['min_level'] or 0 - last_restocked = inventory.order_by('-last_restocked').values_list('last_restocked', flat=True).first() - + supplier_ids = WarehouseSupplier.objects.filter(warehouse_id=warehouse_id).values_list('supplier_id', flat=True) + inventory_qs = WarehouseInventory.objects.filter(warehouse_id=warehouse_id).select_related('product', 'product__category') + + supplier_products = SupplierProduct.objects.filter( + supplier_id__in=supplier_ids, + product_id__in=inventory_qs.values_list('product_id', flat=True) + ).values('product_id', 'supplier_id') + + product_supplier_map = defaultdict(set) + for sp in supplier_products: + supplier_id = sp['supplier_id'] + supplier_name = SUPPLIER_NAME_MAP.get(supplier_id, f"Supplier {supplier_id}") + product_supplier_map[sp['product_id']].add(supplier_name) + + product_data = defaultdict(lambda: { + "product_name": "", + "category": "", + "product_count": 0, + "supplied_by": set(), + "supplied_date": None + }) + + for item in inventory_qs: + product_id = item.product.id + data = product_data[product_id] + + data["product_name"] = item.product.product_name + data["category"] = item.product.category.category_name + data["product_count"] += float(item.quantity) + data["supplied_by"].update(product_supplier_map.get(product_id, set())) + + if not data["supplied_date"] or item.created_at.date() > data["supplied_date"]: + data["supplied_date"] = item.created_at.date() inventory_product_details = [] - for item in inventory: + for data in product_data.values(): inventory_product_details.append({ - "product_name": item.product.product_name, - "category": item.product.category.category_name, - "supplied_by": SUPPLIER_NAME_MAP.get(item.supplier_id, f"Supplier {item.supplier_id}"), - "supplied_date": item.created_at.date() if item.created_at else None, - "product_count": int(item.quantity), + "product_name": data["product_name"], + "category": data["category"], + "supplied_by": ", ".join(sorted(data["supplied_by"])), + "supplied_date": data["supplied_date"], + "product_count": round(data["product_count"], 2) }) result = { "warehouse_city": warehouse.warehouse_name, - "minimum_stock_level": float(minimum_stock_level), - "last_restocked": last_restocked.date() if last_restocked else None, - "current_stock_level": float(current_stock_level), + "capacity": float(warehouse.capacity), + "last_restocked": inventory_qs.order_by('-last_restocked').values_list('last_restocked', flat=True).first().date() if inventory_qs.exists() else None, + "current_stock_level": round(inventory_qs.aggregate(total=Sum('quantity'))['total'] or 0, 2), "inventory_product_details": inventory_product_details } return Response(result, status=200) - @api_view(['GET']) def transaction_list(request, warehouse_id): transactions = InventoryTransaction.objects.all() @@ -70,51 +98,64 @@ def transaction_list(request, warehouse_id): return Response(serializer.data) -# 4. Supplier Dashboard @api_view(['GET']) def supplier_dashboard(request): supplier_id = request.query_params.get('supplier_id') - warehouse_id = request.query_params.get('warehouse_id') - if not supplier_id: return Response({"error": "supplier_id required"}, status=400) - inventory = WarehouseInventory.objects.filter(supplier_id=supplier_id) - if warehouse_id: - inventory = inventory.filter(warehouse_id=warehouse_id) - - summary = [] - for item in inventory: - summary.append({ - "product": item.product.product_name, - "quantity": float(item.quantity), - "last_restocked": item.last_restocked, - }) - return Response(summary) + warehouses = WarehouseSupplier.objects.filter(supplier_id=supplier_id).values_list('warehouse_id', flat=True) + + inventories = ( + WarehouseInventory.objects + .filter(warehouse_id__in=warehouses) + .select_related('product', 'warehouse') + .filter(product__supplierproduct__supplier_id=supplier_id) + .values( + 'product__product_name', + 'product__product_SKU', + 'warehouse__warehouse_name' + ) + .annotate(total_quantity=Sum('quantity')) + ) + summary = [ + { + "product_name": item['product__product_name'], + "SKU": item['product__product_SKU'], + "warehouse": item['warehouse__warehouse_name'], + "Quantity_on_hand": float(item['total_quantity']), + } + for item in inventories + ] + + return Response(summary) @api_view(['POST']) def mark_delivery_received(request): - supplier_id = request.query_params.get('supplier_id') - product_id = request.query_params.get('product_id') - warehouse_id = request.query_params.get('warehouse_id') - quantity = request.query_params.get('quantity') + supplier_id = request.data.get('supplier_id') + product_id = request.data.get('product_id') + warehouse_id = request.data.get('warehouse_id') + quantity = request.data.get('quantity') - if not all([supplier_id, product_id, quantity, warehouse_id]): - return Response({'error': 'Missing fields'}, status=400) + if not all([supplier_id, product_id, warehouse_id, quantity]): + return Response({'error': 'Missing required fields'}, status=400) try: - inventory = WarehouseInventory.objects.get( - supplier_id=supplier_id, - product_id=product_id, - warehouse_id=warehouse_id - ) - except WarehouseInventory.DoesNotExist: - return Response({'error': 'Inventory not found'}, status=404) + quantity = Decimal(quantity) + except: + return Response({'error': 'Invalid quantity format'}, status=400) + + inventory, created = WarehouseInventory.objects.get_or_create( + product_id=product_id, + warehouse_id=warehouse_id, + defaults={'quantity': quantity, 'last_restocked': timezone.now()} + ) - inventory.quantity += Decimal(quantity) - inventory.last_restocked = timezone.now() - inventory.save() + if not created: + inventory.quantity += quantity + inventory.last_restocked = timezone.now() + inventory.save() InventoryTransaction.objects.create( inventory=inventory, @@ -124,72 +165,278 @@ def mark_delivery_received(request): created_by=f"Supplier {supplier_id}" ) - supplier_product, created = SupplierProduct.objects.get_or_create( + sp, created = SupplierProduct.objects.get_or_create( supplier_id=supplier_id, product_id=product_id, defaults={"maximum_capacity": inventory.quantity} ) if not created: - supplier_product.maximum_capacity = inventory.quantity - supplier_product.save() + sp.maximum_capacity = max(sp.maximum_capacity, inventory.quantity) + sp.save() - return Response({"status": "Delivery recorded and product updated"}, status=200) + return Response({"status": "Delivery recorded and inventory updated"}) -@api_view(['GET']) -def get_supplier_products(request, supplier_id): - inventory = WarehouseInventory.objects.filter(supplier_id=supplier_id) - if not inventory.exists(): - return Response([], status=status.HTTP_200_OK) - result = [] - product_ids = inventory.values_list('product_id', flat=True).distinct() +@api_view(['GET']) +def get_supplier_products(request, supplier_id): + warehouse_ids = WarehouseSupplier.objects.filter(supplier_id=supplier_id).values_list('warehouse_id', flat=True) - for product_id in product_ids: - product = Product.objects.filter(id=product_id).first() - supplier_product = SupplierProduct.objects.filter(product_id=product_id, supplier_id=supplier_id).first() - if not product: - continue + inventory_qs = WarehouseInventory.objects.filter(warehouse_id__in=warehouse_ids) - stock_level = inventory.filter(product_id=product_id).aggregate( - total=Sum('quantity') - )['total'] or 0 + product_ids = inventory_qs.values_list('product_id', flat=True).distinct() + products = ( + Product.objects + .filter(id__in=product_ids) + .select_related('category') + .prefetch_related('supplierproduct_set') + ) - supplier_product = SupplierProduct.objects.filter( - product=product, supplier_id=supplier_id - ).first() - lead_time_days = supplier_product.lead_time_days if supplier_product else None + stock_map = { + pid: inventory_qs.filter(product_id=pid).aggregate(total=Sum('quantity'))['total'] or 0 + for pid in product_ids + } + result = [] + for product in products: + sp = product.supplierproduct_set.filter(supplier_id=supplier_id).first() result.append({ "id": product.id, "name": product.product_name, "supplier_id": supplier_id, - "lead_time_days": lead_time_days, - "stock_level": int(stock_level), + "lead_time_days": sp.lead_time_days if sp else None, + "stock_level": int(stock_map.get(product.id, 0)), }) - return Response(result, status=status.HTTP_200_OK) + return Response(result) + @api_view(['GET']) def get_suppliers_by_category(request): category_name = request.query_params.get('category') - if not category_name: return Response({"error": "Category parameter is required."}, status=400) try: category = ProductCategory.objects.get(category_name__iexact=category_name) except ProductCategory.DoesNotExist: - return Response({"supplier_ids": []}, status=200) + return Response({"supplier_ids": []}) product_ids = Product.objects.filter(category=category).values_list('id', flat=True) - supplier_ids = WarehouseInventory.objects.filter( + supplier_ids = SupplierProduct.objects.filter(product_id__in=product_ids).values_list('supplier_id', flat=True).distinct() + + return Response({"supplier_ids": list(supplier_ids)}) + + + +@api_view(['GET']) +def order_inventory_summary(request): + warehouse_id = request.query_params.get('warehouse_id') + if not warehouse_id: + return Response({"error": "warehouse_id is required"}, status=400) + + product_ids = {p["product_id"] for order in ORDERS for p in order["products"]} + + products = Product.objects.filter(id__in=product_ids).in_bulk() + product_id_to_name = {pid: prod.product_name for pid, prod in products.items()} + + enriched_orders = [ + { + "order_id": order["order_id"], + "products": [ + { + "product_name": product_id_to_name.get(p["product_id"], "Unknown Product"), + "product_count": p["product_count"] + } for p in order["products"] + ] + } for order in ORDERS + ] + + inventory_summary_qs = WarehouseInventory.objects.filter( + warehouse_id=warehouse_id, + product_id__in=product_ids + ).values('product_id').annotate(available_count=Sum('quantity')) + + inventory_summary = [ + { + "product_name": product_id_to_name.get(row["product_id"], "Unknown Product"), + "available_count": int(row["available_count"]) + } for row in inventory_summary_qs + ] + + return Response({ + "orders": enriched_orders, + "inventory": inventory_summary + }) + + + +"""pip install httpx + + +import httpx +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework import status +from django.db.models import Sum +from product.models import Product +from warehouse.models import WarehouseInventory + +ORDER_SERVICE_URL = "http://order-service/api/v0/orders/warehouse/{warehouse_id}?minimal=True" + +@api_view(['GET']) +def order_inventory_summary(request): + warehouse_id = request.query_params.get('warehouse_id') + if not warehouse_id: + return Response({"error": "Missing warehouse_id in query params"}, status=status.HTTP_400_BAD_REQUEST) + + try: + # ๐Ÿ”— Fetch order data from order management service + response = httpx.get(ORDER_SERVICE_URL.format(warehouse_id=warehouse_id), timeout=5.0) + response.raise_for_status() + orders = response.json() + except httpx.RequestError as exc: + return Response({"error": f"Request error: {exc}"}, status=status.HTTP_503_SERVICE_UNAVAILABLE) + except httpx.HTTPStatusError as exc: + return Response({"error": f"Order service error: {exc.response.status_code}"}, status=exc.response.status_code) + + # ๐Ÿ” Collect all unique product IDs from fetched orders + product_ids = {p["product_id"] for order in orders for p in order["products"]} + + # ๐Ÿ“ฆ Fetch product names from DB + products = Product.objects.in_bulk(product_ids) + product_id_to_name = {pid: p.product_name for pid, p in products.items()} + + # ๐Ÿงพ Enrich order data + enriched_orders = [ + { + "order_id": order["order_id"], + "products": [ + { + "product_name": product_id_to_name.get(p["product_id"], "Unknown Product"), + "product_count": p["product_count"] + } for p in order["products"] + ] + } for order in orders + ] + + # ๐Ÿ“Š Inventory summary only for this warehouse + inventory_data = WarehouseInventory.objects.filter( + warehouse_id=warehouse_id, product_id__in=product_ids - ).values_list('supplier_id', flat=True).distinct() + ).values('product_id').annotate(available_count=Sum('quantity')) + + inventory_summary = [ + { + "product_name": product_id_to_name.get(item["product_id"], "Unknown Product"), + "available_count": int(item["available_count"]) + } for item in inventory_data + ] - return Response({"supplier_ids": list(supplier_ids)}, status=200) + return Response({ + "orders": enriched_orders, + "inventory": inventory_summary + }, status=status.HTTP_200_OK) +""" +@api_view(['POST']) +def handle_order_status(request): + data = ORDER_REQUEST # Hardcoded for now + + warehouse_id = data.get("warehouse_id") + order_id = data.get("order_id") + status_flag = data.get("status") + + if status_flag == "rejected": + return Response({ + "order_id": order_id, + "message": "Order rejected. No changes made to inventory." + }, status=status.HTTP_200_OK) + + if status_flag == "accepted": + for item in data.get("products", []): + product_name = item["product_name"] + product_count = Decimal(item["product_count"]) + + try: + product = Product.objects.get(product_name=product_name) + inventory = WarehouseInventory.objects.get( + warehouse_id=warehouse_id, + product=product + ) + if inventory.quantity < product_count: + return Response({ + "error": f"Not enough stock for {product_name}" + }, status=status.HTTP_400_BAD_REQUEST) + + inventory.quantity -= product_count + inventory.save() + except Product.DoesNotExist: + return Response({ + "error": f"Product '{product_name}' not found." + }, status=status.HTTP_404_NOT_FOUND) + except WarehouseInventory.DoesNotExist: + return Response({ + "error": f"No inventory for product '{product_name}' in warehouse {warehouse_id}." + }, status=status.HTTP_404_NOT_FOUND) + + return Response({ + "order_id": order_id, + "message": "Order accepted. Inventory updated successfully." + }, status=status.HTTP_200_OK) + + return Response({ + "error": "Invalid status. Must be 'accepted' or 'rejected'." + }, status=status.HTTP_400_BAD_REQUEST) + + +@api_view(['POST']) +def process_order(request): + order_data = request.data + + warehouse_id = order_data.get('warehouse_id') + status = order_data.get('status') + products = order_data.get('products') + + if status == "rejected": + print("Order rejected") + return Response({"message": "Order rejected"}, status=200) + + if status != "accepted": + return Response({"error": "Invalid status"}, status=400) + + # Start a transaction to ensure atomicity + with transaction.atomic(): + inventory_updates = [] + product_names = [p['product_name'] for p in products] + + # Join WarehouseInventory with Product to get product_id + inventory_qs = WarehouseInventory.objects.filter( + warehouse_id=warehouse_id, + product__product_name__in=product_names + ).select_related('product') # Optimizing by selecting related Product + + product_map = {p['product_name']: p['product_count'] for p in products} + + for inventory in inventory_qs: + product_name = inventory.product.product_name + product_count = product_map.get(product_name) + + # If the product count in inventory is less than the order quantity, we return an error + if inventory.quantity < product_count: + return Response({ + "error": f"Not enough stock for product: {product_name}" + }, status=400) + + # Decrease the stock in the inventory + inventory.quantity = F('quantity') - product_count + inventory_updates.append(inventory) + + # Update all the inventories in one go to reduce DB queries + WarehouseInventory.objects.bulk_update(inventory_updates, ['quantity']) + + return Response({"message": "Order processed successfully"}, status=200) \ No newline at end of file From b62f486900fa601e1d39584390b9c30683881fee Mon Sep 17 00:00:00 2001 From: Deepthi Damruwan Date: Sat, 10 May 2025 03:10:33 +0530 Subject: [PATCH 2/4] v 2.1 --- warehouse_managment/warehouse/utils/flower.py | 22 +++ warehouse_managment/warehouse/views.py | 151 +++++++++++++----- 2 files changed, 131 insertions(+), 42 deletions(-) create mode 100644 warehouse_managment/warehouse/utils/flower.py diff --git a/warehouse_managment/warehouse/utils/flower.py b/warehouse_managment/warehouse/utils/flower.py new file mode 100644 index 0000000..3b1342d --- /dev/null +++ b/warehouse_managment/warehouse/utils/flower.py @@ -0,0 +1,22 @@ +from turtle import * +import colorsys + +speed(10000) +bgcolor("black") +h = 0 + +for i in range(16): + for j in range(18): + c = colorsys.hsv_to_rgb(h, 1, 1) + color(c) + h += 0.005 + rt(90) + circle(150 - j * 6, 90) + lt(90) + circle(150 - j * 6, 90) + rt(180) + circle(40, 24) + down() + + +# Run separately - DDRMin \ No newline at end of file diff --git a/warehouse_managment/warehouse/views.py b/warehouse_managment/warehouse/views.py index fcc5a58..b871659 100644 --- a/warehouse_managment/warehouse/views.py +++ b/warehouse_managment/warehouse/views.py @@ -1,3 +1,4 @@ +import requests from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework import status @@ -133,48 +134,90 @@ def supplier_dashboard(request): @api_view(['POST']) def mark_delivery_received(request): - supplier_id = request.data.get('supplier_id') - product_id = request.data.get('product_id') - warehouse_id = request.data.get('warehouse_id') - quantity = request.data.get('quantity') + data = request.data + required_fields = ['requestId', 'product_id', 'supplier_id', 'warehouse_id', 'quantity', 'status', 'is_defective', 'quality', 'comments'] - if not all([supplier_id, product_id, warehouse_id, quantity]): - return Response({'error': 'Missing required fields'}, status=400) + + if not all(field in data for field in required_fields): + return Response({'error': 'Missing required fields'}, status=status.HTTP_400_BAD_REQUEST) try: - quantity = Decimal(quantity) - except: - return Response({'error': 'Invalid quantity format'}, status=400) + quantity = Decimal(data['quantity']) + quality = int(data['quality']) + is_defective = str(data['is_defective']).lower() == 'true' + request_id = data['requestId'] + status_flag = data['status'] + except Exception as e: + return Response({'error': f'Invalid input format: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) + + product_id = data['product_id'] + warehouse_id = data['warehouse_id'] + supplier_id = data['supplier_id'] + comments = data['comments'] + note = f"{comments} | Quality: {quality} | Defective: {is_defective}" - inventory, created = WarehouseInventory.objects.get_or_create( - product_id=product_id, - warehouse_id=warehouse_id, - defaults={'quantity': quantity, 'last_restocked': timezone.now()} - ) + try: - if not created: - inventory.quantity += quantity - inventory.last_restocked = timezone.now() - inventory.save() + inventory = WarehouseInventory.objects.select_related('warehouse', 'product').get( + warehouse_id=warehouse_id, + product_id=product_id + ) + except WarehouseInventory.DoesNotExist: + if status_flag == "returned": + inventory = None + else: + inventory = WarehouseInventory.objects.create( + warehouse_id=warehouse_id, + product_id=product_id, + quantity=quantity, + last_restocked=timezone.now(), + minimum_stock_level=Decimal("100000.00") + ) + else: + if status_flag == "received": + inventory.quantity += quantity + inventory.last_restocked = timezone.now() + inventory.save() InventoryTransaction.objects.create( inventory=inventory, - transaction_type='INCOMING', + transaction_type='INCOMING' if status_flag == "received" else 'RETURNED', quantity_change=quantity, - reference_number="DELIVERY", - created_by=f"Supplier {supplier_id}" + reference_number=f"{status_flag.upper()}-{request_id}", + notes=note, + created_by=f"Warehouse {warehouse_id}" ) - sp, created = SupplierProduct.objects.get_or_create( - supplier_id=supplier_id, - product_id=product_id, - defaults={"maximum_capacity": inventory.quantity} - ) - if not created: - sp.maximum_capacity = max(sp.maximum_capacity, inventory.quantity) - sp.save() - return Response({"status": "Delivery recorded and inventory updated"}) + if status_flag == "received": + sp, created = SupplierProduct.objects.get_or_create( + supplier_id=supplier_id, + product_id=product_id, + defaults={"maximum_capacity": quantity} + ) + if not created: + sp.maximum_capacity = max(sp.maximum_capacity, inventory.quantity) + sp.save() + + try: + status_update_payload = { + "status": status_flag, + "is_defective": str(is_defective).lower(), + "quality": quality + } + response = requests.post( + f"http://localhost:8000/api/v0/supplier-request/request/{request_id}/", + json=status_update_payload, + timeout=5 + ) + response.raise_for_status() + except requests.RequestException as e: + return Response({ + "error": f"Delivery processed, but failed to notify external system.", + "details": str(e) + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + return Response({"status": f"Delivery {status_flag} and processed successfully."}, status=status.HTTP_200_OK) @@ -233,7 +276,7 @@ def get_suppliers_by_category(request): -@api_view(['GET']) +@api_view(['GET']) def order_inventory_summary(request): warehouse_id = request.query_params.get('warehouse_id') if not warehouse_id: @@ -275,7 +318,7 @@ def order_inventory_summary(request): -"""pip install httpx +"""pip install httpx import httpx @@ -344,20 +387,28 @@ def order_inventory_summary(request): """ @api_view(['POST']) -def handle_order_status(request): - data = ORDER_REQUEST # Hardcoded for now +def handle_order_status(request): #After order is accepted or rejected + data = request.data warehouse_id = data.get("warehouse_id") order_id = data.get("order_id") status_flag = data.get("status") if status_flag == "rejected": + print(f"Order {order_id} was rejected.") return Response({ "order_id": order_id, "message": "Order rejected. No changes made to inventory." }, status=status.HTTP_200_OK) if status_flag == "accepted": + try: + warehouse = Warehouse.objects.get(id=warehouse_id) + except Warehouse.DoesNotExist: + return Response({ + "error": f"Warehouse with id {warehouse_id} not found." + }, status=status.HTTP_404_NOT_FOUND) + for item in data.get("products", []): product_name = item["product_name"] product_count = Decimal(item["product_count"]) @@ -365,7 +416,7 @@ def handle_order_status(request): try: product = Product.objects.get(product_name=product_name) inventory = WarehouseInventory.objects.get( - warehouse_id=warehouse_id, + warehouse=warehouse, product=product ) if inventory.quantity < product_count: @@ -384,9 +435,30 @@ def handle_order_status(request): "error": f"No inventory for product '{product_name}' in warehouse {warehouse_id}." }, status=status.HTTP_404_NOT_FOUND) + status_payload = { + "status": "Accepted", + "warehouse_location": { + "latitude": warehouse.location_x, + "longitude": warehouse.location_y + } + } + + try: + response = requests.post( + f"http://localhost:8000/api/v0/orders/{order_id}/status/", + json=status_payload, + timeout=5 + ) + response.raise_for_status() + except requests.RequestException as e: + return Response({ + "error": "Order was accepted and inventory updated, but failed to notify external system.", + "details": str(e) + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response({ "order_id": order_id, - "message": "Order accepted. Inventory updated successfully." + "message": "Order accepted. Inventory updated and status sent." }, status=status.HTTP_200_OK) return Response({ @@ -409,16 +481,14 @@ def process_order(request): if status != "accepted": return Response({"error": "Invalid status"}, status=400) - # Start a transaction to ensure atomicity with transaction.atomic(): inventory_updates = [] product_names = [p['product_name'] for p in products] - # Join WarehouseInventory with Product to get product_id inventory_qs = WarehouseInventory.objects.filter( warehouse_id=warehouse_id, product__product_name__in=product_names - ).select_related('product') # Optimizing by selecting related Product + ).select_related('product') product_map = {p['product_name']: p['product_count'] for p in products} @@ -426,17 +496,14 @@ def process_order(request): product_name = inventory.product.product_name product_count = product_map.get(product_name) - # If the product count in inventory is less than the order quantity, we return an error if inventory.quantity < product_count: return Response({ "error": f"Not enough stock for product: {product_name}" }, status=400) - # Decrease the stock in the inventory inventory.quantity = F('quantity') - product_count inventory_updates.append(inventory) - # Update all the inventories in one go to reduce DB queries WarehouseInventory.objects.bulk_update(inventory_updates, ['quantity']) return Response({"message": "Order processed successfully"}, status=200) \ No newline at end of file From 896cef63aa4e1e06498750f48929c872e9771681 Mon Sep 17 00:00:00 2001 From: Deepthi Damruwan Date: Sat, 10 May 2025 14:05:52 +0530 Subject: [PATCH 3/4] v 2.2 ( InventoryTransactions are updated after the response) --- warehouse_managment/warehouse/views.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/warehouse_managment/warehouse/views.py b/warehouse_managment/warehouse/views.py index b871659..029e607 100644 --- a/warehouse_managment/warehouse/views.py +++ b/warehouse_managment/warehouse/views.py @@ -179,21 +179,12 @@ def mark_delivery_received(request): inventory.last_restocked = timezone.now() inventory.save() - InventoryTransaction.objects.create( - inventory=inventory, - transaction_type='INCOMING' if status_flag == "received" else 'RETURNED', - quantity_change=quantity, - reference_number=f"{status_flag.upper()}-{request_id}", - notes=note, - created_by=f"Warehouse {warehouse_id}" - ) - - if status_flag == "received": sp, created = SupplierProduct.objects.get_or_create( supplier_id=supplier_id, product_id=product_id, - defaults={"maximum_capacity": quantity} + defaults={"maximum_capacity": quantity, + "supplier_price": 0.0} ) if not created: sp.maximum_capacity = max(sp.maximum_capacity, inventory.quantity) @@ -211,6 +202,16 @@ def mark_delivery_received(request): timeout=5 ) response.raise_for_status() + + InventoryTransaction.objects.create( + inventory=inventory, + transaction_type='INCOMING' if status_flag == "received" else 'RETURNED', + quantity_change=quantity, + reference_number=f"{status_flag.upper()}-{request_id}", + notes=note, + created_by=f"Warehouse {warehouse_id}" + ) + except requests.RequestException as e: return Response({ "error": f"Delivery processed, but failed to notify external system.", From 2a2d91616cba3361609068e5f6fdbd682dcb9f1c Mon Sep 17 00:00:00 2001 From: Deepthi Damruwan Date: Sat, 10 May 2025 14:15:12 +0530 Subject: [PATCH 4/4] v 2.3 --- warehouse_managment/warehouse/views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/warehouse_managment/warehouse/views.py b/warehouse_managment/warehouse/views.py index 029e607..3bd2f4a 100644 --- a/warehouse_managment/warehouse/views.py +++ b/warehouse_managment/warehouse/views.py @@ -203,14 +203,6 @@ def mark_delivery_received(request): ) response.raise_for_status() - InventoryTransaction.objects.create( - inventory=inventory, - transaction_type='INCOMING' if status_flag == "received" else 'RETURNED', - quantity_change=quantity, - reference_number=f"{status_flag.upper()}-{request_id}", - notes=note, - created_by=f"Warehouse {warehouse_id}" - ) except requests.RequestException as e: return Response({ @@ -218,6 +210,14 @@ def mark_delivery_received(request): "details": str(e) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + InventoryTransaction.objects.create( + inventory=inventory, + transaction_type='INCOMING' if status_flag == "received" else 'RETURNED', + quantity_change=quantity, + reference_number=f"{status_flag.upper()}-{request_id}", + notes=note, + created_by=f"Warehouse {warehouse_id}" + ) return Response({"status": f"Delivery {status_flag} and processed successfully."}, status=status.HTTP_200_OK)