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 RedeemingProduct(ContributorModel): """ This model store products which allow to redeem by point or coupon. """ objects = RedeemingProductQuerySet.as_manager() product = fields.ForeignKey( Product, on_delete=models.CASCADE, ) kind = fields.IntegerField( choices=RedeemKindsEnum.to_tuple(), default=RedeemKindsEnum.NA.value, ) start_date = fields.DateTimeField( null=True, blank=True, ) end_date = fields.DateTimeField( null=True, blank=True, ) @property def is_expired(self): import datetime now = datetime.datetime.now().replace(tzinfo=pytz.UTC) if not self.end_date: return False return self.end_date.replace(tzinfo=pytz.UTC) > now
class User(AbstractUser): """ User model. """ role = fields.IntegerField( choices=UserRolesEnum.to_tuple(), default=UserRolesEnum.CUSTOMER_ROLE.value, ) @property def is_user(self): return hasattr(self, 'role') and self.role in \ [UserRolesEnum.USER_ROLE.value] @property def is_admin(self): return hasattr(self, 'role') and self.role in \ [UserRolesEnum.ADMIN_ROLE.value] @property def is_customer(self): return hasattr(self, 'role') and self.role in \ [UserRolesEnum.CUSTOMER_ROLE.value] def save(self, *args, **kwargs) -> None: """ Create new user and also create customer. If user is existed, update user and update related customer. """ try: user_full_name = self.get_full_name() if self.get_full_name() \ else self.username # New object if self.pk is None: super(User, self).save(*args, **kwargs) email_obj = Email.objects.create(email=self.email) Customer.objects.create( email=email_obj, account=self, name=user_full_name, ) # Update existing object. else: super(User, self).save(*args, **kwargs) customer_obj = Customer.objects.get(account=self) if customer_obj.email is not None: customer_obj.email.email = self.email customer_obj.email.save() customer_obj.name = user_full_name customer_obj.save() except Exception as e: raise e
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 AssigningPoint(ContributorModel): """ AssigningPoint model. """ objects = AssigningPointQuerySet.as_manager() customer = fields.ForeignKey( Customer, on_delete=models.CASCADE, ) point = fields.IntegerField( default=0, validators=[MinValueValidator(0)], help_text='Assigning point which assigned to customer.') description = fields.LongDescField()
class GiftItem(ContributorModel): """ Gift Item model """ objects = GiftItemQuerySet.as_manager() product = fields.OneToOneField( Product, on_delete=models.CASCADE, ) gift = fields.ForeignKey( Gift, on_delete=models.CASCADE, ) quantity = fields.IntegerField( verbose_name='Number of item', default=0, validators=[MinValueValidator(0)], )
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 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
class Customer(ContributorModel): """ Customer model. """ objects = CustomerQuerySet.as_manager() name = fields.ShortNameField(verbose_name='Customer name', ) account = fields.OneToOneField( 'accounts.User', on_delete=models.CASCADE, ) membership = fields.ForeignKey( Membership, on_delete=models.CASCADE, null=True, blank=True, ) email = models.OneToOneField( Email, on_delete=models.CASCADE, ) phone_number = fields.PhoneNumberField() available_point = fields.IntegerField( help_text='Current point', default=0, null=False, blank=False, validators=[MinValueValidator(0)], ) total_earned_point = fields.IntegerField( help_text='Total earned point from the beginning', default=0, null=False, blank=False, validators=[MinValueValidator(0)], ) is_active = models.BooleanField( help_text='Indicate that customer is active or not. ' 'True if active; otherwise, False', default=True, ) def __str__(self): return f'customer: {self.id}' def clean(self): """ Don't allow `total_earned_point` is less than `available_point` """ if self.total_earned_point < self.available_point: total_earned_point_msg = '`total_earned_point` must be greater ' \ 'than or equal `available_point`' available_point_msg = '`available_point` must be less ' \ 'than or equal `total_earned_point`' raise ValidationError({ 'total_earned_point': ValidationError(_(total_earned_point_msg)), 'available_point': ValidationError(_(available_point_msg)) }) def save(self, **kwargs): """ Save the customer information. Check and set membership level for customer based on the total earned point. """ try: level_obj = MembershipLevel.objects.filter( require_point__lte=self.total_earned_point).order_by( '-require_point').first() if level_obj: if self.membership: self.membership.level = level_obj self.membership.save() else: level_filtered = Membership.objects.filter(level=level_obj)\ .first() self.membership = level_filtered if level_filtered else \ Membership.objects.create(level=level_obj) super(Customer, self).save(**kwargs) except Exception as e: raise e