ComptaClub/tools/pdf_generator.py

672 lines
23 KiB
Python

from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.platypus import Table, TableStyle, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
from django.conf import settings
from datetime import date, datetime, timedelta
from django.db.models import Q, Sum
import os
import locale
from PIL import Image
from textwrap import wrap
from comptabilite.models import (
Transaction,
TransactionType,
EvaluationRules,
EvaluationRulesAdaptation,
Annuality,
ComplementaryInformations,
)
from comptabilite.tools import (
get_transactions_and_sums_for_year_and_type,
get_transactiontypes_and_total_amount_transactions,
get_transactiontype_and_sum_for_spf_export,
get_right_engagement_and_sump_spf,
get_asset_liability_and_sump_spf,
)
import os
import yaml
import re
EXPENSES = 0
RECETTES = 1
X = 35
Y = 841.89
INDENT = 5
RIGHT_X = 595.27 - X
TITLED_X = 125
INDENTED_X = X + INDENT
INDENTED_RIGHT_X = RIGHT_X - INDENT
MIDDLE = (RIGHT_X - X) / 2
COMMON_LINE_HEIGHT = -15
SMALL_LINE_HEIGHT = -10
LARGE_LINE_HEIGHT = -20
DOUBLE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 2
BIG_LINE_HEIGHT = COMMON_LINE_HEIGHT * 3
HUGE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 4
class PDFDocument(object):
""" Create the PDF object, using the response object as its "file" """
def __init__(self, response):
self.document = Canvas(response, pagesize=A4)
self.y = Y - X
self.__load_config()
def __load_config(self):
""" Charge le contenu du fichier SITE_CONFIG.YAML qui contient les données relatives à
l'ASBL.
"""
current_path = os.path.dirname(os.path.realpath(__file__))
self.club_infos = None
with open(os.path.join(current_path, "site_config.yaml"), "r") as stream:
try:
self.club_infos = yaml.load(stream, Loader=yaml.FullLoader)
except yaml.YAMLError as exc:
print(exc)
def add_vspace(self, height=COMMON_LINE_HEIGHT):
""" Passe à la ligne avec une hauteur de ligne passée en paramètre.
Args:
height (int): hauteur de la ligne.
Returns:
ne retourne rien.
"""
self.y += height
# if y < 120;
# document.PageBreak()
# y = 790
def add_string(
self, x, string, font_family="Helvetica", font_decoration=None, font_size=10
):
if font_decoration:
font_family += "-" + font_decoration
self.document.setFont(font_family, font_size)
self.document.drawString(x, self.y, string)
def add_new_line(
self,
x,
string,
height=COMMON_LINE_HEIGHT,
font_family="Helvetica",
font_decoration=None,
font_size=10,
):
self.add_vspace(height)
self.add_string(x, string, font_family, font_decoration, font_size)
def add_header(self, contract=None):
""" Génère le header du document.
Args:
contract (contract): instance de la class Contract.
Returns:
ne retourne rien.
"""
rect_height = 40
self.document.rect(X, self.y, RIGHT_X - X, -rect_height, fill=0)
self.add_new_line(INDENTED_X, self.club_infos["NAME"])
self.document.drawRightString(
INDENTED_RIGHT_X, self.y, "N° Entreprise : " + self.club_infos["BCE_NUMBER"]
)
self.add_new_line(
INDENTED_X,
"Siège social : "
+ self.club_infos["ADDRESS"]
+ " - "
+ self.club_infos["CITY"]
+ " "
+ self.club_infos["ZIP"],
)
if contract is not None:
self.document.drawRightString(
INDENTED_RIGHT_X, self.y, "N° de Référence : " + str(contract.reference)
)
self.add_vspace(BIG_LINE_HEIGHT)
def download(self):
""" Close the PDF object cleanly, and send it to download. """
self.document.showPage()
self.document.save()
class SpfDocument(PDFDocument):
def generate(self, accounting_year):
""" Genère un document aux normes du SPF Finance.
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
self.add_header()
self.add_title(accounting_year)
self.add_recette_expenses(accounting_year)
self.add_annexes(accounting_year)
def add_title(self, accounting_year):
""" Ajoute un titre au document.
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
self.add_string(
130,
"Comptes simplifiés de l'exercice " + accounting_year,
font_decoration="Bold",
font_size=20,
)
self.add_vspace(DOUBLE_LINE_HEIGHT)
def add_recette_expenses(self, accounting_year):
""" Ajoute l'état des recettes et dépense pour une année comptable passée en paramètre.
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
self.add_string(
X, "Etat recettes/dépenses (en €)", font_decoration="Oblique", font_size=14
)
self.__display_transactiontypes_table(accounting_year)
self.add_vspace()
def add_annexes(self, accounting_year):
""" Ajoute les annexes au document PDF
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
self.add_new_line(X, "Annexes", font_decoration="Oblique", font_size=14)
self.add_evaluation_rules(accounting_year)
self.add_modification_evaluation_rules(accounting_year)
self.add_additional_information(accounting_year)
self.add_patrimony(accounting_year)
self.add_right_engagement(accounting_year)
def add_evaluation_rules(self, accounting_year):
""" Ajoute les règles d'évaluation au PDF
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
self.add_new_line(X, "1. Résumé des règles d'évaluation")
rules_list = EvaluationRules.objects.filter(
Q(stop_date__year__lte=accounting_year) | Q(stop_date__isnull=True)
).exclude(start_date__year__gt=accounting_year)
if rules_list:
for rule in rules_list:
self.add_new_line(
INDENTED_X + INDENT * 2,
rule.label + " : " + rule.explanation,
font_size=9,
)
else:
self.add_new_line(
INDENTED_X + INDENT * 2, "Pas de règle d'évaluation.", font_size=9
)
self.add_vspace()
def add_modification_evaluation_rules(self, accounting_year):
""" Ajoute les modifications d'évaluation au PDF
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
self.add_new_line(X, "2. Adaptation des règles d'évaluation")
rules_adaptation_list = EvaluationRulesAdaptation.objects.filter(
start_date__year=accounting_year
)
if rules_adaptation_list:
for line in rules_adaptation_list:
self.add_new_line(
INDENTED_X + INDENT * 2,
line.label + " : " + line.information,
font_size=9,
)
else:
self.add_new_line(
INDENTED_X + INDENT * 2,
"Pas d'adaptation des règles d'évaluation.",
font_size=9,
)
self.add_vspace()
def add_additional_information(self, accounting_year):
""" Ajoute les informations complémentaires au PDF
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
self.add_new_line(X, "3. Informations complémentaires")
annuality = Annuality.objects.filter(year__year=accounting_year)
complementary_informations = ComplementaryInformations.objects.filter(
annuality=annuality[0]
)
if complementary_informations:
for line in complementary_informations:
self.add_new_line(
INDENTED_X + INDENT * 2,
line.label + " : " + line.information,
font_size=9,
)
else:
self.add_new_line(
INDENTED_X + INDENT * 2,
"Pas d'informations complémentaires.",
font_size=9,
)
self.add_vspace()
def add_patrimony(self, accounting_year):
""" Ajoute les informations du patrimoine au PDF
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien.
"""
self.add_new_line(X, "4. Etat du patrimoine")
annuality = Annuality.objects.filter(year__year=accounting_year)
tmp_compta_expenses = get_transactiontype_and_sum_for_spf_export(
accounting_year, EXPENSES
)
tmp_compta_recettes = get_transactiontype_and_sum_for_spf_export(
accounting_year, RECETTES
)
assets_list = get_asset_liability_and_sump_spf(accounting_year, category=0)
for item in assets_list:
if item[0] == "Liquidité":
item[1] += (
annuality[0].opening_balance
+ tmp_compta_recettes["sum_total_transaction"]
- tmp_compta_expenses["sum_total_transaction"]
)
liability_list = get_asset_liability_and_sump_spf(accounting_year, category=1)
self.__display_table_header("AVOIRS", "DETTES")
save = self.y
self.__display_table_two_column(1, assets_list)
longest_y = self.y
self.y = save
self.__display_table_two_column(2, liability_list)
if self.y > longest_y:
self.y = longest_y
self.add_vspace()
def add_right_engagement(self, accounting_year):
""" Ajoute les droits & engagements au PDF
Args:
accounting_year (int): année comptable.
Returns:
ne retourne rien
"""
self.add_new_line(
X,
"5. Droits et engagements importants qui ne sont pas susceptibles d'être quantifiés",
)
# self.__display_table_header("DROITS", "ENGAGEMENT")
self.add_new_line(
INDENTED_X + INDENT * 2, "Pas de droits ou d'engagements.", font_size=9
)
self.add_vspace()
def __display_transactiontypes_table(self, accounting_year):
""" Ajoute le table pour les recettes & dépenses d'une année comptable.
Args:
accounting_year (int): année comptable
Returns:
ne retourne rien.
"""
expenses = get_transactiontype_and_sum_for_spf_export(accounting_year, EXPENSES)
recettes = get_transactiontype_and_sum_for_spf_export(accounting_year, RECETTES)
self.__display_table_header("DEPENSES", "RECETTES")
self.__display_transactiontype_table_body(
expenses,
recettes,
int(expenses["sum_total_transaction"]),
int(recettes["sum_total_transaction"]),
)
def __display_table_header(self, title_left, title_right):
self.add_vspace()
self.document.rect(X, self.y - 5, RIGHT_X - X, -COMMON_LINE_HEIGHT, fill=0)
self.add_string(INDENTED_X, title_left, font_decoration="Bold", font_size=9)
self.add_string(
MIDDLE + INDENTED_X, title_right, font_decoration="Bold", font_size=9
)
def __display_transactiontype_table_body(
self, expenses, recettes, totalexpenses, totalrecettes
):
for i in range(4):
self.__display_table_line(
expenses["transaction_type_info"][i]["label"],
int(expenses["transaction_type_info"][i]["sum_total_amount"]),
recettes["transaction_type_info"][i]["label"],
int(recettes["transaction_type_info"][i]["sum_total_amount"]),
)
self.add_vspace()
self.document.rect(
X, self.y - 5, MIDDLE, 5 * -COMMON_LINE_HEIGHT
) # Séparation en deux
self.document.rect(
MIDDLE, self.y - 5, MIDDLE, 5 * -COMMON_LINE_HEIGHT
) # Séparation descriptif et montant recettes
self.y -= COMMON_LINE_HEIGHT
self.__display_table_line(
"Total des dépenses",
totalexpenses,
"Total des recettes",
totalrecettes,
font_decoration="Bold",
)
def __display_key_part(self, column_number, key, font_decoration=None):
""" Ajoute dans la colonne d'un tableau (à deux colonnes) la clef d'un couple clef/valeur.
Args:
column_number (int): numéro de la colonne du tableau
key (str): la clef à afficher
font_decoration (str): décoration de la police de carectères
Returns:
ne retourne rien.
"""
if column_number == 1:
space = INDENTED_X
else:
space = MIDDLE + INDENTED_X
self.add_string(space, key, font_decoration=font_decoration, font_size=9)
def __display_value_part(self, column_number, value):
""" Ajoute dans la colonne d'un tableau (à deux colonnes) la valeur d'un couple clef/valeur
Args:
column_number (int): numéro de la colonne du tableau
value (str): la valeur à afficher
Returns:
ne retourne rien.
"""
if column_number == 1:
space = MIDDLE + X - INDENT
else:
space = INDENTED_RIGHT_X
self.document.drawRightString(
space, self.y, str(locale.format("%d", int(value), grouping=True)),
)
def __display_column(self, column_number, key, value, font_decoration):
self.__display_key_part(column_number, key, font_decoration)
self.__display_value_part(column_number, value)
def __display_table_line(self, key1, value1, key2, value2, font_decoration=None):
self.document.rect(X, self.y - 5, RIGHT_X - X, COMMON_LINE_HEIGHT, fill=0)
self.add_vspace()
self.__display_column(1, key1, value1, font_decoration)
self.__display_column(2, key2, value2, font_decoration)
def __display_table_two_column(self, column_number, list):
if column_number == 1:
begin_rect_line = X
begin_rect_res = MIDDLE
begin_text = INDENTED_X
begin_res = MIDDLE + X - INDENT
else:
begin_rect_line = MIDDLE + X
begin_rect_res = MIDDLE * 2
begin_text = MIDDLE + INDENTED_X
begin_res = INDENTED_RIGHT_X
total = 0
# locale.setlocale(locale.LC_ALL, "pt_br.utf-8")
for line in list:
self.document.rect(
begin_rect_line, self.y - 5, MIDDLE, COMMON_LINE_HEIGHT, fill=0
)
self.document.rect(
begin_rect_res, self.y - 5, X, COMMON_LINE_HEIGHT, fill=0
)
self.add_new_line(begin_text, line[0], font_size=9)
self.document.drawRightString(
begin_res, self.y, str(locale.format("%d", int(line[1]), grouping=True))
)
total += int(line[1])
return total
class BillPaper(PDFDocument):
def generate(self, contract):
""" Génère une facture pour un contrat
Args:
contract (contract): instance de la class Contract.
Returns:
ne retourne rien
"""
self.amount = 0
self.styles = getSampleStyleSheet()
self.style = self.styles["BodyText"]
self.add_header(contract)
self.add_bill_title(contract)
self.add_prestations(contract)
self.add_conclusion(contract)
self.add_signature()
self.add_terms_of_sales()
def add_bill_title(self, contract):
""" Génère le titre de la facture.
Args:
contract (contract): instance de la class Contract.
Returns:
ne retourne rien
"""
self.y = 650
text = (("<b>" + contract.client.name + "</b>") if contract.client.is_company else "") + "<br />" + "A l'attention de <b>" + contract.client.contact + "</b><br />" + contract.client.address + "<br />" + str(contract.client.postal_code) + " " + contract.client.city + "<br /><br />Concernant la/le <b>" + contract.title + "</b>"
paragraph = Paragraph(text, self.style)
width, height = paragraph.wrap(10*cm, 10*cm)
paragraph.drawOn(self.document, TITLED_X, self.y)
self.add_vspace(BIG_LINE_HEIGHT)
def __add_section_title(self, title_text):
""" Ajout le titre d'une section """
text = "<b>" + title_text + "</b>"
paragraph = Paragraph(text, self.style)
width, height = paragraph.wrap(8*cm, 8*cm)
paragraph.drawOn(self.document, INDENTED_X, self.y)
self.add_vspace(-height)
def add_prestations(self, contract):
""" Génère l'affichage des prestations : tableau, liste des prestations, …
Args:
contract (contract): instance de la class Contract.
Returns:
ne retourne rien.
"""
self.__add_section_title("Prestations")
total = 0
elements = []
prestations_list = contract.get_prestation.all()
for prestation in prestations_list:
total += prestation.total_amount
data = [['Date', 'Libellé', 'Nbre', 'Prix Unit.', 'Total']]
for prestation in prestations_list:
data.append(
[
str(prestation.date),
prestation.label,
str(prestation.unit),
str(prestation.unit_price),
str(prestation.total_amount)
]
)
data.append([' ', ' ', ' ', 'Total', str(total)])
style = TableStyle(
[
('BACKGROUND', (0,0), (-1,0), '#317BB5'), # première ligne bleue
('TEXTCOLOR', (0,0), (-1,0), '#FFFFFF'),
('ALIGN', (2,0), (-1, 0), 'CENTER'),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('BACKGROUND', (0,-1), (-1,-1), '#317BB5'), # dernière ligne bleue
('ALIGN', (2,1), (-1, -1), 'RIGHT'),
('FONTNAME', (0,-1), (-1,-1), 'Helvetica-Bold'),
('TEXTCOLOR', (0,-1), (-1,-1), '#FFFFFF'),
]
)
table = Table(data, [2.6*cm, 8.2*cm, 2.6*cm, 2.6*cm, 2.6*cm])
table.setStyle(style)
width, height = table.wrapOn(self.document, 19*cm, 15*cm)
self.add_vspace(-(9 * height / 10))
table.drawOn(self.document, X, self.y)
self.add_vspace(-height / 3)
self.document.drawRightString(INDENTED_X + 442, self.y, "Acompte")
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(contract.advance))
self.add_vspace()
self.document.setFont("Helvetica-Bold", 10)
self.document.drawRightString(INDENTED_X + 442, self.y, "Solde à payer")
self.amount = total - contract.advance
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(self.amount))
def add_conclusion(self, contract):
""" Affiche la add_add_conclusion de la facture.
Args:
contract (contract): instance de la class Contract.
Returns:
ne retourne rien.
"""
self.add_vspace(HUGE_LINE_HEIGHT)
text = "Merci de bien vouloir payer la somme de "
self.add_new_line(INDENTED_X, text)
space = len(text)
self.add_string(INDENTED_X + (space * 4.55), str(self.amount), font_decoration="Bold")
space += len(str(self.amount) + " ")
self.add_string(INDENTED_X + (space * 4.55), "€ sur le compte ")
space += len("€ sur le compte")
self.add_string(
INDENTED_X + (space * 4.55), self.club_infos["IBAN"], font_decoration="Bold"
)
space += len(" " + self.club_infos["IBAN"] + " ")
self.add_string(
INDENTED_X + (space * 4.55),
" (" + self.club_infos["BIC"] + " - " + self.club_infos["BANK"] + ")",
)
self.add_vspace(COMMON_LINE_HEIGHT)
if not contract.is_paid:
the_date = datetime.now()
pay_date = the_date + timedelta(days=25)
self.add_string(INDENTED_X, "Pour le ")
space = len("Pour le ")
date = str(pay_date.day) + "/" + str(pay_date.month) + "/" + str(pay_date.year)
self.add_string(INDENTED_X + (space * 4.55), date, font_decoration="Bold")
space += len(date + " ")
self.add_string(INDENTED_X + (space * 4.55), " au plus tard, avec la référence :")
space += len("au plus tard, avec la référence:")
self.add_string(
INDENTED_X + (space * 4.55),
'"' + str(contract.reference) + '"',
font_decoration="Bold",
)
def add_signature(self):
""" Génère la signature. """
self.add_vspace(HUGE_LINE_HEIGHT)
self.document.drawString(
INDENTED_X,
self.y,
self.club_infos["CITY"] + ", le " + date.today().strftime("%d-%m-%Y"),
)
self.document.drawRightString(RIGHT_X, self.y, "Président")
url = os.path.join(settings.STATICFILES_DIRS[0], "img/signature.png")
self.add_vspace(DOUBLE_LINE_HEIGHT)
self.document.drawImage(url, INDENTED_X + 340, self.y, width=180, height=39)
def add_terms_of_sales(self):
""" Ajoute les conditions générales de payement au bas de la facture """
self.y = 125
self.__add_section_title("Conditions générales de paiement")
lines = [
"Facture payable au comptant.",
"En cas de défaut de paiement à l'échéance, il est dû de plein droit et sans mise en demeure, un interêt fixé au taux de 15% l'an.",
"Tout réclamation, pour être admise, doit être faite dans les huit jours de la réception de la facture.",
"En cas de litige concernant la présente facture, seuls les tribunaux de Mons seront compétents."
]
text_object = self.document.beginText()
text_object.setTextOrigin(INDENTED_X, self.y)
text_object.setFont("Helvetica", 11)
for line in lines:
wraped_text = "\n".join(wrap(line, 108))
text_object.textLines(wraped_text)
self.document.drawText(text_object)