from decimal import Decimal from django.core.exceptions import ObjectDoesNotExist from django.core.validators import MinValueValidator from django.db import models from django.utils.translation import ugettext_lazy as _ from djangocms_text_ckeditor.fields import HTMLField from cms.models.fields import PlaceholderField from shop.money import Money, MoneyMaker from shop.money.fields import MoneyField from shop.models.product import BaseProduct, BaseProductManager, AvailableProductMixin, CMSPageReferenceMixin from shop.models.defaults.cart import Cart from shop.models.defaults.cart_item import CartItem from shop.models.order import BaseOrderItem from shop.models.defaults.delivery import Delivery from shop.models.defaults.delivery_item import DeliveryItem from shop.models.defaults.order import Order from shop.models.defaults.mapping import ProductPage, ProductImage from shop.models.defaults.address import BillingAddress, ShippingAddress from shop.models.defaults.customer import Customer __all__ = ['Cart', 'CartItem', 'Order', 'Delivery', 'DeliveryItem', 'BillingAddress', 'ShippingAddress', 'Customer', ] class OrderItem(BaseOrderItem): quantity = models.PositiveIntegerField(_("Ordered quantity")) canceled = models.BooleanField(_("Item canceled "), default=False) def populate_from_cart_item(self, cart_item, request): super().populate_from_cart_item(cart_item, request) # the product's unit_price must be fetched from the product's variant try: variant = cart_item.product.get_product_variant( product_code=cart_item.product_code) self._unit_price = Decimal(variant.unit_price) except (KeyError, ObjectDoesNotExist) as e: raise CartItem.DoesNotExist(e) class Manufacturer(models.Model): name = models.CharField( _("Name"), max_length=50, unique=True, ) def __str__(self): return self.name class ProductManager(BaseProductManager): pass class Product(CMSPageReferenceMixin, BaseProduct): """ Base class to describe a polymorphic product. Here we declare common fields available in all of our different product types. These common fields are also used to build up the view displaying a list of all products. """ product_name = models.CharField( _("Product Name"), max_length=255, ) slug = models.SlugField( _("Slug"), unique=True, ) caption = HTMLField( verbose_name=_("Caption"), blank=True, null=True, configuration='CKEDITOR_SETTINGS_CAPTION', help_text=_( "Short description used in the catalog's list view of products."), ) # common product properties manufacturer = models.ForeignKey( Manufacturer, on_delete=models.CASCADE, verbose_name=_("Manufacturer"), ) # controlling the catalog order = models.PositiveIntegerField( _("Sort by"), db_index=True, ) cms_pages = models.ManyToManyField( 'cms.Page', through=ProductPage, help_text=_("Choose list view this product shall appear on."), ) images = models.ManyToManyField( 'filer.Image', through=ProductImage, ) class Meta: ordering = ('order',) verbose_name = _("Product") verbose_name_plural = _("Products") objects = ProductManager() # filter expression used to lookup for a product item using the Select2 widget lookup_fields = ['product_name__icontains'] def __str__(self): return self.product_name @property def sample_image(self): return self.images.first() class Commodity(AvailableProductMixin, Product): """ This Commodity model inherits from polymorphic Product, and therefore has to be redefined. """ unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) quantity = models.PositiveIntegerField( _("Quantity"), default=0, validators=[MinValueValidator(0)], help_text=_("Available quantity in stock") ) # controlling the catalog placeholder = PlaceholderField("Commodity Details") show_breadcrumb = True # hard coded to always show the product's breadcrumb class Meta: verbose_name = _("Commodity") verbose_name_plural = _("Commodities") default_manager = ProductManager() def get_price(self, request): return self.unit_price class SmartCard(AvailableProductMixin, Product): unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) card_type = models.CharField( _("Card Type"), choices=[2 * ('{}{}'.format(s, t),) for t in ['SD', 'SDXC', 'SDHC', 'SDHC II'] for s in ['', 'micro ']], max_length=15, ) speed = models.CharField( _("Transfer Speed"), choices=[(str(s), "{} MB/s".format(s)) for s in [4, 20, 30, 40, 48, 80, 95, 280]], max_length=8, ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) storage = models.PositiveIntegerField( _("Storage Capacity"), help_text=_("Storage capacity in GB"), ) description = HTMLField( _("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', help_text=_("Long description for the detail view of this product."), ) quantity = models.PositiveIntegerField( _("Quantity"), default=0, validators=[MinValueValidator(0)], help_text=_("Available quantity in stock") ) class Meta: verbose_name = _("Smart Card") verbose_name_plural = _("Smart Cards") ordering = ['order'] # filter expression used to lookup for a product item using the Select2 widget lookup_fields = ['product_code__startswith', 'product_name__icontains'] def get_price(self, request): return self.unit_price default_manager = ProductManager() class OperatingSystem(models.Model): name = models.CharField( _("Name"), max_length=50, unique=True, ) def __str__(self): return self.name class SmartPhoneModel(Product): """ A generic smart phone model, which must be concretized by a `SmartPhoneVariant` - see below. """ BATTERY_TYPES = [ (1, "Lithium Polymer (Li-Poly)"), (2, "Lithium Ion (Li-Ion)"), ] WIFI_CONNECTIVITY = [ (1, "802.11 b/g/n"), ] BLUETOOTH_CONNECTIVITY = [ (1, "Bluetooth 4.0"), (2, "Bluetooth 3.0"), (3, "Bluetooth 2.1"), ] battery_type = models.PositiveSmallIntegerField( _("Battery type"), choices=BATTERY_TYPES, ) battery_capacity = models.PositiveIntegerField( _("Capacity"), help_text=_("Battery capacity in mAh"), ) ram_storage = models.PositiveIntegerField( _("RAM"), help_text=_("RAM storage in MB"), ) wifi_connectivity = models.PositiveIntegerField( _("WiFi"), choices=WIFI_CONNECTIVITY, help_text=_("WiFi Connectivity"), ) bluetooth = models.PositiveIntegerField( _("Bluetooth"), choices=BLUETOOTH_CONNECTIVITY, help_text=_("Bluetooth Connectivity"), ) gps = models.BooleanField( _("GPS"), default=False, help_text=_("GPS integrated"), ) operating_system = models.ForeignKey( OperatingSystem, on_delete=models.CASCADE, verbose_name=_("Operating System"), ) width = models.DecimalField( _("Width"), max_digits=4, decimal_places=1, help_text=_("Width in mm"), ) height = models.DecimalField( _("Height"), max_digits=4, decimal_places=1, help_text=_("Height in mm"), ) weight = models.DecimalField( _("Weight"), max_digits=5, decimal_places=1, help_text=_("Weight in gram"), ) screen_size = models.DecimalField( _("Screen size"), max_digits=4, decimal_places=2, help_text=_("Diagonal screen size in inch"), ) description = HTMLField( verbose_name=_("Description"), configuration='CKEDITOR_SETTINGS_DESCRIPTION', help_text=_( "Full description used in the catalog's detail view of Smart Phones."), ) class Meta: verbose_name = _("Smart Phone") verbose_name_plural = _("Smart Phones") default_manager = ProductManager() def get_price(self, request): """ Return the starting price for instances of this smart phone model. """ if not hasattr(self, '_price'): if self.variants.exists(): currency = self.variants.first().unit_price.currency aggr = self.variants.aggregate(models.Min('unit_price')) self._price = MoneyMaker(currency)(aggr['unit_price__min']) else: self._price = Money() return self._price def get_availability(self, request, **kwargs): variant = self.get_product_variant(**kwargs) return variant.get_availability(request) def deduct_from_stock(self, quantity, **kwargs): variant = self.get_product_variant(**kwargs) variant.deduct_from_stock(quantity) def is_in_cart(self, cart, watched=False, **kwargs): try: product_code = kwargs['product_code'] except KeyError: return cart_item_qs = CartItem.objects.filter(cart=cart, product=self) for cart_item in cart_item_qs: if cart_item.product_code == product_code: return cart_item def get_product_variant(self, **kwargs): try: product_code = kwargs.get('product_code') return self.variants.get(product_code=product_code) except SmartPhoneVariant.DoesNotExist as e: raise SmartPhoneModel.DoesNotExist(e) def get_product_variants(self): return self.variants.all() class SmartPhoneVariant(AvailableProductMixin, models.Model): product = models.ForeignKey( SmartPhoneModel, on_delete=models.CASCADE, verbose_name=_("Smartphone Model"), related_name='variants', ) product_code = models.CharField( _("Product code"), max_length=255, unique=True, ) unit_price = MoneyField( _("Unit price"), decimal_places=3, help_text=_("Net price for this product"), ) storage = models.PositiveIntegerField( _("Internal Storage"), help_text=_("Internal storage in GB"), ) quantity = models.PositiveIntegerField( _("Quantity"), default=0, validators=[MinValueValidator(0)], help_text=_("Available quantity in stock") ) def __str__(self): return _("{product} with {storage} GB").format(product=self.product, storage=self.storage) def get_price(self, request): return self.unit_price