import html import io import pathlib import string import jinja2 import odfdo 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 ) -> bytes: """Generate a html contract Arguments: contract(models.Contract): Contract source. cheques(list[dict]): cheques formated in dict. occasionals(list[dict]): occasional products. reccurents(list[dict]): recurrent products. recurrent_price(float | None = None): total price of recurent products. total_price(float | None = Non): total price. Return: result(bytes): contract file in pdf as bytes. """ 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): """flatten a list of list. """ 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_currency_style(name: str = 'currency-euro'): """Create a table currency style. Paramenters: name(str): name of the style (default to `currency-euro`). 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', currency: bool = False, ) -> odfdo.Style: """Create a cell style Paramenters: name(str): name of the style (default to `centered-cell`). font_size(str): font_size of the cell (default to `10pt`). bold(str): is the text bold (default to `False`). background_color(str): background_color of the cell (default to `#FFFFFF`). color(str): color of the text of the cell (default to `#000000`). currency(str): is the cell a currency (default to `False`). Returns: odfdo.Style with the correct column-height attribute. """ bold_attr = """ fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" """ if bold else '' currency_attr = """ style:data-style-name="currency-euro"> """ if currency else '' return odfdo.Element.from_tag( f""" """ ) def apply_cell_style( document: odfdo.Document, table: odfdo.Table, currency_cols: list[int] ): """Apply cell style """ document.insert_style( style=create_currency_style(), ) 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', ) ) body_style_even_currency = document.insert_style( create_cell_style( name="body-style-even-currency", bold=False, background_color="#e8eaed", color="#000000", currency=True, ) ) body_style_odd_currency = document.insert_style( create_cell_style( name="body-style-odd-currency", bold=False, background_color="#FFFFFF", color="#000000", currency=True, ) ) footer_style_currency = document.insert_style( create_cell_style( name="footer-cells-currency", bold=True, font_size='12pt', currency=True, ) ) for index, row in enumerate(table.get_rows()): style = body_style_even currency_style = body_style_even_currency if index == 0 or index == 1: style = header_style elif index == len(table.get_rows()) - 1: style = footer_style currency_style = footer_style_currency elif index % 2 == 0: style = body_style_even currency_style = body_style_even_currency else: style = body_style_odd currency_style = body_style_odd_currency for cell_index, cell in enumerate(row.get_cells()): if cell_index in currency_cols and not (index == 0 or index == 1): cell.style = currency_style else: cell.style = style def apply_column_height_style( document: odfdo.Document, table: odfdo.Table ): """Apply column height for a given 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_cell_style_by_column( table: odfdo.Table, style: odfdo.Style, col_index: int ): """Apply cell style for a given table """ for cell in table.get_column_cells(col_index): cell.style = 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): """Generate letters following excel format. Arguments: n(int): `n` letters to generate. Return: result(list[str]): list of `n` letters that follow excel pattern. """ 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: """Compute price for a give contract. """ 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 transform_formula_cells(sheet: odfdo.Spreadsheet): """Transform cell value to a formula using odfdo. """ 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 def merge_shipment_cells( sheet: odfdo.Spreadsheet, prefix_header: list[str], recurrents: list[str], occasionnals: list[str], shipments: list[models.Shipment] ): """Merge cells for shipment header. """ 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) def generate_recap( contracts: list[models.Contract], form: models.Form, ): """Generate excel recap for a list of contracts. """ product_unit_map = { '1': 'g', '2': 'Kg', '3': 'Piece' } if len(contracts) <= 0: # TODO: raise correct exception return None first_contract = contracts[0] reccurents_sorted = sorted( [ product for product in first_contract.products if product.product.type == models.ProductType.RECCURENT ], key=lambda x: x.product.name ) recurrents = [ f'{pr.product.name}{f' - {pr.product.quantity}{pr.product.quantity_unit}' if pr.product.quantity else ''} ({product_unit_map[pr.product.unit]})' for pr in reccurents_sorted ] occasionnals_sorted = sorted( [ product for product in first_contract.products if product.product.type == models.ProductType.OCCASIONAL ], key=lambda x: (x.shipment.name, x.product.name) ) occasionnals = [ f'{pr.product.name}{f' - {pr.product.quantity}{pr.product.quantity_unit}' if pr.product.quantity else ''} ({product_unit_map[pr.product.unit]})' for pr in occasionnals_sorted ] 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 ] 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] ) main_data = [] for index, contract in enumerate(contracts): prices = compute_contract_prices(contract) occasionnal_sorted = sorted( [ product for product in contract.products if product.product.type == models.ProductType.OCCASIONAL ], key=lambda x: (x.shipment.name, x.product.name) ) recurrent_sorted = sorted( [ product for product in contract.products if product.product.type == models.ProductType.RECCURENT ], key=lambda x: x.product.name ) main_data.append([ 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)], prices['total'], *[pr.quantity for pr in recurrent_sorted], prices['recurrent'], *[pr.quantity for pr in occasionnal_sorted], prices['occasionnal'], '', f'{contract.firstname} {contract.lastname}', ]) data = [ [''] * (len(prefix_header) + len(recurrents) + 1) + shipment_header, header, *main_data, footer ] doc = odfdo.Document('spreadsheet') sheet = doc.body.get_sheet(0) sheet.name = 'Recap' sheet.set_values(data) if len(occasionnals) > 0: merge_shipment_cells( sheet, prefix_header, recurrents, occasionnals, shipments ) transform_formula_cells(sheet) apply_column_width_style( doc, doc.body.get_table(0), ['2cm'] + ['6cm'] * 2 + ['2.40cm'] * (len(payment_header) - 1) + ['4cm'] * len(recurrents) + ['4cm'] + ['4cm'] * (len(occasionnals_header) + 1) + ['4cm', '8cm', '6cm'] ) apply_column_height_style( doc, doc.body.get_table(0), ) apply_cell_style( doc, doc.body.get_table(0), [ 3, 4, 5, 6, len(info_header) + len(payment_header), len(info_header) + len(payment_header) + 1 + len(occasionnals), ] ) doc.body.append(sheet) buffer = io.BytesIO() doc.save(buffer) return buffer.getvalue()