diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index f2b13e5..3eb18e6 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -17,88 +17,6 @@ from src.database import get_session router = APIRouter(prefix='/contracts') -def compute_recurrent_prices( - products_quantities: list[dict], - nb_shipment: int -): - """Compute price for recurrent products""" - result = 0 - for product_quantity in products_quantities: - product = product_quantity['product'] - quantity = product_quantity['quantity'] - result += compute_product_price(product, quantity, nb_shipment) - return result - - -def compute_occasional_prices(occasionals: list[dict]): - """Compute prices for occassional products""" - result = 0 - for occasional in occasionals: - result += occasional['price'] - return result - - -def compute_product_price( - product: models.Product, - quantity: int, - nb_shipment: int = 1 -): - """Compute price for a product""" - product_quantity_unit = ( - 1 if product.unit == models.Unit.KILO else 1000 - ) - final_quantity = ( - quantity if product.price else quantity / product_quantity_unit - ) - final_price = ( - product.price if product.price else product.price_kg - ) - return final_price * final_quantity * nb_shipment - - -def find_dict_in_list(lst, key, value): - """Find the index of a dictionnary in a list of dictionnaries given a key - and a value. - """ - for i, dic in enumerate(lst): - if dic[key].id == value: - return i - return -1 - - -def create_occasional_dict(contract_products: list[models.ContractProduct]): - """Create a dictionnary of occasional products""" - result = [] - for contract_product in contract_products: - existing_id = find_dict_in_list( - result, - 'shipment', - contract_product.shipment.id - ) - if existing_id < 0: - result.append({ - 'shipment': contract_product.shipment, - 'price': compute_product_price( - contract_product.product, - contract_product.quantity - ), - 'products': [{ - 'product': contract_product.product, - 'quantity': contract_product.quantity - }] - }) - else: - result[existing_id]['products'].append({ - 'product': contract_product.product, - 'quantity': contract_product.quantity - }) - result[existing_id]['price'] += compute_product_price( - contract_product.product, - contract_product.quantity - ) - return result - - @router.post('') async def create_contract( contract: models.ContractCreate, @@ -114,7 +32,7 @@ async def create_contract( new_contract.products ) ) - occasionals = create_occasional_dict(occasional_contract_products) + occasionals = service.create_occasional_dict(occasional_contract_products) recurrents = list( map( lambda x: {'product': x.product, 'quantity': x.quantity}, @@ -127,11 +45,13 @@ async def create_contract( ) ) ) - recurrent_price = compute_recurrent_prices( + prices = service.generate_products_prices( + occasionals, recurrents, - len(new_contract.form.shipments) + new_contract.form.shipments ) - price = recurrent_price + compute_occasional_prices(occasionals) + recurrent_price = prices['recurrent'] + total_price = prices['total'] cheques = list( map( lambda x: {'name': x.name, 'value': x.value}, @@ -145,7 +65,7 @@ async def create_contract( occasionals, recurrents, '{:10.2f}'.format(recurrent_price), - '{:10.2f}'.format(price) + '{:10.2f}'.format(total_price) ) pdf_file = io.BytesIO(pdf_bytes) contract_id = ( @@ -154,7 +74,8 @@ async def create_contract( f'{new_contract.form.productor.type}_' f'{new_contract.form.season}' ) - service.add_contract_file(session, new_contract.id, pdf_bytes, price) + service.add_contract_file( + session, new_contract.id, pdf_bytes, total_price) except Exception as error: raise HTTPException( status_code=400, diff --git a/backend/src/contracts/generate_contract.py b/backend/src/contracts/generate_contract.py index bfd8e1f..e1d7b2c 100644 --- a/backend/src/contracts/generate_contract.py +++ b/backend/src/contracts/generate_contract.py @@ -1,13 +1,16 @@ 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 @@ -96,60 +99,97 @@ def create_row_style_height(size: str) -> odfdo.Style: ) -def create_center_cell_style(name: str = "centered-cell") -> odfdo.Style: +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'' - '' - '' - '' + f""" + + + + """ ) -def create_cell_style_with_font(name: str = "font", font_size="14pt", bold: bool = False) -> odfdo.Style: - return odfdo.Element.from_tag( - f'' - '' - f'' - '' - ) - - -def apply_center_cell_style(document: odfdo.Document, row: odfdo.Row): - style = document.insert_style( - create_center_cell_style() - ) - for cell in row.get_cells(): - cell.style = style - - -def apply_column_height_style(document: odfdo.Document, row: odfdo.Row, height: str): - style = document.insert_style( - style=create_row_style_height(height), name=height, automatic=True - ) - row.style = style - - -def apply_font_style(document: odfdo.Document, table: odfdo.Table, size: str = "14pt"): - style_header = document.insert_style( - style=create_cell_style_with_font( - 'header_font', font_size=size, bold=True +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" ) ) - style_body = document.insert_style( - style=create_cell_style_with_font( - 'body_font', font_size=size, bold=False + body_style_even = document.insert_style( + create_cell_style( + name="body-style-even", + bold=False, + background_color="#e8eaed", + color="#000000" ) ) - for position in range(table.height): - row = table.get_row(position) + 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_header if position == 0 or position == 1 else style_body - for paragraph in cell.get_paragraphs(): - paragraph.style = cell.style + 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]): @@ -162,7 +202,8 @@ def apply_column_width_style(document: odfdo.Document, table: odfdo.Table, width styles = [] for w in widths: styles.append(document.insert_style( - style=create_column_style_width(w), name=w, automatic=True)) + style=create_column_style_width(w), name=w, automatic=True) + ) for position in range(table.width): col = table.get_column(position) @@ -170,75 +211,201 @@ def apply_column_width_style(document: odfdo.Document, table: odfdo.Table, width 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, ): - recurrents = [pr.name for pr in form.productor.products if pr.type == - models.ProductType.RECCURENT] + 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 = [pr.name for pr in form.productor.products if pr.type == - models.ProductType.OCCASIONAL] + 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] - shipment_header = flatten( - [[f'{shipment.name} - {shipment.date.strftime('%Y-%m-%d')}'] + ["" * len(occasionnals)] for shipment in shipments]) - product_unit_map = { - "1": "g", - "2": "kg", - "3": "p" - } + occ for shipment in shipments for occ in occasionnals + ] - header = ( - ["Nom", "Email"] + - ["Tarif panier", "Total Paniers", "Total à payer"] + - ["Cheque 1", "Cheque 2", "Cheque 3"] + - [f"Total {len(shipments)} livraisons + produits occasionnels"] + + 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 + - ["Remarques", "Nom"] + 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 = [ - [""] * (9 + len(recurrents)) + shipment_header, + [''] * (len(prefix_header) + len(recurrents) + 1) + shipment_header, header, *[ [ + f'{index + 1}', f'{contract.firstname} {contract.lastname}', f'{contract.email}', - *[f'{pr.quantity} {product_unit_map[pr.product.unit]}' for pr in sorted( - contract.products, key=lambda x: x.product.name) if pr.product.type == models.ProductType.RECCURENT], - *[f'{pr.quantity} {product_unit_map[pr.product.unit]}' for pr in sorted( - contract.products, key=lambda x: x.product.name) if pr.product.type == models.ProductType.OCCASIONAL], - "", + *[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 contract in contracts - ] + ] for index, contract in enumerate(contracts) + ], + footer ] - doc = odfdo.Document("spreadsheet") + doc = odfdo.Document('spreadsheet') sheet = doc.body.get_sheet(0) sheet.name = 'Recap' sheet.set_values(data) - apply_column_width_style(doc, doc.body.get_table(0), ["4cm"] * len(header)) - apply_column_height_style( - doc, - doc.body.get_table(0).get_rows((1, 1))[0], - "1.20cm" - ) - apply_center_cell_style(doc, doc.body.get_table(0).get_rows((1, 1))[0]) - apply_font_style(doc, doc.body.get_table(0)) - index = 9 + len(recurrents) + + 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'] * len(info_header) + + ['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('test.ods') + doc.save(buffer) + # doc.save('test.ods') return buffer.getvalue() diff --git a/backend/src/contracts/service.py b/backend/src/contracts/service.py index 79cb063..969eafa 100644 --- a/backend/src/contracts/service.py +++ b/backend/src/contracts/service.py @@ -166,3 +166,103 @@ def is_allowed( .distinct() ) return len(session.exec(statement).all()) > 0 + + +def compute_recurrent_prices( + products_quantities: list[dict], + nb_shipment: int +): + """Compute price for recurrent products""" + result = 0 + for product_quantity in products_quantities: + product = product_quantity['product'] + quantity = product_quantity['quantity'] + result += compute_product_price(product, quantity, nb_shipment) + return result + + +def compute_occasional_prices(occasionals: list[dict]): + """Compute prices for occassional products""" + result = 0 + for occasional in occasionals: + result += occasional['price'] + return result + + +def compute_product_price( + product: models.Product, + quantity: int, + nb_shipment: int = 1 +): + """Compute price for a product""" + product_quantity_unit = ( + 1 if product.unit == models.Unit.KILO else 1000 + ) + final_quantity = ( + quantity if product.price else quantity / product_quantity_unit + ) + final_price = ( + product.price if product.price else product.price_kg + ) + return final_price * final_quantity * nb_shipment + + +def find_dict_in_list(lst, key, value): + """Find the index of a dictionnary in a list of dictionnaries given a key + and a value. + """ + for i, dic in enumerate(lst): + if dic[key].id == value: + return i + return -1 + + +def create_occasional_dict(contract_products: list[models.ContractProduct]): + """Create a dictionnary of occasional products""" + result = [] + for contract_product in contract_products: + existing_id = find_dict_in_list( + result, + 'shipment', + contract_product.shipment.id + ) + if existing_id < 0: + result.append({ + 'shipment': contract_product.shipment, + 'price': compute_product_price( + contract_product.product, + contract_product.quantity + ), + 'products': [{ + 'product': contract_product.product, + 'quantity': contract_product.quantity + }] + }) + else: + result[existing_id]['products'].append({ + 'product': contract_product.product, + 'quantity': contract_product.quantity + }) + result[existing_id]['price'] += compute_product_price( + contract_product.product, + contract_product.quantity + ) + return result + + +def generate_products_prices( + occasionals: list[dict], + recurrents: list[dict], + shipments: list[models.ShipmentPublic] +): + recurrent_price = compute_recurrent_prices( + recurrents, + len(shipments) + ) + occasional_price = compute_occasional_prices(occasionals) + price = recurrent_price + occasional_price + return { + 'total': price, + 'recurrent': recurrent_price, + 'occasionnal': occasional_price + } diff --git a/backend/test.pdf b/backend/test.pdf deleted file mode 100644 index 554d27a..0000000 Binary files a/backend/test.pdf and /dev/null differ diff --git a/frontend/src/pages/Contracts/index.tsx b/frontend/src/pages/Contracts/index.tsx index 74e56c7..9f0e882 100644 --- a/frontend/src/pages/Contracts/index.tsx +++ b/frontend/src/pages/Contracts/index.tsx @@ -27,6 +27,8 @@ export default function Contracts() { const { data: allContracts } = useGetContracts(); const forms = useMemo(() => { + if (!allContracts) + return []; return allContracts ?.map((contract: Contract) => contract.form.name) .filter((contract, index, array) => array.indexOf(contract) === index);