import html import io import math import pathlib import string import jinja2 import odfdo # from odfdo import Cell, Document, Row, Style, Table from odfdo.element import Element from src import models from src.contracts import service from weasyprint import HTML def generate_html_contract( contract: models.Contract, cheques: list[dict], occasionals: list[dict], reccurents: list[dict], recurrent_price: float | None = None, total_price: float | None = None ): template_dir = pathlib.Path("./src/contracts/templates").resolve() template_loader = jinja2.FileSystemLoader(searchpath=template_dir) template_env = jinja2.Environment( loader=template_loader, autoescape=jinja2.select_autoescape(["html", "xml"])) template_file = "layout.html" template = template_env.get_template(template_file) output_text = template.render( contract_name=contract.form.name, contract_type=contract.form.productor.type, contract_season=contract.form.season, referer_name=contract.form.referer.name, referer_email=contract.form.referer.email, productor_name=contract.form.productor.name, productor_address=contract.form.productor.address, payment_methods_map={ "cheque": "Ordre du chèque", "transfer": "virements"}, productor_payment_methods=contract.form.productor.payment_methods, member_name=f'{ html.escape( contract.firstname)} { html.escape( contract.lastname)}', member_email=html.escape( contract.email), member_phone=html.escape( contract.phone), contract_start_date=contract.form.start, contract_end_date=contract.form.end, occasionals=occasionals, recurrents=reccurents, recurrent_price=recurrent_price, total_price=total_price, contract_payment_method={ "cheque": "chèque", "transfer": "virements"}[ contract.payment_method], cheques=cheques) return HTML( string=output_text, base_url=template_dir, ).write_pdf() def flatten(xss): return [x for xs in xss for x in xs] def create_column_style_width(size: str) -> odfdo.Style: """Create a table columm style for a given width. Paramenters: size(str): size of the style (format ) unit can be in, cm... see odfdo documentation. Returns: odfdo.Style with the correct column-width attribute. """ return odfdo.Element.from_tag( '' f'' '' ) def create_row_style_height(size: str) -> odfdo.Style: """Create a table height style for a given height. Paramenters: size(str): size of the style (format ) unit can be in, cm... see odfdo documentation. Returns: odfdo.Style with the correct column-height attribute. """ return odfdo.Element.from_tag( '' f'' '' ) def create_cell_style( name: str = "centered-cell", font_size: str = '10pt', bold: bool = False, background_color: str = '#FFFFFF', color: str = '#000000' ) -> odfdo.Style: bold_attr = """ fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" """ if bold else '' return odfdo.Element.from_tag( f""" """ ) def apply_cell_style(document: odfdo.Document, table: odfdo.Table): header_style = document.insert_style( create_cell_style( name="header-cells", bold=True, font_size='12pt', background_color="#3480eb", color="#FFF" ) ) body_style_even = document.insert_style( create_cell_style( name="body-style-even", bold=False, background_color="#e8eaed", color="#000000" ) ) body_style_odd = document.insert_style( create_cell_style( name="body-style-odd", bold=False, background_color="#FFFFFF", color="#000000" ) ) footer_style = document.insert_style( create_cell_style( name="footer-cells", bold=True, font_size='12pt', ) ) for index, row in enumerate(table.get_rows()): style = body_style_even if index == 0 or index == 1: style = header_style elif index % 2 == 0: style = body_style_even elif index == len(table.get_rows()) - 1: style = footer_style else: style = body_style_odd for cell in row.get_cells(): cell.style = style def apply_column_height_style(document: odfdo.Document, table: odfdo.Table): header_style = document.insert_style( style=create_row_style_height('1.60cm'), name='1.60cm', automatic=True ) body_style = document.insert_style( style=create_row_style_height('0.90cm'), name='0.90cm', automatic=True ) for index, row in enumerate(table.get_rows()): if index == 1: row.style = header_style else: row.style = body_style def apply_column_width_style(document: odfdo.Document, table: odfdo.Table, widths: list[str]): """Apply column width style to a table. Parameters: document(odfdo.Document): Document where the table is located. table(odfdo.Table): Table to apply columns widths. widths(list[str]): list of width in format unit ca be in, cm... see odfdo documentation. """ styles = [] for w in widths: styles.append(document.insert_style( style=create_column_style_width(w), name=w, automatic=True) ) for position in range(table.width): col = table.get_column(position) col.style = styles[position] table.set_column(position, col) def generate_ods_letters(n: int): letters = string.ascii_lowercase result = [] for i in range(n): if i > len(letters) - 1: letter = f'{letters[int(i / len(letters)) - 1]}' letter += f'{letters[i % len(letters)]}' result.append(letter) continue letter = letters[i] result.append(letters[i]) return result def compute_contract_prices(contract: models.Contract) -> dict: occasional_contract_products = list( filter( lambda contract_product: ( contract_product.product.type == models.ProductType.OCCASIONAL ), contract.products ) ) occasionals_dict = service.create_occasional_dict( occasional_contract_products) recurrents_dict = list( map( lambda x: {'product': x.product, 'quantity': x.quantity}, filter( lambda contract_product: ( contract_product.product.type == models.ProductType.RECCURENT ), contract.products ) ) ) prices = service.generate_products_prices( occasionals_dict, recurrents_dict, contract.form.shipments ) return prices def generate_recap( contracts: list[models.Contract], form: models.Form, ): product_unit_map = { '1': 'g', '2': 'Kg', '3': 'Piece' } recurrents = [ f'{pr.name}({product_unit_map[pr.unit]})' for pr in form.productor.products if pr.type == models.ProductType.RECCURENT ] recurrents.sort() occasionnals = [ f'{pr.name}({product_unit_map[pr.unit]})' for pr in form.productor.products if pr.type == models.ProductType.OCCASIONAL ] occasionnals.sort() shipments = form.shipments occasionnals_header = [ occ for shipment in shipments for occ in occasionnals ] info_header: list[str] = ['', 'Nom', 'Email'] cheque_header: list[str] = ['Cheque 1', 'Cheque 2', 'Cheque 3'] payment_header = ( cheque_header + [f'Total {len(shipments)} livraisons + produits occasionnels'] ) prefix_header: list[str] = ( info_header + payment_header ) suffix_header: list[str] = [ 'Total produits occasionnels', 'Remarques', 'Nom' ] shipment_header = flatten([ [f'{shipment.name} - {shipment.date.strftime('%Y-%m-%d')}'] + ['' * len(occasionnals)] for shipment in shipments] + [''] * len(suffix_header) ) header: list[str] = ( prefix_header + recurrents + ['Total produits récurrents'] + occasionnals_header + suffix_header ) letters = generate_ods_letters(len(header)) payment_formula_letters = letters[ len(info_header):len(info_header) + len(payment_header) ] recurent_formula_letters = letters[ len(info_header)+len(payment_formula_letters): len(info_header)+len(payment_formula_letters)+len(recurrents) + 1 ] occasionnals_formula_letters = letters[ len(info_header)+len(payment_formula_letters)+len(recurent_formula_letters): len(info_header)+len(payment_formula_letters) + len(recurent_formula_letters)+len(occasionnals_header) + 1 ] print(payment_formula_letters) print(recurent_formula_letters) print(occasionnals_formula_letters) footer = ( ['', 'Total contrats', ''] + [f'=SUM({letter}3:{letter}{2+len(contracts)})' for letter in payment_formula_letters] + [f'=SUM({letter}3:{letter}{2+len(contracts)})' for letter in recurent_formula_letters] + [f'=SUM({letter}3:{letter}{2+len(contracts)})' for letter in occasionnals_formula_letters] ) data = [ [''] * (len(prefix_header) + len(recurrents) + 1) + shipment_header, header, *[ [ f'{index + 1}', f'{contract.firstname} {contract.lastname}', f'{contract.email}', *[float(contract.cheques[i].value) if len( contract.cheques) > i else '' for i in range(3)], compute_contract_prices(contract)['total'], *[pr.quantity for pr in sorted( contract.products, key=lambda x: x.product.name) if pr.product.type == models.ProductType.RECCURENT], compute_contract_prices(contract)['recurrent'], *[pr.quantity for pr in sorted( contract.products, key=lambda x: x.product.name) if pr.product.type == models.ProductType.OCCASIONAL], compute_contract_prices(contract)['occasionnal'], '', f'{contract.firstname} {contract.lastname}', ] for index, contract in enumerate(contracts) ], footer ] doc = odfdo.Document('spreadsheet') sheet = doc.body.get_sheet(0) sheet.name = 'Recap' sheet.set_values(data) index = len(prefix_header) + len(recurrents) + 1 for _ in enumerate(shipments): startcol = index endcol = index+len(occasionnals) - 1 sheet.set_span((startcol, 0, endcol, 0), merge=True) index += len(occasionnals) for row in sheet.get_rows(): for cell in row.get_cells(): if not cell.value or cell.get_attribute("office:value-type") == "float": continue if '=' in cell.value: formula = cell.value cell.clear() cell.formula = formula apply_column_width_style( doc, doc.body.get_table(0), ['2cm'] + ['4cm'] * 2 + ['2.40cm'] * (len(payment_header) - 1) + ['4cm'] * len(recurrents) + ['4cm'] + ['4cm'] * (len(occasionnals_header) + 1) + ['4cm', '8cm', '4cm'] ) apply_column_height_style( doc, doc.body.get_table(0), ) apply_cell_style(doc, doc.body.get_table(0)) doc.body.append(sheet) buffer = io.BytesIO() doc.save(buffer) # doc.save('test.ods') return buffer.getvalue()