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 = (("" + contract.client.name + "") if contract.client.is_company else "") + "
" + "A l'attention de " + contract.client.contact + "
" + contract.client.address + "
" + str(contract.client.postal_code) + " " + contract.client.city + "

Concernant la/le " + contract.title + "" 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 = "" + title_text + "" 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)