class Product(ProductCommonInfoModel): objects = ProductQuerySet.as_manager() price = fields.FloatField(validators=[MinValueValidator(0)], ) quantity = fields.IntegerField(validators=[MinValueValidator(0)], ) sold_quantity = fields.IntegerField(validators=[MinValueValidator(0)], ) category = fields.ForeignKey( Category, verbose_name='Product category', on_delete=models.SET_NULL, null=True, blank=True, ) gender = fields.ForeignKey( Gender, verbose_name='Product gender', on_delete=models.SET_NULL, null=True, blank=True, ) base_colour = fields.ForeignKey( BaseColour, verbose_name='Product base colour', on_delete=models.SET_NULL, null=True, blank=True, ) season = fields.ForeignKey( Season, verbose_name='Season', on_delete=models.SET_NULL, null=True, blank=True, ) usage = fields.ForeignKey( Usage, verbose_name='Product usage', on_delete=models.SET_NULL, null=True, blank=True, ) article_type = fields.ForeignKey( ArticleType, verbose_name='Article type', on_delete=models.SET_NULL, null=True, blank=True, ) is_published = fields.BooleanField( default=False, help_text='This is a flag which is used to determine that product is' 'published or not. `False`: not published; otherwise, `True`.') def __str__(self): return super().__str__()
class MembershipLevel(ContributorModel): """ MembershipLevel model """ objects = MembershipLevelQuerySet.as_manager() previous = fields.ForeignKey( 'self', on_delete=models.SET_NULL, related_name='level_previous', null=True, blank=True, ) next = fields.ForeignKey( 'self', on_delete=models.SET_NULL, related_name='level_next', null=True, blank=True, ) name = fields.ShortNameField( null=False, blank=False, unique=True, ) require_point = fields.IntegerField( default=0, validators=[MinValueValidator(0)], help_text='Require point to reach the membership level.') earning_point_rate = fields.FloatField( default=1, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text='The rate to calculate the earning point for customer based' 'on the total amount on the bill.', ) burning_point_rate = fields.FloatField( default=1, validators=[MinValueValidator(0), MaxValueValidator(1)], help_text='The rate to calculate amount when customer uses point to' 'redeem products.', )
class Transaction(ContributorModel): """ Transaction model """ objects = TransactionQuerySet.as_manager() order = fields.OneToOneField( Order, on_delete=models.CASCADE, help_text='The order which transaction belong to', ) user = fields.ForeignKey( 'accounts.User', on_delete=models.CASCADE, help_text='The user who own this transaction', ) coupon = fields.ForeignKey( Coupon, on_delete=models.CASCADE, null=True, blank=True, ) total_amount = fields.FloatField( default=0, validators=[MinValueValidator(0)], help_text='Total amount of an order', ) total_pay_amount = fields.FloatField( default=0, validators=[MinValueValidator(0)], help_text='The actual amount that customer need to pay', ) earning_point = fields.IntegerField( validators=[MinValueValidator(0)], default=0, help_text='Number of point of an order that customer earns', ) burning_point = fields.IntegerField( validators=[MinValueValidator(0)], default=0, help_text='Number of point that customer spend to redeem product', )
class Coupon(ContributorModel): """ Coupon model. """ objects = CouponQuerySet.as_manager() reward = fields.ForeignKey( 'Reward', on_delete=models.CASCADE, help_text='The reward that coupon belong to', ) kind = fields.IntegerField( verbose_name='Coupon kind', choices=CouponKindsEnum.to_tuple(), default=CouponKindsEnum.PERCENTAGE.value, ) code = fields.CharField( verbose_name='Reward unique coupon code', max_length=10, unique=True, ) start_date = models.DateTimeField( verbose_name='Valid date time', null=True, blank=True, ) end_date = models.DateTimeField( verbose_name='Expire date time', null=True, blank=True, ) amount = fields.FloatField( help_text='Depend on coupon kind. If coupon kind is percentage coupon, ' 'its value must be between 0 to 100. If coupon kind is money,' 'its value must be greater than 0', validators=[MinValueValidator(0)], ) target_amount = fields.FloatField( help_text='How much need to be bought to use coupon.', validators=[MinValueValidator(0)], ) is_minimum_purchase = fields.BooleanField( default=True, help_text='This flag is used to determine when customer can use coupon.' '`True`: Coupon can be used when total amount on bill is ' 'greater than or equal `target amount`.' '`False`: Coupon can be used when total amount on bill is' 'less than target amount.', ) is_one_time_using = fields.BooleanField( default=True, help_text='True if coupon is allowed one time using, ' '`False` if it can be used all times.', ) can_by_any_product = fields.BooleanField( default=False, help_text='`True`: coupon can buy any product. ' '`False`: coupon can buy products in the allowed list only.', ) is_active = fields.BooleanField( default=True, help_text='`True` if coupon is active; otherwise, `False`.', ) is_expired = fields.BooleanField( default=False, help_text='`True` if coupon is expired; otherwise, `False`.', ) def __str__(self): return f'coupon:{self.id}' def save(self, **kwargs): if not self.code: self.code = uuid.uuid4() super().save(**kwargs)
class Order(ContributorModel): """ Order model """ objects = OrderQuerySet.as_manager() user = fields.ForeignKey( 'accounts.User', on_delete=models.CASCADE, help_text='The owner of the order', ) status = fields.IntegerField( choices=OrderStatusesEnum.to_tuple(), default=OrderStatusesEnum.SHOPPING.value, help_text='The order status', ) total_amount = fields.FloatField( default=0, validators=[MinValueValidator(0)], help_text='Total amount before applying coupon and points', ) shipping_address = fields.ForeignKey( AddressBook, on_delete=models.SET_NULL, null=True, default=None, help_text='The shipping address', ) coupon = fields.ForeignKey( Coupon, on_delete=models.SET_NULL, null=True, default=None, help_text='The coupon', ) burning_point = fields.IntegerField( default=0, help_text='Number of points which used to redeem products', ) @property def total_pay_amount(self) -> float: """ Calculate the total payment amount @return: Total payment amount """ if self.burning_point: customer_obj = Customer.objects.get(account=self.user) burning_point_rate = customer_obj.membership.level.\ burning_point_rate return self.total_amount - burning_point_rate * self.burning_point else: return self.total_amount @property def num_of_items(self) -> int: """ Number of items in the cart @return: The number of order/cart items """ return OrderItem.objects.non_archived_only().\ filter(order=self.id).count() def __str__(self): return f'order-{self.id}' def clean(self): """ Don't allow `total_pay_amount` greater than `total_amount` """ if self.total_amount < self.total_pay_amount: total_amount_msg = '`total_amount` must be greater ' \ 'than or equal `total_pay_amount`' total_pay_amount_msg = '`total_pay_amount` must be less ' \ 'than or equal `total_amount`' raise ValidationError({ 'total_pay_amount': ValidationError(_(total_pay_amount_msg)), 'total_amount': ValidationError(_(total_amount_msg)) }) def save(self, **kwargs): """ Save an order and auto create transaction based on the order status. If order is paid, transaction is created if not existed. Otherwise, just save order only TODO: Need to calculate total pay amount when applying coupon. Temporary skip this task. """ try: # If order status is not paid status. Just save and by pass. if self.status is not OrderStatusesEnum.PAID.value: super(Order, self).save(**kwargs) return self._create_transaction(**kwargs) except Exception as e: raise e def _create_transaction(self, **kwargs): """ Auto create a transaction if the order status is paid """ # Get customer info who has this order membership_level = None customer_obj = Customer.objects.get(account=self.user) if customer_obj and customer_obj.membership: membership_level = customer_obj.membership.level earning_point_rate = membership_level.earning_point_rate \ if membership_level else 0 earning_point = int(self.total_pay_amount * earning_point_rate) transaction_data = { 'order': self, 'user': self.user, 'total_amount': self.total_amount, 'total_pay_amount': self.total_pay_amount, 'earning_point': earning_point, 'created_by': self.last_modified_by, 'last_modified_by': self.last_modified_by, } # Update earning point for customer. customer_obj.total_earned_point += earning_point customer_obj.available_point += earning_point customer_obj.save() # If order status is paid status, check and create transaction. if self.pk is None: super(Order, self).save(**kwargs) Transaction.objects.create(**transaction_data) else: old_instance = Order.objects.get(pk=self.pk) super(Order, self).save(**kwargs) transaction = Transaction.objects.filter(order=self) if old_instance.status is not OrderStatusesEnum.PAID.value \ and len(transaction) == 0: Transaction.objects.create(**transaction_data)
class OrderItem(ContributorModel): """ Order Item model """ objects = OrderItemQuerySet.as_manager() order = fields.ForeignKey( Order, on_delete=models.CASCADE, ) product = fields.ForeignKey( Product, on_delete=models.CASCADE, ) quantity = fields.IntegerField(validators=[MinValueValidator(1)], ) copy_price = fields.FloatField( default=0, null=True, ) @property def price(self): if not self.copy_price: self.copy_price = self.product.price return self.copy_price @property def amount(self): return self.price * self.quantity def __str__(self): return f'order-item-{self.id}' def delete(self, using=None, keep_parents=False): """ Delete an order item and update the order information. """ try: super(OrderItem, self).delete(using, keep_parents) self.order.total_amount = models.F('total_amount') - self.amount self.order.save() except Exception as e: raise e def save(self, **kwargs): """ Save an order item and update the order information. """ try: if self.pk is None: super(OrderItem, self).save(**kwargs) self.order.total_amount += self.amount self.order.save() else: old_order_item = OrderItem.objects.get(pk=self.pk) super(OrderItem, self).save(**kwargs) if old_order_item.amount is not self.amount: self.order.total_amount = self.order.total_amount - \ old_order_item.amount + \ self.amount self.order.save() except Exception as e: raise e