251 lines
10 KiB
Python
251 lines
10 KiB
Python
from .models import ShopOrderModel, ShopOrderItem
|
|
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
|
|
|
|
|
|
@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),
|
|
)
|
|
|
|
|
|
@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
|
|
) |