integrated seller items
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 19s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 19s
This commit is contained in:
parent
10361ecc2c
commit
ba49657163
21 changed files with 228 additions and 59 deletions
BIN
customerwebsitebackend/__pycache__/db_router.cpython-312.pyc
Normal file
BIN
customerwebsitebackend/__pycache__/db_router.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
20
customerwebsitebackend/db_router.py
Normal file
20
customerwebsitebackend/db_router.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
class DatabaseRouter:
|
||||
def db_for_read(self, model, **hints):
|
||||
if model._meta.model_name == 'product':
|
||||
return 'productprofile'
|
||||
elif model._meta.model_name in ['sellerorder', 'orderitem']:
|
||||
return 'customerprofile'
|
||||
return 'default'
|
||||
|
||||
def db_for_write(self, model, **hints):
|
||||
return self.db_for_read(model, **hints)
|
||||
|
||||
def allow_relation(self, obj1, obj2, **hints):
|
||||
return True
|
||||
|
||||
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
||||
if model_name == 'product':
|
||||
return db == 'productprofile'
|
||||
elif model_name in ['sellerorder', 'orderitem']:
|
||||
return db == 'customerprofile'
|
||||
return db == 'default'
|
||||
|
|
@ -88,9 +88,27 @@ DATABASES = {
|
|||
'PASSWORD': 'vtechnosoft@123A',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '5432',
|
||||
},
|
||||
'productprofile': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'productprofile',
|
||||
'USER': 'vignesh',
|
||||
'PASSWORD': 'vtechnosoft@123A',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '5432',
|
||||
},
|
||||
'customerprofile': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': 'customerprofile',
|
||||
'USER': 'vignesh',
|
||||
'PASSWORD': 'vtechnosoft@123A',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
|
||||
DATABASE_ROUTERS = ['customerwebsitebackend.db_router.DatabaseRouter']
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
|
||||
|
|
|
|||
|
|
@ -5,3 +5,7 @@ INFO 2026-08-14 13:43:48,808 autoreload 57706 139697750147200 /home/vignesh/gith
|
|||
INFO 2026-08-14 13:43:49,076 autoreload 58191 123150790480000 Watching for file changes with StatReloader
|
||||
INFO 2026-08-14 13:46:26,838 autoreload 58191 123150790480000 /home/vignesh/github/customerwebsitebackend/customerwebsitebackend/settings.py changed, reloading.
|
||||
INFO 2026-08-14 13:46:27,112 autoreload 60038 140193920041088 Watching for file changes with StatReloader
|
||||
INFO 2026-08-14 13:52:49,207 autoreload 60038 140193920041088 /home/vignesh/github/customerwebsitebackend/orders/models.py changed, reloading.
|
||||
INFO 2026-08-14 13:52:49,524 autoreload 64078 126174254198912 Watching for file changes with StatReloader
|
||||
INFO 2026-08-14 13:53:37,086 autoreload 64078 126174254198912 /home/vignesh/github/customerwebsitebackend/orders/models.py changed, reloading.
|
||||
INFO 2026-08-14 13:53:37,360 autoreload 64566 128112213057664 Watching for file changes with StatReloader
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,46 @@
|
|||
# Generated by Django 6.1 on 2026-08-14 13:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SellerOrder',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('supplier_id', models.IntegerField()),
|
||||
('order_id', models.CharField(max_length=50, unique=True)),
|
||||
('date', models.DateField()),
|
||||
('item', models.CharField(max_length=255)),
|
||||
('quantity', models.IntegerField(default=1)),
|
||||
('customer', models.CharField(max_length=255)),
|
||||
('total', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('status', models.CharField(default='Pending Acceptance', max_length=50)),
|
||||
('carrier', models.CharField(default='Pending', max_length=100)),
|
||||
('tracking', models.CharField(default='Pending', max_length=100)),
|
||||
('eta', models.CharField(default='N/A', max_length=50)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'api_order',
|
||||
'managed': False,
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cartitem',
|
||||
name='product',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartitem',
|
||||
name='product_id',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OrderItem',
|
||||
),
|
||||
]
|
||||
Binary file not shown.
|
|
@ -21,12 +21,19 @@ class Cart(models.Model):
|
|||
|
||||
class CartItem(models.Model):
|
||||
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
product_id = models.IntegerField(default=1) # Keep reference as simple int since product is in productprofile db
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
price_at_add = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
@property
|
||||
def product(self):
|
||||
try:
|
||||
return Product.objects.get(id=self.product_id)
|
||||
except Product.DoesNotExist:
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.quantity} x {self.product.title}"
|
||||
return f"{self.quantity} x Product {self.product_id}"
|
||||
|
||||
class Order(models.Model):
|
||||
STATUS_CHOICES = (
|
||||
|
|
@ -47,13 +54,23 @@ class Order(models.Model):
|
|||
def __str__(self):
|
||||
return f"Order {self.id} by {self.user.username}"
|
||||
|
||||
class OrderItem(models.Model):
|
||||
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
|
||||
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
|
||||
quantity = models.PositiveIntegerField()
|
||||
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
tracking_number = models.CharField(max_length=100, blank=True, null=True)
|
||||
carrier = models.CharField(max_length=100, blank=True, null=True)
|
||||
# Align to seller_central_backend api_order schema
|
||||
class SellerOrder(models.Model):
|
||||
supplier_id = models.IntegerField() # Raw integer referring to User in sellerprofile db
|
||||
order_id = models.CharField(max_length=50, unique=True)
|
||||
date = models.DateField()
|
||||
item = models.CharField(max_length=255)
|
||||
quantity = models.IntegerField(default=1)
|
||||
customer = models.CharField(max_length=255)
|
||||
total = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
status = models.CharField(max_length=50, default='Pending Acceptance')
|
||||
carrier = models.CharField(max_length=100, default='Pending')
|
||||
tracking = models.CharField(max_length=100, default='Pending')
|
||||
eta = models.CharField(max_length=50, default='N/A')
|
||||
|
||||
class Meta:
|
||||
db_table = 'api_order'
|
||||
managed = False # Managed by seller central backend migrations
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.quantity} x {self.product.title if self.product else 'Deleted Product'}"
|
||||
return self.order_id
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Cart, CartItem, Order, OrderItem
|
||||
from .models import Cart, CartItem, Order, SellerOrder
|
||||
from products.serializers import ProductSerializer
|
||||
|
||||
class CartItemSerializer(serializers.ModelSerializer):
|
||||
product = ProductSerializer(read_only=True)
|
||||
product_id = serializers.IntegerField(write_only=True)
|
||||
product = serializers.SerializerMethodField()
|
||||
product_id = serializers.IntegerField()
|
||||
subtotal = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CartItem
|
||||
fields = ['id', 'product', 'product_id', 'quantity', 'price_at_add', 'subtotal']
|
||||
|
||||
def get_product(self, obj):
|
||||
prod = obj.product
|
||||
if prod:
|
||||
return ProductSerializer(prod).data
|
||||
return None
|
||||
|
||||
def get_subtotal(self, obj):
|
||||
return obj.quantity * obj.price_at_add
|
||||
|
||||
|
|
@ -25,18 +31,9 @@ class CartSerializer(serializers.ModelSerializer):
|
|||
def get_total_price(self, obj):
|
||||
return sum(item.quantity * item.price_at_add for item in obj.items.all())
|
||||
|
||||
class OrderItemSerializer(serializers.ModelSerializer):
|
||||
product_title = serializers.CharField(source='product.title', read_only=True)
|
||||
product_image = serializers.CharField(source='product.image_url', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = OrderItem
|
||||
fields = ['id', 'product', 'product_title', 'product_image', 'quantity', 'unit_price', 'tracking_number', 'carrier']
|
||||
|
||||
class OrderSerializer(serializers.ModelSerializer):
|
||||
items = OrderItemSerializer(many=True, read_only=True)
|
||||
username = serializers.CharField(source='user.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['id', 'username', 'razorpay_order_id', 'razorpay_payment_id', 'total_amount', 'status', 'shipping_address', 'placed_at', 'items']
|
||||
fields = ['id', 'username', 'razorpay_order_id', 'razorpay_payment_id', 'total_amount', 'status', 'shipping_address', 'placed_at']
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ audit_logger = logging.getLogger('audit')
|
|||
from rest_framework import status, permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from .models import Cart, CartItem, Order, OrderItem
|
||||
from .models import Cart, CartItem, Order, SellerOrder
|
||||
from .serializers import CartSerializer, OrderSerializer
|
||||
from products.models import Product
|
||||
import uuid
|
||||
from django.utils.timezone import now
|
||||
|
||||
class CartView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
|
@ -26,17 +27,17 @@ class CartView(APIView):
|
|||
quantity = int(request.data.get('quantity', 1))
|
||||
|
||||
try:
|
||||
product = Product.objects.get(id=product_id, is_active=True)
|
||||
product = Product.objects.get(id=product_id)
|
||||
except Product.DoesNotExist:
|
||||
return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
cart_item, created = CartItem.objects.get_or_create(
|
||||
cart=cart,
|
||||
product=product,
|
||||
product_id=product_id,
|
||||
defaults={'price_at_add': product.price, 'quantity': 0}
|
||||
)
|
||||
cart_item.quantity += quantity
|
||||
cart_item.price_at_add = product.price # update price to latest
|
||||
cart_item.price_at_add = product.price
|
||||
cart_item.save()
|
||||
|
||||
return Response({"message": "Item added to cart"})
|
||||
|
|
@ -86,13 +87,10 @@ class CheckoutView(APIView):
|
|||
if not shipping_address:
|
||||
return Response({"error": "Shipping address is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Calculate total
|
||||
total_amount = sum(item.quantity * item.price_at_add for item in items)
|
||||
|
||||
# Mock Razorpay Order Creation
|
||||
razorpay_order_id = f"order_{uuid.uuid4().hex[:14]}"
|
||||
|
||||
# Create Order
|
||||
# 1. Create a local Customer Order
|
||||
order = Order.objects.create(
|
||||
user=user,
|
||||
total_amount=total_amount,
|
||||
|
|
@ -102,17 +100,20 @@ class CheckoutView(APIView):
|
|||
)
|
||||
audit_logger.info(f"AUDIT - Checkout: User '{user.username}' created order ID {order.id} for amount ${total_amount:.2f}.")
|
||||
|
||||
# 2. Write to shared SellerOrder (api_order) so sellers get notified of incoming requests
|
||||
for item in items:
|
||||
OrderItem.objects.create(
|
||||
order=order,
|
||||
product=item.product,
|
||||
prod = item.product
|
||||
if prod:
|
||||
SellerOrder.objects.create(
|
||||
supplier_id=prod.supplier_id,
|
||||
order_id=f"{order.id}-{prod.id}",
|
||||
date=now().date(),
|
||||
item=prod.title,
|
||||
quantity=item.quantity,
|
||||
unit_price=item.price_at_add
|
||||
customer=user.username,
|
||||
total=item.quantity * item.price_at_add,
|
||||
status='Pending Acceptance'
|
||||
)
|
||||
# Deduct stock
|
||||
if item.product.stock >= item.quantity:
|
||||
item.product.stock -= item.quantity
|
||||
item.product.save()
|
||||
|
||||
# Clear Cart
|
||||
items.delete()
|
||||
|
|
@ -141,6 +142,9 @@ class PaymentSuccessView(APIView):
|
|||
order.save()
|
||||
audit_logger.info(f"AUDIT - Payment Success: User '{request.user.username}' successfully paid order ID {order.id} (Payment ID: {payment_id}).")
|
||||
|
||||
# Update matching SellerOrders status
|
||||
SellerOrder.objects.filter(order_id__startswith=f"{order.id}-").update(status='Ready to Ship')
|
||||
|
||||
return Response({"message": "Payment verified and order confirmed"})
|
||||
|
||||
class OrderHistoryView(APIView):
|
||||
|
|
@ -149,7 +153,40 @@ class OrderHistoryView(APIView):
|
|||
def get(self, request):
|
||||
orders = Order.objects.filter(user=request.user).order_by('-placed_at')
|
||||
serializer = OrderSerializer(orders, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
# Merge tracking details from SellerOrders
|
||||
response_data = []
|
||||
for order_data in serializer.data:
|
||||
order_id = order_data['id']
|
||||
seller_orders = SellerOrder.objects.filter(order_id__startswith=f"{order_id}-")
|
||||
|
||||
# Enrich with tracking details if available
|
||||
order_data['items'] = []
|
||||
for s_ord in seller_orders:
|
||||
order_data['items'].append({
|
||||
"product_title": s_ord.item,
|
||||
"quantity": s_ord.quantity,
|
||||
"unit_price": float(s_ord.total / s_ord.quantity),
|
||||
"tracking_number": s_ord.tracking if s_ord.tracking != 'Pending' else None,
|
||||
"carrier": s_ord.carrier if s_ord.carrier != 'Pending' else None,
|
||||
"status": s_ord.status
|
||||
})
|
||||
|
||||
# Aggregate status
|
||||
if seller_orders.exists():
|
||||
statuses = [o.status for o in seller_orders]
|
||||
if 'Pending Acceptance' in statuses:
|
||||
order_data['status'] = 'pending'
|
||||
elif 'Ready to Ship' in statuses:
|
||||
order_data['status'] = 'confirmed'
|
||||
elif 'Shipped' in statuses:
|
||||
order_data['status'] = 'shipped'
|
||||
elif 'Delivered' in statuses:
|
||||
order_data['status'] = 'delivered'
|
||||
|
||||
response_data.append(order_data)
|
||||
|
||||
return Response(response_data)
|
||||
|
||||
class OrderDetailView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
|
@ -159,5 +196,20 @@ class OrderDetailView(APIView):
|
|||
order = Order.objects.get(id=pk, user=request.user)
|
||||
except Order.DoesNotExist:
|
||||
return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = OrderSerializer(order)
|
||||
return Response(serializer.data)
|
||||
order_data = serializer.data
|
||||
|
||||
seller_orders = SellerOrder.objects.filter(order_id__startswith=f"{pk}-")
|
||||
order_data['items'] = []
|
||||
for s_ord in seller_orders:
|
||||
order_data['items'].append({
|
||||
"product_title": s_ord.item,
|
||||
"quantity": s_ord.quantity,
|
||||
"unit_price": float(s_ord.total / s_ord.quantity),
|
||||
"tracking_number": s_ord.tracking if s_ord.tracking != 'Pending' else None,
|
||||
"carrier": s_ord.carrier if s_ord.carrier != 'Pending' else None,
|
||||
"status": s_ord.status
|
||||
})
|
||||
|
||||
return Response(order_data)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
17
products/migrations/0002_alter_product_options.py
Normal file
17
products/migrations/0002_alter_product_options.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 6.1 on 2026-08-14 13:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='product',
|
||||
options={'managed': False},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
|||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class Category(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
|
@ -11,25 +11,22 @@ class Category(models.Model):
|
|||
verbose_name_plural = "Categories"
|
||||
ordering = ['sort_order', 'name']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Product(models.Model):
|
||||
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products')
|
||||
# Align to seller_central_backend api_product schema
|
||||
supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products', db_constraint=False)
|
||||
title = models.CharField(max_length=255)
|
||||
category = models.CharField(max_length=100) # CharField matching seller central
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
stock = models.IntegerField(default=0)
|
||||
sku = models.CharField(max_length=100, unique=True)
|
||||
image_url = models.URLField(max_length=500, blank=True, null=True)
|
||||
description = models.TextField(blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
image = models.TextField(blank=True, null=True) # Textfield for base64 / URL
|
||||
|
||||
class Meta:
|
||||
db_table = 'api_product'
|
||||
managed = False # Managed by seller central backend migrations
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ class CategorySerializer(serializers.ModelSerializer):
|
|||
fields = ['id', 'name', 'slug', 'is_active', 'sort_order']
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
category_name = serializers.CharField(source='category.name', read_only=True)
|
||||
category_name = serializers.CharField(source='category', read_only=True)
|
||||
image_url = serializers.CharField(source='image', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['id', 'category', 'category_name', 'title', 'price', 'stock', 'sku', 'image_url', 'description', 'is_active']
|
||||
fields = ['id', 'category', 'category_name', 'title', 'price', 'stock', 'sku', 'image_url']
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class ProductListView(generics.ListAPIView):
|
|||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Product.objects.filter(is_active=True)
|
||||
queryset = Product.objects.all()
|
||||
category_slug = self.request.query_params.get('category', None)
|
||||
search_query = self.request.query_params.get('search', None)
|
||||
|
||||
|
|
@ -29,6 +29,6 @@ class ProductListView(generics.ListAPIView):
|
|||
return queryset
|
||||
|
||||
class ProductDetailView(generics.RetrieveAPIView):
|
||||
queryset = Product.objects.filter(is_active=True)
|
||||
queryset = Product.objects.all()
|
||||
serializer_class = ProductSerializer
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
|
|
|||
Loading…
Reference in a new issue