f5_model_final / app /services /invoice_service.py
EL GHAFRAOUI AYOUB
C
4e55f01
raw
history blame
15.4 kB
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib import colors
from reportlab.lib.units import cm
from io import BytesIO
from app.db.models import Invoice
import logging
import os
import datetime
# Set up logging
logger = logging.getLogger(__name__)
class InvoiceService:
@staticmethod
def generate_pdf(data: Invoice) -> bytes:
try:
buffer = BytesIO()
pdf = canvas.Canvas(buffer, pagesize=A4)
page_width, page_height = A4
# Constants
HEADER_BLUE = (0.29, 0.45, 0.68)
BLUE_LIGHT = (1.5, 1.5, 1)
WHITE = (1, 1, 1)
BLACK = (0, 0, 0)
MARGIN = 30
LINE_HEIGHT = 20
BOX_PADDING = 10
STANDARD_FONT_SIZE = 10
BOTTOM_MARGIN = 50 # Minimum margin at the bottom of the page
# Helper function to draw centered text
def draw_centered_text(pdf, text, x, y, width, font="Helvetica", size=STANDARD_FONT_SIZE):
text_width = pdf.stringWidth(text, font, size)
pdf.drawString(x + (width - text_width) / 2, y, text)
# Helper function to draw a bordered box
def draw_box(pdf, x, y, width, height, fill_color=None, stroke_color=BLACK):
if fill_color:
pdf.setFillColorRGB(*fill_color)
pdf.rect(x, y, width, height, fill=1, stroke=0)
pdf.setFillColorRGB(*stroke_color)
pdf.rect(x, y, width, height, stroke=1)
# Add this helper function after the existing helper functions
def draw_wrapped_text(pdf, text, x, y, width, font="Helvetica", size=STANDARD_FONT_SIZE):
"""Draw text wrapped to fit within a given width."""
pdf.setFont(font, size)
words = text.split()
lines = []
current_line = []
for word in words:
current_line.append(word)
line_width = pdf.stringWidth(' '.join(current_line), font, size)
if line_width > width:
current_line.pop() # Remove last word
if current_line: # Only add if there are words
lines.append(' '.join(current_line))
current_line = [word] # Start new line with the word that didn't fit
if current_line: # Add the last line
lines.append(' '.join(current_line))
return lines
# Get the absolute path to the logo file
current_dir = os.path.dirname(os.path.abspath(__file__))
logo_path = os.path.join(current_dir, "..", "static", "logo.png")
# Top section layout
top_margin = page_height - 120
# Left side: Logo - moved far left and up
if os.path.exists(logo_path):
pdf.drawImage(logo_path, MARGIN, top_margin - 10, width=100, height=100)
# Right side: DEVIS and Client Box - moved far right and up
pdf.setFont("Helvetica-Bold", 36)
devis_text = "DEVIS"
devis_width = pdf.stringWidth(devis_text, "Helvetica-Bold", 36)
devis_x = page_width - devis_width - MARGIN - 10
devis_y = top_margin + 50
pdf.drawString(devis_x, devis_y, devis_text)
# Client info box - moved right under DEVIS
box_width = 200
box_height = 80
box_x = page_width - box_width - MARGIN + 10
box_y = devis_y - 120
# Draw client box
pdf.rect(box_x, box_y, box_width, box_height, stroke=1)
# Client Info
pdf.setFont("Helvetica", STANDARD_FONT_SIZE)
client_info = [
data.client_name,
data.project,
data.address,
data.ville, # Add ville here
data.client_phone
]
available_height = box_height - BOX_PADDING * 2
y_position = box_y + box_height - BOX_PADDING
for text in client_info:
if text:
# Calculate available width for text
available_width = box_width - BOX_PADDING * 2
# Get wrapped lines
lines = draw_wrapped_text(pdf, str(text), box_x, y_position, available_width)
# Draw each line
for line in lines:
text_width = pdf.stringWidth(line, "Helvetica", STANDARD_FONT_SIZE)
x = box_x + (box_width - text_width) / 2
pdf.drawString(x, y_position, line)
y_position -= STANDARD_FONT_SIZE + 2 # Add some spacing between lines
# Add extra spacing between different info pieces
y_position -= 2
# Info boxes (Date, N° Devis, PLANCHER) - adjusted starting position
info_y = top_margin - 50
box_label_width = 120
box_value_width = 80
for label, value in [
("Date du devis :", data.date.strftime("%d/%m/%Y")),
("N° Devis :", data.invoice_number),
("PLANCHER :", data.frame_number or "PH RDC")
]:
draw_box(pdf, MARGIN, info_y, box_label_width, LINE_HEIGHT, fill_color=HEADER_BLUE)
pdf.setFillColorRGB(*WHITE)
pdf.drawString(MARGIN + BOX_PADDING, info_y + 6, label)
draw_box(pdf, MARGIN + box_label_width, info_y, box_value_width, LINE_HEIGHT, fill_color=WHITE)
pdf.setFillColorRGB(*BLACK)
draw_centered_text(pdf, str(value), MARGIN + box_label_width, info_y + 6, box_value_width)
info_y -= 25
# Table headers
table_y = info_y - 30
headers = [
("Description", 150),
("Unité", 50),
("NBRE", 50),
("ML/Qté", 60),
("P.U", 60),
("Total HT", 170)
]
total_width = sum(width for _, width in headers)
table_x = (page_width - total_width) / 2 # Center table
draw_box(pdf, table_x, table_y, total_width, LINE_HEIGHT, fill_color=HEADER_BLUE)
pdf.setFillColorRGB(*WHITE)
current_x = table_x
for title, width in headers:
draw_box(pdf, current_x, table_y, width, LINE_HEIGHT)
pdf.setFillColorRGB(*WHITE)
draw_centered_text(pdf, title, current_x, table_y + 6, width)
current_x += width
# Draw sections and items
current_y = table_y - LINE_HEIGHT - 10
def draw_section_header2(title):
nonlocal current_y
draw_box(pdf, table_x, current_y, total_width, LINE_HEIGHT, fill_color=WHITE)
pdf.setFont("Helvetica-Bold", 9)
pdf.setFillColorRGB(*BLACK)
pdf.drawString(table_x + BOX_PADDING, current_y + 6, title)
current_y -= LINE_HEIGHT
pdf.setFont("Helvetica", STANDARD_FONT_SIZE)
def format_currency(value):
return "{:,.2f}".format(value).replace(",", " ")
def draw_item_row(item, indent=False):
nonlocal current_y
pdf.setFillColorRGB(*BLACK)
current_x = table_x
draw_box(pdf, current_x, current_y, total_width, LINE_HEIGHT, fill_color=WHITE)
cells = [
(" " + item.description if indent else item.description, 150),
(item.unit, 50),
(str(item.quantity), 50),
(f"{item.length:.2f}", 60),
(f"{format_currency(item.unit_price)}", 60),
(f"{format_currency(item.total_price)}", 170)
]
for i, (value, width) in enumerate(cells):
draw_box(pdf, current_x, current_y, width, LINE_HEIGHT)
if i == len(cells) - 1:
pdf.setFont("Helvetica-Bold", STANDARD_FONT_SIZE)
else:
pdf.setFont("Helvetica", STANDARD_FONT_SIZE)
if isinstance(value, str) and value.startswith(" "):
pdf.drawString(current_x + 20, current_y + 6, value.strip())
else:
draw_centered_text(pdf, str(value), current_x, current_y + 6, width)
current_x += width
pdf.setFont("Helvetica", STANDARD_FONT_SIZE)
current_y -= LINE_HEIGHT
# Check if we need a new page
if current_y < BOTTOM_MARGIN:
pdf.showPage()
current_y = page_height - MARGIN
# Redraw headers on the new page
draw_box(pdf, table_x, current_y, total_width, LINE_HEIGHT, fill_color=HEADER_BLUE)
pdf.setFillColorRGB(*WHITE)
current_x = table_x
for title, width in headers:
draw_box(pdf, current_x, current_y, width, LINE_HEIGHT)
pdf.setFillColorRGB(*WHITE)
draw_centered_text(pdf, title, current_x, current_y + 6, width)
current_x += width
current_y -= LINE_HEIGHT
# Draw sections
sections = [
("POUTRELLES", "PCP"),
("HOURDIS", "HOURDIS"),
("PANNEAU TREILLIS SOUDES", "PTS"),
("AGGLOS", "AGGLOS")
]
for section_title, keyword in sections:
# Get items for this section
items = [i for i in data.items if keyword in i.description]
# Only draw section if it has items
if items:
draw_section_header2(section_title)
for item in items:
draw_item_row(item, indent=(keyword != "lfflflflf"))
# NB box with text
nb_box_width = 200
nb_box_height = 80
pdf.setFillColorRGB(*BLACK)
pdf.rect(20, current_y - nb_box_height, nb_box_width, nb_box_height, stroke=1)
pdf.setFont("Helvetica-Bold", STANDARD_FONT_SIZE)
pdf.drawString(30, current_y - nb_box_height + 60, "NB:")
# Add the new text
pdf.setFont("Helvetica", STANDARD_FONT_SIZE)
nb_text = "Toute modification apportée aux plans BA initialement fournis, entraine automatiquement la modification de ce devis."
words = nb_text.split()
lines = []
current_line = []
for word in words:
current_line.append(word)
if pdf.stringWidth(' '.join(current_line), "Helvetica", STANDARD_FONT_SIZE) > nb_box_width - 20:
current_line.pop()
lines.append(' '.join(current_line))
current_line = [word]
if current_line:
lines.append(' '.join(current_line))
for i, line in enumerate(lines):
pdf.drawString(30, current_y - nb_box_height + 45 - (i * 10), line)
# ADD text after the NB box
pdf.setFont("Helvetica-Bold", 9)
pdf.drawString(30, current_y - nb_box_height - 15, "Validité du devis : 1 mois")
# Totals section
pdf.setFont("Helvetica-Bold", 12)
current_y -= 20
totals_table_width = 300
row_height = 20
# Define the widths for the left and right boxes
left_box_width = 130 # Width of the left box
right_box_width = totals_table_width - left_box_width # Remaining width for the right box
for i, (label1, label2, value) in enumerate([
("Total", "H.T", f"{format_currency(data.total_ht)} DH"),
("TVA", "20%", f"{format_currency(data.tax)} DH"),
("Total", "TTC", f"{format_currency(data.total_ttc)} DH")
]):
y = current_y - (i * row_height)
totals_x = (page_width - totals_table_width) - 27
# Draw the left box
draw_box(pdf, totals_x, y, left_box_width, row_height)
# Draw the right box
draw_box(pdf, totals_x + left_box_width, y, right_box_width, row_height)
# Draw left-aligned labels in the left box
pdf.drawString(totals_x + 10, y + 6, f"{label1} {label2}")
# Calculate the center x position for the right box
center_x = totals_x + left_box_width + (right_box_width / 2)
# Draw centered value in the right box
pdf.drawCentredString(center_x, y + 6, value)
# Footer
## add the firt line text
pdf.setFont("Helvetica", STANDARD_FONT_SIZE - 3)
footer_text = "CARRIPREFA"
pdf.drawCentredString(page_width / 2, 40, footer_text)
pdf.setFont("Helvetica", STANDARD_FONT_SIZE - 3)
footer_text = "Douar Ait Laarassi Tidili, Cercle El Kelâa, Route de Safi, Km 14-40000 Marrakech"
pdf.drawCentredString(page_width / 2 + 20, 30, footer_text)
# Add commercial info to footer
commercial_info = dict(
salah="06 62 29 99 78",
khaled="06 66 24 80 94",
ismail="06 66 24 50 15",
jamal="06 70 08 36 50"
)
if data.commercial and data.commercial.lower() != 'divers':
commercial_text = f"Commercial: {data.commercial.upper()}"
commercial_phone = f"Tél: {commercial_info.get(data.commercial.lower(), '')}"
pdf.drawString(MARGIN + 10, 30, commercial_text)
pdf.drawString(MARGIN + 10, 20, commercial_phone)
footer_contact = "Tél: 05 24 01 33 34 Fax : 05 24 01 33 29 E-mail : [email protected]"
pdf.drawCentredString(page_width / 2 + 10, 20, footer_contact)
pdf.drawString(page_width - 100, 30, f"Date: {datetime.datetime.now().strftime('%d/%m/%Y %H:%M')}")
pdf.drawString(page_width - 100, 20, f"Page {pdf.getPageNumber()}/{pdf.getPageNumber()}")
else:
footer_contact = "Tél: 05 24 01 33 34 Fax : 05 24 01 33 29 E-mail : [email protected]"
pdf.drawCentredString(page_width / 2 + 10, 20, footer_contact)
pdf.drawString(page_width - 100, 30, f"Date: {datetime.datetime.now().strftime('%d/%m/%Y %H:%M')}")
pdf.drawString(page_width - 100, 20, f"Page {pdf.getPageNumber()}/{pdf.getPageNumber()}")
pdf.save()
buffer.seek(0)
return buffer.getvalue()
except Exception as e:
logger.error(f"Error in PDF generation: {str(e)}", exc_info=True)
raise