diff --git a/commerce/controllers.py b/commerce/controllers.py index 0d8791b..f7929ab 100644 --- a/commerce/controllers.py +++ b/commerce/controllers.py @@ -1,222 +1,437 @@ from typing import List + +import string + from django.contrib.auth.models import User + from django.db.models import Q + from django.shortcuts import get_object_or_404 + from ninja import Router + from pydantic import UUID4 +import random + + +from commerce.models import Product, Category, City, Vendor, Item,Order,Address -from commerce.models import Product, Category, City, Vendor, Item from commerce.schemas import MessageOut, ProductOut, CitiesOut, CitySchema, VendorOut, ItemOut, ItemSchema, ItemCreate + products_controller = Router(tags=['products']) + address_controller = Router(tags=['addresses']) + vendor_controller = Router(tags=['vendors']) + order_controller = Router(tags=['orders']) + @vendor_controller.get('', response=List[VendorOut]) + def list_vendors(request): + return Vendor.objects.all() + @products_controller.get('', response={ + 200: List[ProductOut], + 404: MessageOut + }) def list_products( + request, *, + q: str = None, + price_from: int = None, + price_to: int = None, + vendor=None, ): + products_qs = Product.objects.filter(is_active=True).select_related('merchant', 'vendor', 'category', 'label') + if not products_qs: + return 404, {'detail': 'No products found'} + if q: + products_qs = products_qs.filter( + Q(name__icontains=q) | Q(description__icontains=q) ) if price_from: + products_qs = products_qs.filter(discounted_price__gte=price_from) if price_to: + products_qs = products_qs.filter(discounted_price__lte=price_to) + if vendor: + products_qs = products_qs.filter(vendor_id=vendor) + return products_qs + """ + # product = Product.objects.all().select_related('merchant', 'category', 'vendor', 'label') # print(product) # + # order = Product.objects.all().select_related('address', 'user').prefetch_related('items') + # try: + # one_product = Product.objects.get(id='8d3dd0f1-2910-457c-89e3-1b0ed6aa720a') + # except Product.DoesNotExist: + # return {"detail": "Not found"} # print(one_product) # + # shortcut_function = get_object_or_404(Product, id='8d3dd0f1-2910-457c-89e3-1b0ed6aa720a') + # print(shortcut_function) + # print(type(product)) + # print(product.merchant.name) + # print(type(product.merchant)) + # print(type(product.category)) + Product <- Merchant, Label, Category, Vendor + Retrieve 1000 Products form DB + products = Product.objects.all()[:1000] (select * from product limit 1000) for p in products: print(p) + for every product, we retrieve (Merchant, Label, Category, Vendor) records + Merchant.objects.get(id=p.merchant_id) (select * from merchant where id = 'p.merchant_id') + Label.objects.get(id=p.label_id) (select * from merchant where id = 'p.label_id') + Category.objects.get(id=p.category_id) (select * from merchant where id = 'p.category_id') + Vendor.objects.get(id=p.vendor_id) (select * from merchant where id = 'p.vendor_id') + 4*1000+1 + Solution: Eager loading + products = (select * from product limit 1000) + mids = [p1.merchant_id, p2.merchant_id, ...] + [p1.label_id, p2.label_id, ...] . . . + select * from merchant where id in (mids) * 4 for (label, category and vendor) + 4+1 + """ + @address_controller.get('') + def list_addresses(request): - pass + return list(Address.objects.values()) # @products_controller.get('categories', response=List[CategoryOut]) + # def list_categories(request): + # return Category.objects.all() + @address_controller.get('cities', response={ + 200: List[CitiesOut], + 404: MessageOut + }) + def list_cities(request): + cities_qs = City.objects.all() + if cities_qs: + return cities_qs + return 404, {'detail': 'No cities found'} + @address_controller.get('cities/{id}', response={ + 200: CitiesOut, + 404: MessageOut + }) + def retrieve_city(request, id: UUID4): + return get_object_or_404(City, id=id) + @address_controller.post('cities', response={ + 201: CitiesOut, + 400: MessageOut + }) + def create_city(request, city_in: CitySchema): + city = City(**city_in.dict()) + city.save() + return 201, city + @address_controller.put('cities/{id}', response={ + 200: CitiesOut, + 400: MessageOut + }) + def update_city(request, id: UUID4, city_in: CitySchema): + city = get_object_or_404(City, id=id) + city.name = city_in.name + city.save() + return 200, city + @address_controller.delete('cities/{id}', response={ + 204: MessageOut + }) + def delete_city(request, id: UUID4): + city = get_object_or_404(City, id=id) + city.delete() + return 204, {'detail': ''} + @order_controller.get('cart', response={ + 200: List[ItemOut], + 404: MessageOut + }) + def view_cart(request): + cart_items = Item.objects.filter(user=User.objects.first(), ordered=False) if cart_items: return cart_items + return 404, {'detail': 'Your cart is empty, go shop like crazy!'} + @order_controller.post('add-to-cart', response={ + 200: MessageOut, + # 400: MessageOut + }) + def add_update_cart(request, item_in: ItemCreate): + try: + item = Item.objects.get(product_id=item_in.product_id, user=User.objects.first()) + item.item_qty += 1 + item.save() + except Item.DoesNotExist: + Item.objects.create(**item_in.dict(), user=User.objects.first()) + return 200, {'detail': 'Added to cart successfully'} + @order_controller.post('item/{id}/reduce-quantity', response={ + 200: MessageOut, + }) + def reduce_item_quantity(request, id: UUID4): + item = get_object_or_404(Item, id=id, user=User.objects.first()) + if item.item_qty <= 1: item.delete() + return 200, {'detail': 'Item deleted!'} + item.item_qty -= 1 + item.save() + return 200, {'detail': 'Item quantity reduced successfully!'} + @order_controller.delete('item/{id}', response={ + 204: MessageOut + }) + def delete_item(request, id: UUID4): + item = get_object_or_404(Item, id=id, user=User.objects.first()) item.delete() + return 204, {'detail': 'Item deleted!'} + + + + + +@order_controller.post('item/{id}/increase-quantity', response={ + + 200: MessageOut, + +}) + +def increase_item_quantity(request, id: UUID4): + + item = get_object_or_404(Item, id=id, user=User.objects.first()) + + item.item_qty += 1 + + item.save() + + + return 200, {'detail': 'Item quantity increase successfully!'} + +def create_ref_code(): + return ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + +@order_controller.post('order', response={ + + 200: MessageOut, + +}) +@order_controller.post("creatOrder") +def create_order(request): + + order_q=Order.objects.create( + user=User.objects.first(), + status=OrderStatus.objects.get(is_default=True), + ref_code=create_ref_code(), + ordered=False , + ) + + user_items=Item.objects.filter(user=User.objects.first()).filter(ordered=False) + + order_q.items.add(*user_items) + order_q.total=order_q.order_total + user_items.update(ordered=True) + order_q.save() + return{'detels':'suces'} + + +@order_controller.post("checkout") +def checkout(request,n:str): + checkout_qs=Item.objects.filter(user=User.objects.first()).filter(ordered=False) + if checkout_qs: + return checkout_qs + return{'detels':'filde'} + + + + + + + + + + + + + + + + diff --git a/commerce/models.py b/commerce/models.py index 2d9bfa6..c51fb1f 100644 --- a/commerce/models.py +++ b/commerce/models.py @@ -1,107 +1,189 @@ import uuid + from PIL import Image + from django.contrib.auth import get_user_model + from django.db import models + User = get_user_model() + class Entity(models.Model): + class Meta: + abstract = True + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + created = models.DateTimeField(editable=False, auto_now_add=True) + updated = models.DateTimeField(editable=False, auto_now=True) + class Product(Entity): + name = models.CharField(verbose_name='name', max_length=255) + description = models.TextField('description', null=True, blank=True) + weight = models.FloatField('weight', null=True, blank=True) + width = models.FloatField('width', null=True, blank=True) + height = models.FloatField('height', null=True, blank=True) + length = models.FloatField('length', null=True, blank=True) + qty = models.DecimalField('qty', max_digits=10, decimal_places=2) + cost = models.DecimalField('cost', max_digits=10, decimal_places=2) + price = models.DecimalField('price', max_digits=10, decimal_places=2) + discounted_price = models.DecimalField('discounted price', max_digits=10, decimal_places=2) + vendor = models.ForeignKey('commerce.Vendor', verbose_name='vendor', related_name='products', + on_delete=models.SET_NULL, + null=True, blank=True) + category = models.ForeignKey('commerce.Category', verbose_name='category', related_name='products', + null=True, + blank=True, + on_delete=models.SET_NULL) + merchant = models.ForeignKey('commerce.Merchant', verbose_name='merchant', related_name='products', + null=True, + blank=True, + on_delete=models.SET_NULL) + is_featured = models.BooleanField('is featured') + is_active = models.BooleanField('is active') + label = models.ForeignKey('commerce.Label', verbose_name='label', related_name='products', null=True, blank=True, + on_delete=models.CASCADE) + def __str__(self): return self.name + class Order(Entity): + user = models.ForeignKey(User, verbose_name='user', related_name='orders', null=True, blank=True, + on_delete=models.CASCADE) + address = models.ForeignKey('commerce.Address', verbose_name='address', null=True, blank=True, + on_delete=models.CASCADE) + total = models.DecimalField('total', blank=True, null=True, max_digits=1000, decimal_places=0) + status = models.ForeignKey('commerce.OrderStatus', verbose_name='status', related_name='orders', + on_delete=models.CASCADE) + note = models.CharField('note', null=True, blank=True, max_length=255) + ref_code = models.CharField('ref code', max_length=255) + ordered = models.BooleanField('ordered') + items = models.ManyToManyField('commerce.Item', verbose_name='items', related_name='order') + def __str__(self): + return f'{self.user.first_name} + {self.total}' + @property + def order_total(self): return sum( + i.product.discounted_price * i.item_qty for i in self.items.all() ) + class Item(Entity): + """ + Product can live alone in the system, while + Item can only live within an order + """ + user = models.ForeignKey(User, verbose_name='user', related_name='items', on_delete=models.CASCADE) + product = models.ForeignKey('commerce.Product', verbose_name='product', + on_delete=models.CASCADE) + item_qty = models.IntegerField('item_qty') + ordered = models.BooleanField('ordered', default=False) + def __str__(self): return f'' + class OrderStatus(Entity): + NEW = 'NEW' # Order with reference created, items are in the basket. + # CREATED = 'CREATED' # Created with items and pending payment. + # HOLD = 'HOLD' # Stock reduced but still awaiting payment. + # FAILED = 'FAILED' # Payment failed, retry is available. + # CANCELLED = 'CANCELLED' # Cancelled by seller, stock increased. + PROCESSING = 'PROCESSING' # Payment confirmed, processing order. + SHIPPED = 'SHIPPED' # Shipped to customer. + COMPLETED = 'COMPLETED' # Completed and received by customer. + REFUNDED = 'REFUNDED' # Fully refunded by seller. + title = models.CharField('title', max_length=255, choices=[ + (NEW, NEW), + (PROCESSING, PROCESSING), + (SHIPPED, SHIPPED), + (COMPLETED, COMPLETED), + (REFUNDED, REFUNDED), + ]) is_default = models.BooleanField('is default') @@ -115,103 +197,173 @@ class Category(Entity): related_name='children', null=True, blank=True, + on_delete=models.CASCADE) + name = models.CharField('name', max_length=255) + description = models.TextField('description') + image = models.ImageField('image', upload_to='category/') + is_active = models.BooleanField('is active') + def __str__(self): + if self.parent: + return f'- {self.name}' + return f'{self.name}' + class Meta: + verbose_name = 'category' + verbose_name_plural = 'categories' + @property + def children(self): + return self.children + class Merchant(Entity): + name = models.CharField('name', max_length=255) + def __str__(self): return self.name + class ProductImage(Entity): + image = models.ImageField('image', upload_to='product/') + is_default_image = models.BooleanField('is default image') + product = models.ForeignKey('commerce.Product', verbose_name='product', related_name='images', + on_delete=models.CASCADE) + def __str__(self): + return str(self.product.name) + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None, *args, **kwargs): + super().save(*args, **kwargs) + img = Image.open(self.image.path) + if img.height > 500 or img.width > 500: + output_size = (500, 500) + img.thumbnail(output_size) + img.save(self.image.path) + # print(self.image.path) + class Label(Entity): + name = models.CharField('name', max_length=255) + class Meta: + verbose_name = 'label' + verbose_name_plural = 'labels' + def __str__(self): return self.name + class Vendor(Entity): + name = models.CharField('name', max_length=255) + image = models.ImageField('image', upload_to='vendor/') + def __str__(self): return self.name + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None, *args, **kwargs): + super().save(*args, **kwargs) + img = Image.open(self.image.path) + if img.height > 500 or img.width > 500: + output_size = (500, 500) + img.thumbnail(output_size) + img.save(self.image.path) + # print(self.image.path) + class City(Entity): + name = models.CharField('city', max_length=255) + def __str__(self): return self.name + class Meta: + verbose_name = 'city' + verbose_name_plural = 'cities' + class Address(Entity): + user = models.ForeignKey(User, verbose_name='user', related_name='address', + on_delete=models.CASCADE) + work_address = models.BooleanField('work address', null=True, blank=True) + address1 = models.CharField('address1', max_length=255) + address2 = models.CharField('address2', null=True, blank=True, max_length=255) + city = models.ForeignKey(City, related_name='addresses', on_delete=models.CASCADE) + phone = models.CharField('phone', max_length=255) + def __str__(self): + return f'{self.user.first_name} - {self.address1} - {self.address2} - {self.phone}' + diff --git a/commerce/schemas.py b/commerce/schemas.py index 5b7d0d4..a7d7a63 100644 --- a/commerce/schemas.py +++ b/commerce/schemas.py @@ -90,3 +90,6 @@ class ItemCreate(Schema): class ItemOut(UUIDSchema, ItemSchema): pass + + +