Files
2026-05-16 21:09:39 +03:30

288 lines
12 KiB
Python

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)