feat: Add Telegram chat ID to ShopModel for automatic invoice sending
chore: Update Dockerfile to install WeasyPrint dependencies feat: Enhance ShopOrderModelAdmin with invoice download buttons feat: Implement invoice generation for OrderModel and ShopOrderModel feat: Send invoice to shop's Telegram chat upon ShopOrderModel creation feat: Create Celery task to send shop order invoice via Telegram feat: Add invoice download endpoints for OrderModel and ShopOrderModel feat: Implement views for downloading order and shop order invoices chore: Update requirements.txt to include necessary packages for PDF generation feat: Create HTML templates for order and shop order invoices
This commit is contained in:
+35
-2
@@ -12,6 +12,8 @@ from azbankgateways.models.banks import Bank
|
||||
from unfold.decorators import action
|
||||
from django.shortcuts import redirect
|
||||
from .permissons import ShopOrderAdminPermission
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
class OrderItemModelInline(StackedInline):
|
||||
model = OrderItemModel
|
||||
@@ -97,6 +99,22 @@ class ShopOrderItemInline(StackedInline):
|
||||
@admin.register(ShopOrderModel)
|
||||
class ShopOrderModelAdmin(ShopOrderAdminPermission, ModelAdmin):
|
||||
inlines = [ShopOrderItemInline]
|
||||
list_display = ['id', 'shop', 'order', 'customer_name', 'status', 'is_paid', 'is_settled', 'download_invoice_button']
|
||||
readonly_fields = ['download_invoice_link']
|
||||
|
||||
def download_invoice_button(self, obj):
|
||||
if obj.pk:
|
||||
url = reverse('download-shop-order-invoice', args=[obj.pk])
|
||||
return mark_safe(f'<a class="button" href="{url}" target="_blank" style="background-color: #28a745; color: white; border-color: #28a745;">دانلود فاکتور</a>')
|
||||
return '-'
|
||||
download_invoice_button.short_description = 'فاکتور'
|
||||
|
||||
def download_invoice_link(self, obj):
|
||||
if obj.pk:
|
||||
url = reverse('download-shop-order-invoice', args=[obj.pk])
|
||||
return mark_safe(f'<a href="{url}" target="_blank" style="background-color: #28a745; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">دانلود فاکتور PDF</a>')
|
||||
return '-'
|
||||
download_invoice_link.short_description = 'دانلود فاکتور'
|
||||
|
||||
|
||||
def get_queryset(self, request):
|
||||
@@ -118,8 +136,8 @@ class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
search_fields = ['user__phone', 'user__first_name', 'user__last_name', 'user__email']
|
||||
list_filter = ['is_paid', 'status']
|
||||
actions_list = ['redirect_to_learn', 'udpate_bank_status']
|
||||
list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address',]
|
||||
readonly_fields = ('created_at', 'tax', 'final_price', 'cart_total', 'discount_amount', 'discount_code', 'user', 'address', 'is_paid')
|
||||
list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address', 'download_invoice_button']
|
||||
readonly_fields = ('created_at', 'tax', 'final_price', 'cart_total', 'discount_amount', 'discount_code', 'user', 'address', 'is_paid', 'download_invoice_link')
|
||||
compressed_fields = True
|
||||
warn_unsaved_form = True
|
||||
# exclude = ('bank_records',)
|
||||
@@ -129,9 +147,24 @@ class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
|
||||
}
|
||||
}
|
||||
inlines = [OrderItemModelInline, BankRecordInline]
|
||||
|
||||
def order_id(self, obj):
|
||||
return f"سفارش {obj.pk + 1000}"
|
||||
order_id.short_description = "شماره سفارش"
|
||||
|
||||
def download_invoice_button(self, obj):
|
||||
if obj.pk:
|
||||
url = reverse('download-order-invoice', args=[obj.pk])
|
||||
return mark_safe(f'<a class="button" href="{url}" target="_blank" style="background-color: #28a745; color: white; border-color: #28a745;">دانلود فاکتور</a>')
|
||||
return '-'
|
||||
download_invoice_button.short_description = 'فاکتور'
|
||||
|
||||
def download_invoice_link(self, obj):
|
||||
if obj.pk:
|
||||
url = reverse('download-order-invoice', args=[obj.pk])
|
||||
return mark_safe(f'<a href="{url}" target="_blank" style="background-color: #28a745; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">دانلود فاکتور PDF</a>')
|
||||
return '-'
|
||||
download_invoice_link.short_description = 'دانلود فاکتور'
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Invoice generation utilities for OrderModel and ShopOrderModel.
|
||||
These functions generate PDF invoices from HTML templates.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
from django.template.loader import render_to_string
|
||||
from weasyprint import HTML
|
||||
from django.conf import settings
|
||||
import jdatetime
|
||||
|
||||
|
||||
def generate_order_invoice(order_id):
|
||||
"""
|
||||
Generate a PDF invoice for an OrderModel instance.
|
||||
|
||||
Args:
|
||||
order_id: The ID of the OrderModel instance
|
||||
|
||||
Returns:
|
||||
BytesIO: A BytesIO object containing the PDF data
|
||||
"""
|
||||
from .models import OrderModel
|
||||
|
||||
try:
|
||||
order = OrderModel.objects.select_related(
|
||||
'user', 'address', 'discount_code'
|
||||
).prefetch_related('items__product__product').get(pk=order_id)
|
||||
except OrderModel.DoesNotExist:
|
||||
raise ValueError(f"Order with ID {order_id} does not exist")
|
||||
|
||||
# Prepare items with calculated discount amounts
|
||||
items = order.items.all()
|
||||
items_with_discount = []
|
||||
for item in items:
|
||||
# Calculate the discount amount from the discount_percent
|
||||
# price is after discount, so we need to calculate original price
|
||||
# original_price = price / (1 - discount_percent/100)
|
||||
# discount_amount = original_price - price
|
||||
if item.discount_percent > 0:
|
||||
discount_multiplier = (100 - item.discount_percent) / 100
|
||||
price_after_discount = item.price
|
||||
price_before_discount = int(price_after_discount / discount_multiplier) if discount_multiplier > 0 else price_after_discount
|
||||
item_discount_amount = (price_before_discount - price_after_discount)
|
||||
else:
|
||||
item_discount_amount = 0
|
||||
price_before_discount = item.price
|
||||
|
||||
items_with_discount.append({
|
||||
'item': item,
|
||||
'discount_amount': item_discount_amount,
|
||||
'price_before_discount': price_before_discount
|
||||
})
|
||||
|
||||
# Prepare context for the template
|
||||
context = {
|
||||
'order': order,
|
||||
'order_number': order.pk + 1000,
|
||||
'items': items,
|
||||
'items_with_discount': items_with_discount,
|
||||
'user': order.user,
|
||||
'address': order.address,
|
||||
'discount_code': order.discount_code,
|
||||
'created_at_jalali': jdatetime.datetime.fromgregorian(datetime=order.created_at) if order.created_at else None,
|
||||
'total_items': sum(item.quantity for item in items),
|
||||
'subtotal': order.cart_total,
|
||||
'discount_amount': order.discount_amount or 0,
|
||||
'special_discount_total': order.special_discount_total or 0,
|
||||
'tax': order.tax or 0,
|
||||
'final_price': order.final_price,
|
||||
'is_paid': order.is_paid,
|
||||
'status': order.get_status_display(),
|
||||
}
|
||||
|
||||
# Render HTML template
|
||||
html_string = render_to_string('order/invoice_order.html', context)
|
||||
|
||||
# Generate PDF
|
||||
html = HTML(string=html_string, base_url=settings.STATIC_URL)
|
||||
pdf_file = BytesIO()
|
||||
html.write_pdf(pdf_file)
|
||||
pdf_file.seek(0)
|
||||
|
||||
return pdf_file
|
||||
|
||||
|
||||
def generate_shop_order_invoice(shop_order_id):
|
||||
"""
|
||||
Generate a PDF invoice for a ShopOrderModel instance.
|
||||
|
||||
Args:
|
||||
shop_order_id: The ID of the ShopOrderModel instance
|
||||
|
||||
Returns:
|
||||
BytesIO: A BytesIO object containing the PDF data
|
||||
"""
|
||||
from .models import ShopOrderModel
|
||||
|
||||
try:
|
||||
shop_order = ShopOrderModel.objects.select_related(
|
||||
'order', 'shop', 'shop__user', 'customer', 'address'
|
||||
).prefetch_related('items__order_item__product__product').get(pk=shop_order_id)
|
||||
except ShopOrderModel.DoesNotExist:
|
||||
raise ValueError(f"ShopOrder with ID {shop_order_id} does not exist")
|
||||
|
||||
# Prepare context for the template
|
||||
context = {
|
||||
'shop_order': shop_order,
|
||||
'order_number': shop_order.order.pk + 1000,
|
||||
'shop_order_id': shop_order.pk,
|
||||
'shop': shop_order.shop,
|
||||
'customer': shop_order.customer,
|
||||
'customer_name': shop_order.customer_name,
|
||||
'customer_phone': shop_order.customer_phone,
|
||||
'address_text': shop_order.address_text,
|
||||
'address_postal_code': shop_order.address_postal_code,
|
||||
'address_phone': shop_order.address_phone,
|
||||
'address_city': shop_order.address_city,
|
||||
'address_province': shop_order.address_province,
|
||||
'address_recipient_name': shop_order.address_recipient_name,
|
||||
'items': shop_order.items.all(),
|
||||
'created_at_jalali': jdatetime.datetime.fromgregorian(datetime=shop_order.order_created_at) if shop_order.order_created_at else None,
|
||||
'total_items': shop_order.items_count,
|
||||
'subtotal': shop_order.subtotal,
|
||||
'discount_amount': shop_order.discount_amount,
|
||||
'special_discount_amount': shop_order.special_discount_amount,
|
||||
'tax_amount': shop_order.tax_amount,
|
||||
'commission_percent': shop_order.commission_percent,
|
||||
'commission_amount': shop_order.commission_amount,
|
||||
'payable_amount': shop_order.payable_amount,
|
||||
'is_paid': shop_order.is_paid,
|
||||
'status': shop_order.get_status_display(),
|
||||
'is_settled': shop_order.is_settled,
|
||||
}
|
||||
|
||||
# Render HTML template
|
||||
html_string = render_to_string('order/invoice_shop_order.html', context)
|
||||
|
||||
# Generate PDF
|
||||
html = HTML(string=html_string, base_url=settings.STATIC_URL)
|
||||
pdf_file = BytesIO()
|
||||
html.write_pdf(pdf_file)
|
||||
pdf_file.seek(0)
|
||||
|
||||
return pdf_file
|
||||
@@ -6,7 +6,8 @@ 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
|
||||
from .tasks import send_change_status_notif, send_change_status_sms, send_shop_order_invoice_telegram_task
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@receiver(pre_save, sender=OrderModel)
|
||||
@@ -180,3 +181,37 @@ def create_shop_orders_on_payment(sender, instance: OrderModel, created, **kwarg
|
||||
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
|
||||
)
|
||||
+60
-1
@@ -119,4 +119,63 @@ def generate_daily_shop_reports():
|
||||
|
||||
result = f'Generated reports for {target_date}: {reports_created} created, {reports_updated} updated'
|
||||
logging.info(result)
|
||||
return result
|
||||
return result
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_shop_order_invoice_telegram_task(shop_order_id, chat_id, bot_token):
|
||||
"""Send shop order invoice PDF to Telegram chat.
|
||||
|
||||
Args:
|
||||
shop_order_id: ID of the ShopOrderModel
|
||||
chat_id: Telegram chat ID to send invoice to
|
||||
bot_token: Telegram bot token for authentication
|
||||
|
||||
Returns:
|
||||
Success or error message
|
||||
"""
|
||||
import asyncio
|
||||
import io
|
||||
from telegram import Bot
|
||||
from telegram.error import TelegramError
|
||||
from .invoice_generator import generate_shop_order_invoice
|
||||
from .models import ShopOrderModel
|
||||
|
||||
try:
|
||||
# Get the shop order
|
||||
shop_order = ShopOrderModel.objects.get(pk=shop_order_id)
|
||||
|
||||
# Generate invoice PDF
|
||||
pdf_buffer = generate_shop_order_invoice(shop_order_id)
|
||||
|
||||
# Reset buffer position
|
||||
pdf_buffer.seek(0)
|
||||
|
||||
# Send via Telegram
|
||||
async def send_invoice():
|
||||
bot = Bot(token=bot_token)
|
||||
await bot.send_document(
|
||||
chat_id=chat_id,
|
||||
document=pdf_buffer,
|
||||
filename=f'invoice_shop_order_{shop_order_id}.pdf',
|
||||
caption=f'فاکتور سفارش #{shop_order_id}\n{shop_order.shop.shop_name}'
|
||||
)
|
||||
|
||||
# Run async function
|
||||
asyncio.run(send_invoice())
|
||||
|
||||
logging.info(f'Successfully sent shop order invoice {shop_order_id} to Telegram chat {chat_id}')
|
||||
return f'Invoice sent successfully to chat {chat_id}'
|
||||
|
||||
except ShopOrderModel.DoesNotExist:
|
||||
error_msg = f'ShopOrderModel with id {shop_order_id} does not exist'
|
||||
logging.error(error_msg)
|
||||
return error_msg
|
||||
except TelegramError as e:
|
||||
error_msg = f'Telegram error sending invoice {shop_order_id}: {str(e)}'
|
||||
logging.error(error_msg)
|
||||
return error_msg
|
||||
except Exception as e:
|
||||
error_msg = f'Error sending invoice {shop_order_id} to Telegram: {str(e)}'
|
||||
logging.error(error_msg)
|
||||
return error_msg
|
||||
|
||||
@@ -15,4 +15,8 @@ urlpatterns = [
|
||||
path('transaction/<int:tracking_code>',
|
||||
CallbackView.as_view(), name='callback-gateway'),
|
||||
path('<int:pk>', OrderGetView.as_view(), name='order-get'),
|
||||
|
||||
# Invoice download endpoints
|
||||
path('invoice/order/<int:order_id>/download', download_order_invoice, name='download-order-invoice'),
|
||||
path('invoice/shop-order/<int:shop_order_id>/download', download_shop_order_invoice, name='download-shop-order-invoice'),
|
||||
]
|
||||
|
||||
@@ -508,3 +508,59 @@ class SetAddressForCartView(APIView):
|
||||
cart_order.address = address_object
|
||||
cart_order.save()
|
||||
return Response({'detail': 'ادرس با موفقیت انتخاب شد'})
|
||||
|
||||
|
||||
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseNotFound
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .invoice_generator import generate_order_invoice, generate_shop_order_invoice
|
||||
|
||||
|
||||
@login_required
|
||||
def download_order_invoice(request, order_id):
|
||||
"""
|
||||
Download invoice PDF for a specific OrderModel.
|
||||
Requires superuser permissions.
|
||||
"""
|
||||
if not request.user.is_superuser:
|
||||
return HttpResponseForbidden("شما اجازه دسترسی به این فاکتور را ندارید")
|
||||
|
||||
try:
|
||||
pdf_file = generate_order_invoice(order_id)
|
||||
|
||||
response = HttpResponse(pdf_file.read(), content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="order_invoice_{order_id + 1000}.pdf"'
|
||||
|
||||
return response
|
||||
except ValueError as e:
|
||||
return HttpResponseNotFound(f"خطا: {str(e)}")
|
||||
except Exception as e:
|
||||
return HttpResponse(f"خطا در ایجاد فاکتور: {str(e)}", status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
def download_shop_order_invoice(request, shop_order_id):
|
||||
"""
|
||||
Download invoice PDF for a specific ShopOrderModel.
|
||||
Shop owners can only download their own invoices, admins can download all.
|
||||
"""
|
||||
from .models import ShopOrderModel
|
||||
|
||||
try:
|
||||
shop_order = ShopOrderModel.objects.get(pk=shop_order_id)
|
||||
|
||||
# Check permissions
|
||||
if not request.user.is_staff and not request.user.is_superuser:
|
||||
if not hasattr(request.user, 'shop') or request.user.shop != shop_order.shop:
|
||||
return HttpResponseForbidden("شما اجازه دسترسی به این فاکتور را ندارید")
|
||||
|
||||
pdf_file = generate_shop_order_invoice(shop_order_id)
|
||||
|
||||
response = HttpResponse(pdf_file.read(), content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="shop_order_invoice_{shop_order_id}.pdf"'
|
||||
|
||||
return response
|
||||
except ShopOrderModel.DoesNotExist:
|
||||
return HttpResponseNotFound("فاکتور مورد نظر یافت نشد")
|
||||
except Exception as e:
|
||||
return HttpResponse(f"خطا در ایجاد فاکتور: {str(e)}", status=500)
|
||||
|
||||
Reference in New Issue
Block a user