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:
Parsa Nazer
2025-12-28 11:43:33 +03:30
parent 6a7e526f23
commit 34715994ce
12 changed files with 1168 additions and 5 deletions
@@ -0,0 +1,18 @@
# Generated by Django 5.1.2 on 2025-12-28 08:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0033_shopmodel_commission_percent'),
]
operations = [
migrations.AddField(
model_name='shopmodel',
name='telegram_chat_id',
field=models.CharField(blank=True, help_text='برای ارسال خودکار فاکتورها به تلگرام', max_length=100, null=True, verbose_name='شناسه چت تلگرام'),
),
]
+1
View File
@@ -143,6 +143,7 @@ class ShopModel(models.Model):
shop_name = models.CharField(max_length=100, verbose_name='نام فروشگاه') shop_name = models.CharField(max_length=100, verbose_name='نام فروشگاه')
shop_description = models.TextField(verbose_name='توضیحات فروشگاه') shop_description = models.TextField(verbose_name='توضیحات فروشگاه')
commission_percent = models.DecimalField(max_digits=5, decimal_places=2, verbose_name='درصد کمیسیون') commission_percent = models.DecimalField(max_digits=5, decimal_places=2, verbose_name='درصد کمیسیون')
telegram_chat_id = models.CharField(max_length=100, blank=True, null=True, verbose_name='شناسه چت تلگرام', help_text='برای ارسال خودکار فاکتورها به تلگرام')
def __str__(self): def __str__(self):
return f"{self.user.phone} - {self.shop_name}" return f"{self.user.phone} - {self.shop_name}"
+11 -1
View File
@@ -5,8 +5,18 @@ ENV PYTHONUNBUFFERED 1
WORKDIR /app WORKDIR /app
# Install system dependencies for WeasyPrint
RUN apt-get update && apt-get install -y \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf-2.0-0 \
libffi-dev \
shared-mime-info \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . /app/ COPY . /app/
+35 -2
View File
@@ -12,6 +12,8 @@ from azbankgateways.models.banks import Bank
from unfold.decorators import action from unfold.decorators import action
from django.shortcuts import redirect from django.shortcuts import redirect
from .permissons import ShopOrderAdminPermission from .permissons import ShopOrderAdminPermission
from django.urls import reverse
from django.utils.safestring import mark_safe
class OrderItemModelInline(StackedInline): class OrderItemModelInline(StackedInline):
model = OrderItemModel model = OrderItemModel
@@ -97,6 +99,22 @@ class ShopOrderItemInline(StackedInline):
@admin.register(ShopOrderModel) @admin.register(ShopOrderModel)
class ShopOrderModelAdmin(ShopOrderAdminPermission, ModelAdmin): class ShopOrderModelAdmin(ShopOrderAdminPermission, ModelAdmin):
inlines = [ShopOrderItemInline] 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): 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'] search_fields = ['user__phone', 'user__first_name', 'user__last_name', 'user__email']
list_filter = ['is_paid', 'status'] list_filter = ['is_paid', 'status']
actions_list = ['redirect_to_learn', 'udpate_bank_status'] actions_list = ['redirect_to_learn', 'udpate_bank_status']
list_display = ['order_id', 'user', 'is_paid', 'status', 'discount_code', 'address',] 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') readonly_fields = ('created_at', 'tax', 'final_price', 'cart_total', 'discount_amount', 'discount_code', 'user', 'address', 'is_paid', 'download_invoice_link')
compressed_fields = True compressed_fields = True
warn_unsaved_form = True warn_unsaved_form = True
# exclude = ('bank_records',) # exclude = ('bank_records',)
@@ -129,10 +147,25 @@ class OrderAdmin(ModelAdmin, ImportExportModelAdmin):
} }
} }
inlines = [OrderItemModelInline, BankRecordInline] inlines = [OrderItemModelInline, BankRecordInline]
def order_id(self, obj): def order_id(self, obj):
return f"سفارش {obj.pk + 1000}" return f"سفارش {obj.pk + 1000}"
order_id.short_description = "شماره سفارش" 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 = 'دانلود فاکتور'
def get_search_results(self, request, queryset, search_term): def get_search_results(self, request, queryset, search_term):
+146
View File
@@ -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
+36 -1
View File
@@ -6,7 +6,8 @@ from django.dispatch import receiver
from .models import OrderModel from .models import OrderModel
from account.models import PushSubscription, UserAddressModel from account.models import PushSubscription, UserAddressModel
import ghasedak_sms 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) @receiver(pre_save, sender=OrderModel)
@@ -180,3 +181,37 @@ def create_shop_orders_on_payment(sender, instance: OrderModel, created, **kwarg
special_discount_amount=int( special_discount_amount=int(
it.special_discount_amount or 0), 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
)
+59
View File
@@ -120,3 +120,62 @@ def generate_daily_shop_reports():
result = f'Generated reports for {target_date}: {reports_created} created, {reports_updated} updated' result = f'Generated reports for {target_date}: {reports_created} created, {reports_updated} updated'
logging.info(result) 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
+4
View File
@@ -15,4 +15,8 @@ urlpatterns = [
path('transaction/<int:tracking_code>', path('transaction/<int:tracking_code>',
CallbackView.as_view(), name='callback-gateway'), CallbackView.as_view(), name='callback-gateway'),
path('<int:pk>', OrderGetView.as_view(), name='order-get'), 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'),
] ]
+56
View File
@@ -508,3 +508,59 @@ class SetAddressForCartView(APIView):
cart_order.address = address_object cart_order.address = address_object
cart_order.save() cart_order.save()
return Response({'detail': 'ادرس با موفقیت انتخاب شد'}) 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)
+10
View File
@@ -11,6 +11,7 @@ billiard==4.2.1
boto3==1.36.26 boto3==1.36.26
botocore==1.36.26 botocore==1.36.26
branca==0.8.1 branca==0.8.1
brotli==1.2.0
celery==5.4.0 celery==5.4.0
certifi==2024.8.30 certifi==2024.8.30
cffi==1.17.1 cffi==1.17.1
@@ -22,6 +23,7 @@ click-repl==0.3.0
colorama==0.4.6 colorama==0.4.6
cron-descriptor==1.4.5 cron-descriptor==1.4.5
cryptography==44.0.1 cryptography==44.0.1
cssselect2==0.8.0
defusedxml==0.8.0rc2 defusedxml==0.8.0rc2
diff-match-patch==20230430 diff-match-patch==20230430
distro==1.9.0 distro==1.9.0
@@ -51,6 +53,7 @@ et-xmlfile==1.1.0
factory_boy==3.3.1 factory_boy==3.3.1
Faker==28.4.1 Faker==28.4.1
folium==0.19.4 folium==0.19.4
fonttools==4.61.1
frozenlist==1.4.1 frozenlist==1.4.1
geoip2==4.8.0 geoip2==4.8.0
ghasedak_sms==1.0.3 ghasedak_sms==1.0.3
@@ -95,7 +98,9 @@ pycparser==2.22
pycryptodome==3.20.0 pycryptodome==3.20.0
pydantic==2.10.6 pydantic==2.10.6
pydantic_core==2.27.2 pydantic_core==2.27.2
pydyf==0.12.1
PyJWT==2.10.1 PyJWT==2.10.1
pyphen==0.17.2
pyTelegramBotAPI==4.23.0 pyTelegramBotAPI==4.23.0
python-crontab==3.2.0 python-crontab==3.2.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
@@ -125,6 +130,8 @@ sqlparse==0.5.1
tablib==3.5.0 tablib==3.5.0
telebot==0.0.5 telebot==0.0.5
text-unidecode==1.3 text-unidecode==1.3
tinycss2==1.5.1
tinyhtml5==2.0.0
tqdm==4.67.1 tqdm==4.67.1
typing_extensions==4.12.2 typing_extensions==4.12.2
tzdata==2024.1 tzdata==2024.1
@@ -132,9 +139,12 @@ uritemplate==4.1.1
urllib3==2.2.3 urllib3==2.2.3
vine==5.1.0 vine==5.1.0
wcwidth==0.2.13 wcwidth==0.2.13
weasyprint==67.0
webencodings==0.5.1
whitenoise==6.7.0 whitenoise==6.7.0
xlrd==2.0.1 xlrd==2.0.1
xlwt==1.3.0 xlwt==1.3.0
xyzservices==2025.1.0 xyzservices==2025.1.0
yarl==1.11.1 yarl==1.11.1
zeep==4.2.1 zeep==4.2.1
zopfli==0.4.0
+357
View File
@@ -0,0 +1,357 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>فاکتور سفارش - {{ order_number }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Vazir', 'Tahoma', Arial, sans-serif;
direction: rtl;
padding: 10px;
background-color: #fff;
}
.invoice-container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 15px;
border-bottom: 2px solid #333;
padding-bottom: 10px;
}
.header h1 {
color: #333;
font-size: 22px;
margin-bottom: 5px;
}
.header h2 {
color: #666;
font-size: 14px;
}
.order-info {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
background-color: #f9f9f9;
padding: 12px;
border-radius: 3px;
}
.info-section {
flex: 1;
}
.info-section h3 {
color: #333;
margin-bottom: 6px;
font-size: 13px;
border-bottom: 1px solid #ddd;
padding-bottom: 3px;
}
.info-row {
margin: 4px 0;
font-size: 11px;
}
.info-label {
font-weight: bold;
color: #555;
}
.info-value {
color: #333;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.items-table th {
background-color: #333;
color: white;
padding: 6px;
text-align: center;
font-size: 11px;
}
.items-table td {
padding: 6px;
text-align: center;
border-bottom: 1px solid #ddd;
font-size: 10px;
}
.items-table tr:hover {
background-color: #f5f5f5;
}
.summary {
margin-top: 15px;
float: left;
width: 300px;
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 6px;
border-bottom: 1px solid #ddd;
font-size: 11px;
}
.summary-row.total {
background-color: #333;
color: white;
font-weight: bold;
font-size: 14px;
margin-top: 6px;
}
.summary-label {
font-weight: bold;
}
.summary-value {
text-align: left;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 9px;
font-weight: bold;
}
.status-paid {
background-color: #4CAF50;
color: white;
}
.status-unpaid {
background-color: #f44336;
color: white;
}
.status-pending {
background-color: #ff9800;
color: white;
}
.footer {
margin-top: 30px;
text-align: center;
padding-top: 10px;
border-top: 1px solid #ddd;
color: #777;
font-size: 9px;
}
.clearfix::after {
content: "";
display: table;
clear: both;
}
.discount-highlight {
color: #d32f2f;
font-weight: bold;
}
</style>
</head>
<body>
<div class="invoice-container">
<div class="header">
<h1>فاکتور فروش</h1>
<h2>شماره سفارش: {{ order_number }}</h2>
</div>
<div class="order-info">
<div class="info-section">
<h3>اطلاعات مشتری</h3>
{% if user %}
<div class="info-row">
<span class="info-label">نام:</span>
<span class="info-value">{{ user.first_name }} {{ user.last_name }}</span>
</div>
<div class="info-row">
<span class="info-label">ایمیل:</span>
<span class="info-value">{{ user.email|default:"---" }}</span>
</div>
<div class="info-row">
<span class="info-label">تلفن:</span>
<span class="info-value">{{ user.phone }}</span>
</div>
{% endif %}
</div>
<div class="info-section">
<h3>اطلاعات سفارش</h3>
<div class="info-row">
<span class="info-label">تاریخ:</span>
<span class="info-value">{{ created_at_jalali|default:"---" }}</span>
</div>
<div class="info-row">
<span class="info-label">وضعیت پرداخت:</span>
<span class="info-value">
{% if is_paid %}
<span class="status-badge status-paid">پرداخت شده</span>
{% else %}
<span class="status-badge status-unpaid">پرداخت نشده</span>
{% endif %}
</span>
</div>
<div class="info-row">
<span class="info-label">وضعیت سفارش:</span>
<span class="info-value">
<span class="status-badge status-pending">{{ status }}</span>
</span>
</div>
</div>
</div>
{% if address %}
<div class="order-info">
<div class="info-section">
<h3>آدرس تحویل</h3>
<div class="info-row">
<span class="info-label">نام گیرنده:</span>
<span class="info-value">{{ address.name }}</span>
</div>
<div class="info-row">
<span class="info-label">آدرس:</span>
<span class="info-value">{{ address.address }}</span>
</div>
<div class="info-row">
<span class="info-label">شهر/استان:</span>
<span class="info-value">{{ address.city }}, {{ address.province }}</span>
</div>
<div class="info-row">
<span class="info-label">کد پستی:</span>
<span class="info-value">{{ address.postal_code }}</span>
</div>
<div class="info-row">
<span class="info-label">تلفن:</span>
<span class="info-value">{{ address.phone }}</span>
</div>
</div>
</div>
{% endif %}
<table class="items-table">
<thead>
<tr>
<th>ردیف</th>
<th>نام محصول</th>
<th>تنوع</th>
<th>تعداد</th>
<th>قیمت اصلی</th>
<th>تخفیف محصول</th>
<th>تخفیف ویژه</th>
<th>قیمت نهایی</th>
</tr>
</thead>
<tbody>
{% for item_data in items_with_discount %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ item_data.item.product.product.name }}</td>
<td>{{ item_data.item.product.title }}</td>
<td>{{ item_data.item.quantity }}</td>
<td>{{ item_data.price_before_discount|floatformat:0 }} تومان</td>
<td class="discount-highlight">
{% if item_data.item.discount_percent > 0 %}
{{ item_data.item.discount_percent }}% ({{ item_data.discount_amount|floatformat:0 }} تومان)
{% else %}
---
{% endif %}
</td>
<td class="discount-highlight">
{% if item_data.item.special_discount_amount > 0 %}
{{ item_data.item.special_discount_amount|floatformat:0 }} تومان
{% else %}
---
{% endif %}
</td>
<td>{{ item_data.item.price|floatformat:0 }} تومان</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="clearfix">
<div class="summary">
<div class="summary-row">
<span class="summary-label">تعداد کل اقلام:</span>
<span class="summary-value">{{ total_items }}</span>
</div>
<div class="summary-row">
<span class="summary-label">جمع کل:</span>
<span class="summary-value">{{ subtotal|floatformat:0 }} تومان</span>
</div>
{% if discount_amount > 0 %}
<div class="summary-row">
<span class="summary-label discount-highlight">تخفیف کد:</span>
<span class="summary-value discount-highlight">{{ discount_amount|floatformat:0 }} تومان</span>
</div>
{% endif %}
{% if special_discount_total > 0 %}
<div class="summary-row">
<span class="summary-label discount-highlight">تخفیف ویژه:</span>
<span class="summary-value discount-highlight">{{ special_discount_total|floatformat:0 }} تومان</span>
</div>
{% endif %}
<div class="summary-row">
<span class="summary-label">مالیات:</span>
<span class="summary-value">{{ tax|floatformat:0 }} تومان</span>
</div>
<div class="summary-row total">
<span class="summary-label">مبلغ قابل پرداخت:</span>
<span class="summary-value">{{ final_price|floatformat:0 }} تومان</span>
</div>
</div>
</div>
{% if discount_code %}
<div class="clearfix" style="margin-top: 100px;">
<div class="order-info">
<div class="info-section">
<h3>کد تخفیف استفاده شده</h3>
<div class="info-row">
<span class="info-label">کد:</span>
<span class="info-value">{{ discount_code.code }}</span>
</div>
<div class="info-row">
<span class="info-label">درصد:</span>
<span class="info-value">{{ discount_code.percent }}%</span>
</div>
</div>
</div>
</div>
{% endif %}
<div class="footer">
<p>با تشکر از خرید شما</p>
<p>این فاکتور به صورت الکترونیکی صادر شده و نیازی به امضا و مهر ندارد</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,434 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>فاکتور فروشگاه - {{ shop_order_id }}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Vazir', 'Tahoma', Arial, sans-serif;
direction: rtl;
padding: 10px;
background-color: #fff;
}
.invoice-container {
max-width: 850px;
margin: 0 auto;
background: white;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 15px;
border-bottom: 2px solid #333;
padding-bottom: 10px;
}
.header h1 {
color: #333;
font-size: 22px;
margin-bottom: 5px;
}
.header h2 {
color: #666;
font-size: 14px;
}
.shop-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px;
border-radius: 5px;
margin-bottom: 15px;
}
.shop-info h3 {
font-size: 15px;
margin-bottom: 5px;
}
.shop-info p {
font-size: 11px;
margin: 3px 0;
}
.order-info {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
background-color: #f9f9f9;
padding: 12px;
border-radius: 3px;
}
.info-section {
flex: 1;
margin: 0 5px;
}
.info-section h3 {
color: #333;
margin-bottom: 6px;
font-size: 13px;
border-bottom: 1px solid #ddd;
padding-bottom: 3px;
}
.info-row {
margin: 4px 0;
font-size: 11px;
}
.info-label {
font-weight: bold;
color: #555;
}
.info-value {
color: #333;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.items-table th {
background-color: #333;
color: white;
padding: 6px;
text-align: center;
font-size: 11px;
}
.items-table td {
padding: 6px;
text-align: center;
border-bottom: 1px solid #ddd;
font-size: 10px;
}
.items-table tr:hover {
background-color: #f5f5f5;
}
.financial-summary {
margin-top: 15px;
background-color: #f9f9f9;
padding: 12px;
border-radius: 5px;
}
.summary-section {
margin-bottom: 10px;
}
.summary-section h3 {
color: #333;
margin-bottom: 8px;
font-size: 14px;
border-bottom: 1px solid #667eea;
padding-bottom: 5px;
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 6px;
border-bottom: 1px solid #ddd;
font-size: 11px;
}
.summary-row.highlight {
background-color: #e3f2fd;
font-weight: bold;
}
.summary-row.total {
background-color: #333;
color: white;
font-weight: bold;
font-size: 14px;
margin-top: 6px;
}
.summary-row.payable {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: bold;
font-size: 15px;
margin-top: 6px;
}
.summary-label {
font-weight: bold;
}
.summary-value {
text-align: left;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 9px;
font-weight: bold;
}
.status-paid {
background-color: #4CAF50;
color: white;
}
.status-unpaid {
background-color: #f44336;
color: white;
}
.status-pending {
background-color: #ff9800;
color: white;
}
.status-settled {
background-color: #2196F3;
color: white;
}
.footer {
margin-top: 30px;
text-align: center;
padding-top: 10px;
border-top: 1px solid #ddd;
color: #777;
font-size: 9px;
}
.clearfix::after {
content: "";
display: table;
clear: both;
}
.discount-highlight {
color: #d32f2f;
font-weight: bold;
}
.commission-highlight {
color: #1976d2;
font-weight: bold;
}
.address-box {
background-color: #fff3cd;
border: 1px solid #ffc107;
padding: 10px;
border-radius: 3px;
margin: 12px 0;
}
.address-box h3 {
color: #856404;
margin-bottom: 6px;
font-size: 13px;
}
</style>
</head>
<body>
<div class="invoice-container">
<div class="header">
<h1>فاکتور فروشگاه</h1>
<h2>شماره سفارش اصلی: {{ order_number }} | شماره فاکتور فروشگاه: {{ shop_order_id }}</h2>
</div>
<div class="shop-info">
<h3>{{ shop.shop_name }}</h3>
<p>{{ shop.shop_description }}</p>
<p>کمیسیون: {{ commission_percent }}%</p>
</div>
<div class="order-info">
<div class="info-section">
<h3>اطلاعات مشتری</h3>
<div class="info-row">
<span class="info-label">نام:</span>
<span class="info-value">{{ customer_name|default:"---" }}</span>
</div>
<div class="info-row">
<span class="info-label">تلفن:</span>
<span class="info-value">{{ customer_phone|default:"---" }}</span>
</div>
</div>
<div class="info-section">
<h3>اطلاعات سفارش</h3>
<div class="info-row">
<span class="info-label">تاریخ:</span>
<span class="info-value">{{ created_at_jalali|default:"---" }}</span>
</div>
<div class="info-row">
<span class="info-label">وضعیت پرداخت:</span>
<span class="info-value">
{% if is_paid %}
<span class="status-badge status-paid">پرداخت شده</span>
{% else %}
<span class="status-badge status-unpaid">پرداخت نشده</span>
{% endif %}
</span>
</div>
<div class="info-row">
<span class="info-label">وضعیت سفارش:</span>
<span class="info-value">
<span class="status-badge status-pending">{{ status }}</span>
</span>
</div>
<div class="info-row">
<span class="info-label">وضعیت تسویه:</span>
<span class="info-value">
{% if is_settled %}
<span class="status-badge status-settled">تسویه شده</span>
{% else %}
<span class="status-badge status-unpaid">تسویه نشده</span>
{% endif %}
</span>
</div>
</div>
</div>
{% if address_text %}
<div class="address-box">
<h3>آدرس تحویل</h3>
<div class="info-row">
<span class="info-label">گیرنده:</span>
<span class="info-value">{{ address_recipient_name }}</span>
</div>
<div class="info-row">
<span class="info-label">آدرس:</span>
<span class="info-value">{{ address_text }}</span>
</div>
<div class="info-row">
<span class="info-label">شهر/استان:</span>
<span class="info-value">{{ address_city }}, {{ address_province }}</span>
</div>
<div class="info-row">
<span class="info-label">کد پستی:</span>
<span class="info-value">{{ address_postal_code }}</span>
</div>
<div class="info-row">
<span class="info-label">تلفن:</span>
<span class="info-value">{{ address_phone }}</span>
</div>
</div>
{% endif %}
<table class="items-table">
<thead>
<tr>
<th>ردیف</th>
<th>نام محصول</th>
<th>تنوع</th>
<th>تعداد</th>
<th>قیمت واحد</th>
<th>تخفیف محصول</th>
<th>تخفیف ویژه</th>
<th>قیمت نهایی</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ item.order_item.product.product.name }}</td>
<td>{{ item.order_item.product.title }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.unit_price|floatformat:0 }} تومان</td>
<td class="discount-highlight">
{% if item.discount_amount > 0 %}
{{ item.order_item.discount_percent }}% ({{ item.discount_amount|floatformat:0 }} تومان)
{% else %}
---
{% endif %}
</td>
<td class="discount-highlight">
{% if item.special_discount_amount > 0 %}
{{ item.special_discount_amount|floatformat:0 }} تومان
{% else %}
---
{% endif %}
</td>
<td>{{ item.total_price|floatformat:0 }} تومان</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="financial-summary">
<div class="summary-section">
<h3>خلاصه مالی</h3>
<div class="summary-row">
<span class="summary-label">تعداد کل اقلام:</span>
<span class="summary-value">{{ total_items }}</span>
</div>
<div class="summary-row">
<span class="summary-label">جمع کل (Subtotal):</span>
<span class="summary-value">{{ subtotal|floatformat:0 }} تومان</span>
</div>
{% if discount_amount > 0 %}
<div class="summary-row">
<span class="summary-label discount-highlight">تخفیف معمولی:</span>
<span class="summary-value discount-highlight">{{ discount_amount|floatformat:0 }} تومان</span>
</div>
{% endif %}
{% if special_discount_amount > 0 %}
<div class="summary-row">
<span class="summary-label discount-highlight">تخفیف ویژه:</span>
<span class="summary-value discount-highlight">{{ special_discount_amount|floatformat:0 }} تومان</span>
</div>
{% endif %}
<div class="summary-row">
<span class="summary-label">مالیات:</span>
<span class="summary-value">{{ tax_amount|floatformat:0 }} تومان</span>
</div>
</div>
<div class="summary-section">
<h3>محاسبات کمیسیون</h3>
<div class="summary-row highlight">
<span class="summary-label commission-highlight">درصد کمیسیون:</span>
<span class="summary-value commission-highlight">{{ commission_percent }}%</span>
</div>
<div class="summary-row highlight">
<span class="summary-label commission-highlight">مبلغ کمیسیون:</span>
<span class="summary-value commission-highlight">{{ commission_amount|floatformat:0 }} تومان</span>
</div>
</div>
<div class="summary-section">
<h3>مبلغ نهایی</h3>
<div class="summary-row payable">
<span class="summary-label">مبلغ قابل پرداخت به فروشگاه:</span>
<span class="summary-value">{{ payable_amount|floatformat:0 }} تومان</span>
</div>
</div>
</div>
<div class="footer">
<p>با تشکر از همکاری شما</p>
<p>این فاکتور به صورت الکترونیکی صادر شده و نیازی به امضا و مهر ندارد</p>
<p>در صورت هرگونه سوال یا مشکل با پشتیبانی تماس بگیرید</p>
</div>
</div>
</body>
</html>