634 lines
27 KiB
Python
634 lines
27 KiB
Python
"""
|
|
PDF/A-3 Generation Service for ZATCA E-Invoice
|
|
|
|
This module handles:
|
|
- PDF/A-3 generation with embedded XML
|
|
- QR code image generation and embedding
|
|
- Bilingual invoice layout (Arabic + English)
|
|
- ZATCA-compliant invoice formatting
|
|
- Dynamic color theming from tenant settings
|
|
"""
|
|
|
|
import base64
|
|
import io
|
|
import logging
|
|
from typing import Optional
|
|
|
|
import qrcode
|
|
import arabic_reshaper
|
|
from bidi.algorithm import get_display
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.units import mm
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
|
|
from reportlab.pdfbase import pdfmetrics
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|
from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT
|
|
from django.conf import settings
|
|
|
|
from core.settings_service import get_tenant_settings_service
|
|
from finance.models import Invoice
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Register Arabic font
|
|
try:
|
|
pdfmetrics.registerFont(TTFont('Arabic', '/System/Library/Fonts/SFArabic.ttf'))
|
|
ARABIC_FONT_AVAILABLE = True
|
|
logger.info("Arabic font registered successfully")
|
|
except Exception as e:
|
|
ARABIC_FONT_AVAILABLE = False
|
|
logger.warning(f"Could not register Arabic font: {e}")
|
|
|
|
|
|
def reshape_arabic(text: str) -> str:
|
|
"""
|
|
Reshape Arabic text for proper display in PDF.
|
|
|
|
Args:
|
|
text: Text containing Arabic characters
|
|
|
|
Returns:
|
|
str: Reshaped text ready for PDF rendering
|
|
"""
|
|
try:
|
|
reshaped_text = arabic_reshaper.reshape(text)
|
|
bidi_text = get_display(reshaped_text)
|
|
return bidi_text
|
|
except Exception as e:
|
|
logger.warning(f"Error reshaping Arabic text: {e}")
|
|
return text
|
|
|
|
|
|
class PDFService:
|
|
"""Service for generating PDF/A-3 invoices."""
|
|
|
|
@staticmethod
|
|
def generate_qr_code_image(qr_data: str) -> io.BytesIO:
|
|
"""
|
|
Generate QR code image from data.
|
|
|
|
Args:
|
|
qr_data: QR code data (base64 TLV encoded)
|
|
|
|
Returns:
|
|
io.BytesIO: QR code image buffer
|
|
"""
|
|
try:
|
|
# Create QR code
|
|
qr = qrcode.QRCode(
|
|
version=1,
|
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
box_size=10,
|
|
border=4,
|
|
)
|
|
qr.add_data(qr_data)
|
|
qr.make(fit=True)
|
|
|
|
# Create image
|
|
img = qr.make_image(fill_color="black", back_color="white")
|
|
|
|
# Save to buffer
|
|
buffer = io.BytesIO()
|
|
img.save(buffer, format='PNG')
|
|
buffer.seek(0)
|
|
|
|
return buffer
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating QR code image: {e}")
|
|
raise
|
|
|
|
@staticmethod
|
|
def _get_tenant_colors(invoice):
|
|
"""
|
|
Get primary and secondary colors from tenant settings.
|
|
|
|
Args:
|
|
invoice: Invoice instance
|
|
|
|
Returns:
|
|
tuple: (primary_color, secondary_color) as HexColor objects
|
|
"""
|
|
try:
|
|
settings_service = get_tenant_settings_service(invoice.tenant)
|
|
primary_color_hex = settings_service.get_setting('basic_primary_color', '#8B5CF6')
|
|
secondary_color_hex = settings_service.get_setting('basic_secondary_color', '#6366F1')
|
|
|
|
# Ensure hex format
|
|
if not primary_color_hex.startswith('#'):
|
|
primary_color_hex = '#8B5CF6'
|
|
if not secondary_color_hex.startswith('#'):
|
|
secondary_color_hex = '#6366F1'
|
|
|
|
return (
|
|
colors.HexColor(primary_color_hex),
|
|
colors.HexColor(secondary_color_hex)
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Error getting tenant colors: {e}, using defaults")
|
|
return (colors.HexColor('#8B5CF6'), colors.HexColor('#6366F1'))
|
|
|
|
@staticmethod
|
|
def generate_invoice_pdf(invoice, include_xml: bool = True) -> bytes:
|
|
"""
|
|
Generate professional ZATCA-compliant invoice PDF with tenant branding.
|
|
|
|
Args:
|
|
invoice: Invoice instance
|
|
include_xml: Whether to embed XML in PDF
|
|
|
|
Returns:
|
|
bytes: PDF content
|
|
"""
|
|
try:
|
|
# Get tenant colors
|
|
primary_color, secondary_color = PDFService._get_tenant_colors(invoice)
|
|
|
|
# Create PDF buffer
|
|
buffer = io.BytesIO()
|
|
|
|
# Create PDF document
|
|
doc = SimpleDocTemplate(
|
|
buffer,
|
|
pagesize=A4,
|
|
rightMargin=15*mm,
|
|
leftMargin=15*mm,
|
|
topMargin=10*mm,
|
|
bottomMargin=10*mm
|
|
)
|
|
|
|
# Container for PDF elements
|
|
elements = []
|
|
|
|
# Styles
|
|
styles = getSampleStyleSheet()
|
|
|
|
# ===== HEADER SECTION =====
|
|
ar_vat_invoice = reshape_arabic("فاتورة ضريبية")
|
|
ar_invoice_no = reshape_arabic("رقم الفاتورة")
|
|
ar_invoice_date = reshape_arabic("تاريخ الفاتورة")
|
|
ar_due = reshape_arabic("تاريخ الاستحقاق")
|
|
ar_bill_to = reshape_arabic("الفاتورة إلى")
|
|
|
|
# Get logo from tenant settings
|
|
settings_service = get_tenant_settings_service(invoice.tenant)
|
|
logo_file = settings_service.get_setting('basic_logo')
|
|
|
|
# Create logo and company info section (left side)
|
|
if logo_file and hasattr(logo_file, 'path'):
|
|
try:
|
|
logo_img = Image(logo_file.path, width=25*mm, height=25*mm, kind='proportional')
|
|
except Exception as e:
|
|
logger.warning(f"Could not load logo image: {e}, using placeholder")
|
|
logo_img = None
|
|
else:
|
|
logo_img = None
|
|
|
|
# Company info below logo
|
|
company_parts = [f'<b><font name = "Arabic" size=11>{reshape_arabic(invoice.tenant.name_ar)}</font></b><br/>']
|
|
if invoice.tenant.vat_number:
|
|
company_parts.append(f'<font size=9>VAT: {invoice.tenant.vat_number}</font><br/>')
|
|
if invoice.tenant.address:
|
|
company_parts.append(f'<font size=9>{invoice.tenant.address}</font><br/>')
|
|
if invoice.tenant.city:
|
|
company_parts.append(f'<font size=9>{invoice.tenant.city}</font>')
|
|
|
|
company_html = ''.join(company_parts)
|
|
company_para = Paragraph(company_html, ParagraphStyle('Company', parent=styles['Normal']))
|
|
|
|
# QR Code (below company info on left)
|
|
qr_img = None
|
|
if invoice.qr_code:
|
|
try:
|
|
qr_buffer = PDFService.generate_qr_code_image(invoice.qr_code)
|
|
qr_img = Image(qr_buffer, width=35 * mm, height=35 * mm)
|
|
except Exception as e:
|
|
logger.warning(f"Could not add QR code: {e}")
|
|
|
|
# Left column content
|
|
left_column_data = []
|
|
if logo_img:
|
|
left_column_data.append([logo_img])
|
|
left_column_data.append([company_para])
|
|
if qr_img:
|
|
left_column_data.append([qr_img])
|
|
|
|
left_column = Table(left_column_data, colWidths=[80*mm])
|
|
left_column.setStyle(TableStyle([
|
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
|
('TOPPADDING', (0, 0), (-1, -1), 5),
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 5),
|
|
]))
|
|
|
|
# Right column: Title and invoice details
|
|
title_html = (
|
|
f'<para align="right">'
|
|
f'<font size=24 color="#FFA500"><b>VAT Invoice</b></font><br/><br/>'
|
|
f'<font name="Arabic" size=16 color="#FFA500"><b>{ar_vat_invoice}</b></font><br/><br/><br/><br/>'
|
|
f'</para>'
|
|
)
|
|
title_para = Paragraph(title_html, ParagraphStyle('Title', parent=styles['Normal']))
|
|
|
|
# Invoice details
|
|
invoice_details_html = (
|
|
f'<para align="right">'
|
|
f'<font size=9>Invoice no.</font> - <font name="Arabic" size=9>{ar_invoice_no}</font><br/>'
|
|
f'<b><font size=10>{invoice.invoice_number}</font></b><br/><br/>'
|
|
f'<font size=9>Invoice date</font> - <font name="Arabic" size=9>{ar_invoice_date}</font><br/>'
|
|
f'<b><font size=10>{invoice.issue_date.strftime("%d.%m.%Y")}</font></b><br/><br/>'
|
|
f'<font size=9>Due</font> - <font name="Arabic" size=9>{ar_due}</font><br/>'
|
|
f'<b><font size=10>{invoice.due_date.strftime("%d.%m.%Y")}</font></b>'
|
|
f'</para>'
|
|
)
|
|
invoice_details_para = Paragraph(invoice_details_html, ParagraphStyle('InvDetails', parent=styles['Normal']))
|
|
|
|
# Bill to section
|
|
bill_to_html = (
|
|
f'<para align="right">'
|
|
f'<font size=9>Bill to</font> - <font name="Arabic" size=9>{ar_bill_to}</font><br/>'
|
|
f'<font name="Arabic" size=10><b>{reshape_arabic(invoice.patient.full_name_ar)}</b></font><br/>'
|
|
)
|
|
if invoice.patient.email:
|
|
bill_to_html += f'<font size=9>{invoice.patient.email}</font><br/>'
|
|
if invoice.patient.phone:
|
|
bill_to_html += f'<font size=9>{invoice.patient.phone}</font>'
|
|
bill_to_html += '</para>'
|
|
bill_to_para = Paragraph(bill_to_html, ParagraphStyle('BillTo', parent=styles['Normal']))
|
|
|
|
# Right column content
|
|
right_column_data = [
|
|
[title_para],
|
|
[invoice_details_para],
|
|
[bill_to_para]
|
|
]
|
|
right_column = Table(right_column_data, colWidths=[110*mm])
|
|
right_column.setStyle(TableStyle([
|
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
('ALIGN', (0, 0), (-1, -1), 'RIGHT'),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
|
('TOPPADDING', (0, 0), (-1, -1), 5),
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
|
|
]))
|
|
|
|
# Combine left and right columns
|
|
header_table = Table([[left_column, right_column]], colWidths=[60*mm, 110*mm])
|
|
header_table.setStyle(TableStyle([
|
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
|
]))
|
|
elements.append(header_table)
|
|
elements.append(Spacer(1, 10*mm))
|
|
|
|
# ===== LINE ITEMS TABLE =====
|
|
ar_description = reshape_arabic("الوصف")
|
|
ar_rate = reshape_arabic("السعر")
|
|
ar_qty = reshape_arabic("الكمية")
|
|
ar_tax = reshape_arabic("الضريبة")
|
|
ar_disc = reshape_arabic("الخصم")
|
|
ar_amount = reshape_arabic("المبلغ")
|
|
|
|
# Saudi Riyal symbol
|
|
sar_symbol = "ر.س"
|
|
|
|
line_items_data = [
|
|
[
|
|
Paragraph(f'<b>Desc.</b><br/><font name="Arabic" size=7>{ar_description}</font>',
|
|
ParagraphStyle('Hdr', parent=styles['Normal'], fontSize=9, alignment=TA_LEFT, textColor=colors.white)),
|
|
Paragraph(f'<b>Price</b><br/><font name="Arabic" size=7>{ar_rate}</font>',
|
|
ParagraphStyle('Hdr', parent=styles['Normal'], fontSize=9, alignment=TA_RIGHT, textColor=colors.white)),
|
|
Paragraph(f'<b>QTY</b><br/><font name="Arabic" size=7>{ar_qty}</font>',
|
|
ParagraphStyle('Hdr', parent=styles['Normal'], fontSize=9, alignment=TA_CENTER, textColor=colors.white)),
|
|
Paragraph(f'<b>VAT</b><br/><font name="Arabic" size=7>{ar_tax}</font>',
|
|
ParagraphStyle('Hdr', parent=styles['Normal'], fontSize=9, alignment=TA_CENTER, textColor=colors.white)),
|
|
Paragraph(f'<b>Discount</b><br/><font name="Arabic" size=7>{ar_disc}</font>',
|
|
ParagraphStyle('Hdr', parent=styles['Normal'], fontSize=9, alignment=TA_CENTER, textColor=colors.white)),
|
|
Paragraph(f'<b>Total</b><br/><font name="Arabic" size=7>{ar_amount}</font>',
|
|
ParagraphStyle('Hdr', parent=styles['Normal'], fontSize=9, alignment=TA_RIGHT, textColor=colors.white))
|
|
]
|
|
]
|
|
|
|
for item in invoice.line_items.all():
|
|
# Calculate VAT percentage
|
|
vat_percentage = "15" if invoice.tax > 0 else "0"
|
|
|
|
line_items_data.append([
|
|
Paragraph(item.description or 'Service', ParagraphStyle('Cell', parent=styles['Normal'], fontSize=9)),
|
|
Paragraph(f'{item.unit_price:.2f}', ParagraphStyle('Cell', parent=styles['Normal'], fontSize=9, alignment=TA_RIGHT)),
|
|
Paragraph(str(item.quantity), ParagraphStyle('Cell', parent=styles['Normal'], fontSize=9, alignment=TA_CENTER)),
|
|
Paragraph(vat_percentage, ParagraphStyle('Cell', parent=styles['Normal'], fontSize=9, alignment=TA_CENTER)),
|
|
Paragraph('0', ParagraphStyle('Cell', parent=styles['Normal'], fontSize=9, alignment=TA_CENTER)),
|
|
Paragraph(f'{item.total:.2f}', ParagraphStyle('Cell', parent=styles['Normal'], fontSize=9, alignment=TA_RIGHT))
|
|
])
|
|
|
|
items_table = Table(line_items_data, colWidths=[60*mm, 25*mm, 15*mm, 18*mm, 18*mm, 34*mm])
|
|
items_table.setStyle(TableStyle([
|
|
# Header with orange background
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#FFA500')),
|
|
('TEXTCOLOR', (0, 0), (-1, -1), colors.white),
|
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
('FONTSIZE', (0, 0), (-1, 0), 9),
|
|
('TOPPADDING', (0, 0), (-1, 0), 8),
|
|
('BOTTOMPADDING', (0, 0), (-1, 0), 8),
|
|
|
|
# Data rows
|
|
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
|
('FONTSIZE', (0, 1), (-1, -1), 9),
|
|
('TEXTCOLOR', (0, 1), (-1, -1), colors.black),
|
|
('TOPPADDING', (0, 1), (-1, -1), 6),
|
|
('BOTTOMPADDING', (0, 1), (-1, -1), 6),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 5),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 5),
|
|
|
|
# Alternating row colors
|
|
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.HexColor('#FFF8E1'), colors.white]),
|
|
|
|
# Borders
|
|
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#FFA500')),
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
|
]))
|
|
|
|
elements.append(items_table)
|
|
elements.append(Spacer(1, 10*mm))
|
|
|
|
# ===== PAYMENT INSTRUCTION AND TOTALS SECTION =====
|
|
# ar_payment_inst = reshape_arabic("تعليمات الدفع")
|
|
ar_subtotal = reshape_arabic("المجموع الفرعي")
|
|
ar_discount = reshape_arabic("الخصم")
|
|
# ar_shipping = reshape_arabic("تكلفة الشحن")
|
|
ar_sales_tax = reshape_arabic("ضريبة القيمة المضافة")
|
|
ar_total = reshape_arabic("الإجمالي")
|
|
ar_amount_paid = reshape_arabic("المبلغ المدفوع")
|
|
ar_balance_due = reshape_arabic("الرصيد المستحق")
|
|
|
|
# Payment instruction (left) - show actual payment details
|
|
payment_inst_parts = [
|
|
# f'<b>Payment instruction</b><br/>'
|
|
# f'<font name="Arabic" size=8>{ar_payment_inst}</font><br/><br/>'
|
|
]
|
|
|
|
# Get payment information
|
|
payments = invoice.payments.filter(status='COMPLETED').order_by('-payment_date')
|
|
if payments.exists():
|
|
for payment in payments:
|
|
payment_method = payment.get_method_display()
|
|
payment_inst_parts.append(
|
|
f'<font size=8><b>{payment_method}</b><br/>'
|
|
f'Amount: {payment.amount:.2f}<br/>'
|
|
f'Date: {payment.payment_date.strftime("%d.%m.%Y")}<br/>'
|
|
)
|
|
if payment.reference:
|
|
payment_inst_parts.append(f'Ref: {payment.reference}<br/>')
|
|
payment_inst_parts.append('<br/></font>')
|
|
else:
|
|
# No payments yet
|
|
payment_inst_parts.append(
|
|
f'<font size=8>Bank Transfer Details:<br/>'
|
|
f'Account: {invoice.tenant.name}<br/>'
|
|
f'VAT: {invoice.tenant.vat_number or "N/A"}</font>'
|
|
)
|
|
|
|
payment_inst_html = ''.join(payment_inst_parts)
|
|
|
|
# Totals (right) - using Saudi Riyal symbol
|
|
totals_html_parts = [f'<para align="right"><font size=9>',
|
|
f'<b><font size=7>Subtotal</font></b> - <b><font name="Arabic" size=7>{ar_subtotal}</font></b><br/>',
|
|
f'<b>{invoice.subtotal:.2f}</b><br/><br/>']
|
|
|
|
if invoice.discount > 0:
|
|
|
|
totals_html_parts.append(f'<b><font size=7>Discount</font></b> - <font name="Arabic" size=7>{ar_discount}</font><br/>')
|
|
totals_html_parts.append(f'<b>{invoice.discount:.2f}</b><br/><br/>')
|
|
|
|
# totals_html_parts.append(f'<b>Shipping Cost, <font name="Arabic">{sar_symbol}</font>:</b> 0.00<br/>')
|
|
# totals_html_parts.append(f'<font name="Arabic" size=7>{ar_shipping}</font><br/>')
|
|
totals_html_parts.append(f'<b><font size=7>VAT</font></b> - <font name="Arabic" size=7>{ar_sales_tax}</font><br/>')
|
|
totals_html_parts.append(f'<b>{invoice.tax:.2f}</b><br/><br/>')
|
|
totals_html_parts.append(f'<b><font size=7>Total</font></b> - <font name="Arabic" size=7>{ar_total}</font><br/>')
|
|
totals_html_parts.append(f'<b>{invoice.total:.2f}</b><br/><br/>')
|
|
totals_html_parts.append(f'<b><font size=7 >Amount paid</font></b> - <font name="Arabic" size=7>{ar_amount_paid}</font><br/>')
|
|
totals_html_parts.append(f'<b><font color="#1F6427">{invoice.amount_paid:.2f}</font></b><br/><br/>')
|
|
totals_html_parts.append(f'<b><font size=7 >Balance Due</font></b> - <font name="Arabic" size=7>{ar_balance_due}</font><br/>')
|
|
totals_html_parts.append(f'<b><font color="#D62C20">{invoice.amount_due:.2f}</font></b>')
|
|
totals_html_parts.append('</font></para>')
|
|
|
|
totals_html = ''.join(totals_html_parts)
|
|
|
|
payment_totals_section = Table([
|
|
[Paragraph(payment_inst_html, ParagraphStyle('PayInst', parent=styles['Normal'])),
|
|
Paragraph(totals_html, ParagraphStyle('Totals', parent=styles['Normal']))]
|
|
], colWidths=[70*mm, 100*mm])
|
|
payment_totals_section.setStyle(TableStyle([
|
|
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 0),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 0),
|
|
]))
|
|
elements.append(payment_totals_section)
|
|
|
|
# Build PDF
|
|
doc.build(elements)
|
|
|
|
# Get PDF content
|
|
pdf_content = buffer.getvalue()
|
|
buffer.close()
|
|
|
|
# If include_xml, embed XML (requires additional library)
|
|
if include_xml and invoice.xml_content:
|
|
pdf_content = PDFService._embed_xml_in_pdf(pdf_content, invoice.xml_content)
|
|
|
|
logger.info(f"PDF generated for invoice {invoice.invoice_number}")
|
|
|
|
return pdf_content
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating PDF: {e}")
|
|
raise
|
|
|
|
@staticmethod
|
|
def _embed_xml_in_pdf(pdf_content: bytes, xml_content: str) -> bytes:
|
|
"""
|
|
Embed XML content in PDF as attachment (PDF/A-3 requirement).
|
|
|
|
Args:
|
|
pdf_content: Original PDF content
|
|
xml_content: XML content to embed
|
|
|
|
Returns:
|
|
bytes: PDF with embedded XML
|
|
"""
|
|
try:
|
|
# This requires PyPDF2 or similar library
|
|
# For now, return original PDF
|
|
# TODO: Implement XML embedding using PyPDF2
|
|
|
|
logger.warning("XML embedding not yet implemented - returning PDF without embedded XML")
|
|
return pdf_content
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error embedding XML in PDF: {e}")
|
|
return pdf_content
|
|
|
|
@staticmethod
|
|
def generate_invoice_pdf_arabic(invoice) -> bytes:
|
|
"""
|
|
Generate Arabic-first invoice PDF.
|
|
|
|
Args:
|
|
invoice: Invoice instance
|
|
|
|
Returns:
|
|
bytes: PDF content
|
|
"""
|
|
try:
|
|
# Create PDF buffer
|
|
buffer = io.BytesIO()
|
|
|
|
# Create canvas for RTL support
|
|
c = canvas.Canvas(buffer, pagesize=A4)
|
|
width, height = A4
|
|
|
|
# Use Arabic font if available
|
|
if ARABIC_FONT_AVAILABLE:
|
|
c.setFont('Arabic', 12)
|
|
else:
|
|
c.setFont('Helvetica', 12)
|
|
|
|
# Header
|
|
c.setFont('Helvetica-Bold', 20)
|
|
ar_title = reshape_arabic("فاتورة ضريبية")
|
|
c.drawCentredString(width/2, height - 50, ar_title)
|
|
c.setFont('Helvetica', 16)
|
|
c.drawCentredString(width/2, height - 75, "TAX INVOICE")
|
|
|
|
# Invoice details (RTL for Arabic)
|
|
y = height - 120
|
|
if ARABIC_FONT_AVAILABLE:
|
|
c.setFont('Arabic', 10)
|
|
else:
|
|
c.setFont('Helvetica', 10)
|
|
|
|
# Right-aligned for Arabic
|
|
ar_inv_num = reshape_arabic(f"رقم الفاتورة: {invoice.invoice_number}")
|
|
c.drawRightString(width - 50, y, ar_inv_num)
|
|
y -= 20
|
|
|
|
ar_date = reshape_arabic(f"التاريخ: {invoice.issue_date}")
|
|
c.drawRightString(width - 50, y, ar_date)
|
|
y -= 20
|
|
|
|
ar_type = reshape_arabic(f"نوع الفاتورة: {invoice.get_invoice_type_display()}")
|
|
c.drawRightString(width - 50, y, ar_type)
|
|
|
|
# Seller information
|
|
y -= 40
|
|
c.setFont('Helvetica-Bold', 12)
|
|
ar_seller = reshape_arabic("معلومات البائع")
|
|
c.drawRightString(width - 50, y, ar_seller)
|
|
y -= 20
|
|
|
|
if ARABIC_FONT_AVAILABLE:
|
|
c.setFont('Arabic', 10)
|
|
else:
|
|
c.setFont('Helvetica', 10)
|
|
|
|
ar_name = reshape_arabic(f"الاسم: {invoice.tenant.name_ar or invoice.tenant.name}")
|
|
c.drawRightString(width - 50, y, ar_name)
|
|
y -= 20
|
|
|
|
ar_vat = reshape_arabic(f"الرقم الضريبي: {invoice.tenant.vat_number or 'N/A'}")
|
|
c.drawRightString(width - 50, y, ar_vat)
|
|
|
|
# Customer information
|
|
y -= 40
|
|
c.setFont('Helvetica-Bold', 12)
|
|
ar_customer = reshape_arabic("معلومات العميل")
|
|
c.drawRightString(width - 50, y, ar_customer)
|
|
y -= 20
|
|
|
|
if ARABIC_FONT_AVAILABLE:
|
|
c.setFont('Arabic', 10)
|
|
else:
|
|
c.setFont('Helvetica', 10)
|
|
|
|
ar_cust_name = reshape_arabic(f"الاسم: {invoice.patient.full_name_ar or invoice.patient.full_name_en}")
|
|
c.drawRightString(width - 50, y, ar_cust_name)
|
|
y -= 20
|
|
|
|
ar_mrn = reshape_arabic(f"رقم السجل الطبي: {invoice.patient.mrn}")
|
|
c.drawRightString(width - 50, y, ar_mrn)
|
|
|
|
# Line items
|
|
y -= 40
|
|
c.setFont('Helvetica-Bold', 12)
|
|
ar_items = reshape_arabic("تفاصيل الفاتورة")
|
|
c.drawRightString(width - 50, y, ar_items)
|
|
|
|
# Draw table
|
|
y -= 30
|
|
table_y = y
|
|
|
|
for idx, item in enumerate(invoice.line_items.all()):
|
|
if ARABIC_FONT_AVAILABLE:
|
|
c.setFont('Arabic', 10)
|
|
else:
|
|
c.setFont('Helvetica', 10)
|
|
|
|
c.drawRightString(width - 50, table_y, f"{item.description or 'Service'}")
|
|
c.drawRightString(width - 250, table_y, f"{item.quantity}")
|
|
c.drawRightString(width - 350, table_y, f"{item.unit_price:.2f}")
|
|
c.drawRightString(width - 450, table_y, f"{item.total:.2f} SAR")
|
|
table_y -= 20
|
|
|
|
# Totals
|
|
y = table_y - 30
|
|
c.setFont('Helvetica-Bold', 11)
|
|
ar_subtotal_text = reshape_arabic(f"المجموع الفرعي: {invoice.subtotal:.2f} SAR")
|
|
c.drawRightString(width - 50, y, ar_subtotal_text)
|
|
y -= 20
|
|
|
|
ar_vat_text = reshape_arabic(f"ضريبة القيمة المضافة (15%): {invoice.tax:.2f} SAR")
|
|
c.drawRightString(width - 50, y, ar_vat_text)
|
|
y -= 20
|
|
|
|
c.setFont('Helvetica-Bold', 14)
|
|
ar_total_text = reshape_arabic(f"المجموع الكلي: {invoice.total:.2f} SAR")
|
|
c.drawRightString(width - 50, y, ar_total_text)
|
|
|
|
# QR Code
|
|
if invoice.qr_code:
|
|
try:
|
|
qr_buffer = PDFService.generate_qr_code_image(invoice.qr_code)
|
|
c.drawImage(qr_buffer, 50, 50, width=100, height=100, preserveAspectRatio=True)
|
|
c.setFont('Helvetica', 8)
|
|
ar_scan_text = reshape_arabic("امسح للتحقق")
|
|
c.drawString(50, 35, f"Scan to verify / {ar_scan_text}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not add QR code: {e}")
|
|
|
|
# Footer
|
|
c.setFont('Helvetica', 8)
|
|
ar_footer_text = reshape_arabic("هذه فاتورة إلكترونية")
|
|
c.drawCentredString(width/2, 30, f"This is a computer-generated invoice / {ar_footer_text}")
|
|
|
|
# Save PDF
|
|
c.save()
|
|
|
|
# Get PDF content
|
|
pdf_content = buffer.getvalue()
|
|
buffer.close()
|
|
|
|
logger.info(f"Arabic PDF generated for invoice {invoice.invoice_number}")
|
|
|
|
return pdf_content
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating Arabic PDF: {e}")
|
|
raise
|