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:
@@ -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='شناسه چت تلگرام'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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
@@ -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
@@ -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,9 +147,24 @@ 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 = 'دانلود فاکتور'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 .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
|
||||||
|
)
|
||||||
+60
-1
@@ -119,4 +119,63 @@ 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
|
||||||
|
|||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user