from .models import ShopOrderModel, ShopOrderItem, ShopDailyReport from django.db import transaction from django.db.models.signals import post_save from django.db.models.signals import pre_save from django.dispatch import receiver from .models import OrderModel from account.models import PushSubscription, UserAddressModel import ghasedak_sms from .tasks import send_change_status_notif, send_change_status_sms, send_shop_order_invoice_telegram_task from django.conf import settings from decimal import Decimal import logging logger = logging.getLogger(__name__) @receiver(pre_save, sender=OrderModel) def order_status_changed(sender, instance, **kwargs): if instance.pk: previous = OrderModel.objects.get(pk=instance.pk) if previous.status != instance.status: new_status = instance.get_status_display() # send_change_status_notif.delay(instance.pk, new_status) # send_change_status_sms.delay(instance.pk, new_status) if previous.status == 'CART' and instance.status == 'ADMIN_PENDING': # update_cart_price_fields() # update_sell_data() # update_quantity() pass @receiver(pre_save, sender=OrderModel) def set_default_address(sender, instance, **kwargs): if instance.address is None and instance.user: default_address = UserAddressModel.objects.filter( user=instance.user, is_main=True).first() if default_address: instance.address = default_address # def update_cart_price_fields(order): # pass def update_sell_data(order): pass def update_quantity(order): pass @receiver(post_save, sender=OrderModel) def create_shop_orders_on_payment(sender, instance: OrderModel, created, **kwargs): """When an order becomes paid, split it into per-shop ShopOrderModel records. This handler is safe to run multiple times (it checks if shop_orders exist). It triggers only when `is_paid` is True. """ # Only generate when order is paid and we don't already have shop orders if not instance.is_paid: return if instance.shop_orders.exists(): return # Collect order items grouped by shop items = instance.items.select_related('product__product__shop') shop_groups = {} for item in items: # product is ProductVariant -> product.product is ProductModel -> shop on ProductModel shop = None try: shop = item.product.product.shop except Exception: shop = None if not shop: # If product has no shop, skip (or you might want a default platform shop) continue shop_groups.setdefault(shop, []).append(item) if not shop_groups: return # Totals for proportional distribution (for cart-level discount and tax only) shop_subtotals = {} for shop, items_list in shop_groups.items(): subtotal = 0 for it in items_list: subtotal += int(it.price) * int(it.quantity) shop_subtotals[shop] = subtotal total_subtotal = sum(shop_subtotals.values()) or 1 order_discount = int(instance.discount_amount or 0) order_tax = int(instance.tax or 0) with transaction.atomic(): for shop, items_list in shop_groups.items(): shop_subtotal = shop_subtotals.get(shop, 0) # Calculate total item-level discounts for this shop (discount_percent) item_level_discounts = Decimal('0') for it in items_list: item_discount = Decimal(str(it.price)) * Decimal(str(it.quantity)) * (Decimal(str(it.discount_percent or 0)) / Decimal('100')) item_level_discounts += item_discount # Calculate total item-level special discounts for this shop # (already calculated as profit * special_discount_percent) item_special_discounts = Decimal('0') for it in items_list: item_special_discounts += Decimal(str(it.special_discount_amount or 0)) # Proportionally allocate cart-level discount allocated_discount = (Decimal(str(order_discount)) * Decimal(str(shop_subtotal)) / Decimal(str(total_subtotal))) if order_discount else Decimal('0') # Calculate tax on shop's amount after all discounts # Tax rate from settings (default 10%) tax_rate = Decimal(str(getattr(settings, 'DEFAULT_TAX_RATE', 10))) base_for_tax = max( Decimal('0'), Decimal(str(shop_subtotal)) - item_level_discounts - item_special_discounts - allocated_discount ) allocated_tax = base_for_tax * (tax_rate / Decimal('100')) commission_percent = getattr(shop, 'commission_percent', 0) or 0 try: commission_percent_value = Decimal(str(commission_percent)) except Exception: commission_percent_value = Decimal('0') # Commission is calculated on the subtotal after ALL discounts: # subtotal - item_discount - item_special_discount - cart_discount base_for_commission = max( Decimal('0'), Decimal(str(shop_subtotal)) - item_level_discounts - item_special_discounts - allocated_discount ) commission_amount = base_for_commission * (commission_percent_value / Decimal('100')) # Payable to shop: subtotal minus all discounts minus commission (no tax added to payable) payable = Decimal(str(shop_subtotal)) - item_level_discounts - item_special_discounts - \ allocated_discount - commission_amount # Prepare customer information customer_phone = (instance.user.phone or '') if instance.user else '' customer_name = (instance.user.full_name or '') if instance.user else '' # Prepare address information (with text backups in case address is deleted) address_text = '' address_postal_code = '' address_phone = '' address_city = '' address_province = '' address_recipient_name = '' if instance.address: address_text = instance.address.address or '' address_postal_code = instance.address.postal_code or '' address_phone = instance.address.phone or '' address_city = instance.address.city or '' address_province = instance.address.province or '' address_recipient_name = instance.address.name or '' # Convert Jalali date to datetime if needed order_created_datetime = None if instance.created_at: try: # If it's already a datetime, use it if hasattr(instance.created_at, 'hour'): order_created_datetime = instance.created_at else: # If it's a date, convert to datetime at midnight from datetime import datetime, time order_created_datetime = datetime.combine(instance.created_at, time.min) except Exception: order_created_datetime = None shop_order = ShopOrderModel.objects.create( order=instance, shop=shop, customer=instance.user, customer_phone=customer_phone, customer_name=customer_name, address=instance.address, address_text=address_text, address_postal_code=address_postal_code, address_phone=address_phone, address_city=address_city, address_province=address_province, address_recipient_name=address_recipient_name, status=instance.status, is_paid=instance.is_paid, subtotal=shop_subtotal, items_count=sum(int(it.quantity) for it in items_list), discount_amount=int(allocated_discount), special_discount_code=instance.special_discount_code, special_discount_amount=int(item_special_discounts), commission_percent=float(commission_percent_value), commission_amount=int(commission_amount), tax_amount=int(allocated_tax), payable_amount=int(payable), order_created_at=order_created_datetime, ) # Create ShopOrderItem rows linking to original OrderItemModel for it in items_list: item_discount_amount = Decimal(str(it.price)) * Decimal(str(it.quantity)) * (Decimal(str(it.discount_percent or 0)) / Decimal('100')) ShopOrderItem.objects.create( shop_order=shop_order, order_item=it, quantity=int(it.quantity), unit_price=int(it.price), total_price=int(it.price) * int(it.quantity), discount_amount=int(item_discount_amount), special_discount_amount=int(it.special_discount_amount or 0), ) shop_list = [] for order_item in instance.items.all(): shop_to_add = order_item.product.product.shop shop_list.append(shop_to_add) shop_list = set(shop_list) for shop in shop_list: sms_api = ghasedak_sms.Ghasedak(api_key="8f7396f1e3c39e3a4621009c558d955336eea6d21cf257dd74ae262d6f22a458XdoDjH6egJsiZsy8") newotpcommand = ghasedak_sms.SendOtpInput( receptors=[ ghasedak_sms.SendOtpReceptorDto( mobile=f'{shop.user.phone}', client_reference_id=str(shop.user.id) ) ], template_name='neworder', inputs=[ ghasedak_sms.SendOtpInput.OtpInput(param='ShopName', value=f'{shop.shop_name}'), ], udh=False ) response = sms_api.send_otp_sms(newotpcommand) if response['statusCode'] == 200: logger.error("sent order notice to shop owner") else: logger.error(f"faield to send order id {response}") @receiver(post_save, sender=ShopOrderModel) def send_invoice_to_shop_telegram(sender, instance: ShopOrderModel, created, **kwargs): """Automatically send invoice to shop's Telegram chat when ShopOrderModel is created. This handler triggers when a new ShopOrderModel is created and the shop has a telegram_chat_id configured. It sends the invoice PDF asynchronously via Celery task. """ if not created: return # Check if shop has telegram_chat_id configured if not instance.shop or not instance.shop.telegram_chat_id: return # Get bot token from settings bot_token = getattr(settings, 'TELEGRAM_BOT_TOKEN', None) if not bot_token: return # Send invoice asynchronously try: send_shop_order_invoice_telegram_task.delay( shop_order_id=instance.pk, chat_id=instance.shop.telegram_chat_id, bot_token=bot_token ) except Exception as e: send_shop_order_invoice_telegram_task( shop_order_id=instance.pk, chat_id=instance.shop.telegram_chat_id, bot_token=bot_token ) @receiver(post_save, sender=ShopDailyReport) def update_shop_orders_settlement_status(sender, instance: ShopDailyReport, **kwargs): """When a ShopDailyReport's is_settled status changes, update all related ShopOrderModel instances.""" # Update all shop orders linked to this daily report instance.shop_orders.update(is_settled=instance.is_settled)