fix contract recap

This commit is contained in:
2026-03-05 20:58:00 +01:00
parent 5c356f5802
commit cb0235e19f
5 changed files with 153 additions and 89 deletions

View File

@@ -1,26 +0,0 @@
default_language_version:
python: python3.13
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: trailing-whitespace
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: check-yaml
- id: check-toml
- id: mixed-line-ending
- id: end-of-file-fixer
- repo: local
hooks:
- id: check-pylint
name: check-pylint
entry: pylint -d R0801,R0903,W0511,W0603,C0103,R0902
language: system
types: [python]
pass_filenames: false
args:
- backend

View File

@@ -34,8 +34,6 @@ dependencies = [
"pytest", "pytest",
"pytest-cov", "pytest-cov",
"pytest-mock", "pytest-mock",
"autopep8",
"prek",
"pylint", "pylint",
] ]

View File

@@ -250,12 +250,13 @@ def get_contract_recap(
) )
form = form_service.get_one(session, form_id=form_id) form = form_service.get_one(session, form_id=form_id)
contracts = service.get_all(session, user, forms=[form.name]) contracts = service.get_all(session, user, forms=[form.name])
filename = f'{form.name}_recapitulatif_contrats.ods'
return StreamingResponse( return StreamingResponse(
io.BytesIO(generate_recap(contracts, form)), io.BytesIO(generate_recap(contracts, form)),
media_type='application/zip', media_type='application/vnd.oasis.opendocument.spreadsheet',
headers={ headers={
'Content-Disposition': ( 'Content-Disposition': (
'attachment; filename=filename.ods' f'attachment; filename={filename}'
) )
} }
) )

View File

@@ -1,14 +1,11 @@
import html import html
import io import io
import math
import pathlib import pathlib
import string import string
import jinja2 import jinja2
import odfdo import odfdo
# from odfdo import Cell, Document, Row, Style, Table
from odfdo.element import Element
from src import models from src import models
from src.contracts import service from src.contracts import service
from weasyprint import HTML from weasyprint import HTML
@@ -99,20 +96,34 @@ def create_row_style_height(size: str) -> odfdo.Style:
) )
def create_currency_style(name:str = 'currency-euro'):
return odfdo.Element.from_tag(
f"""
<number:currency-style style:name="{name}">
<number:number number:min-integer-digits="1" number:decimal-places="2"/>
<number:text> €</number:text>
</number:currency-style>"""
)
def create_cell_style( def create_cell_style(
name: str = "centered-cell", name: str = "centered-cell",
font_size: str = '10pt', font_size: str = '10pt',
bold: bool = False, bold: bool = False,
background_color: str = '#FFFFFF', background_color: str = '#FFFFFF',
color: str = '#000000' color: str = '#000000',
currency: bool = False,
) -> odfdo.Style: ) -> odfdo.Style:
bold_attr = """ bold_attr = """
fo:font-weight="bold" fo:font-weight="bold"
style:font-weight-asian="bold" style:font-weight-asian="bold"
style:font-weight-complex="bold" style:font-weight-complex="bold"
""" if bold else '' """ if bold else ''
currency_attr = """
style:data-style-name="currency-euro">
""" if currency else ''
return odfdo.Element.from_tag( return odfdo.Element.from_tag(
f"""<style:style style:name="{name}" style:family="table-cell"> f"""<style:style style:name="{name}" style:family="table-cell"
{currency_attr}>
<style:table-cell-properties <style:table-cell-properties
fo:border="0.75pt solid #000000" fo:border="0.75pt solid #000000"
style:vertical-align="middle" style:vertical-align="middle"
@@ -127,7 +138,10 @@ def create_cell_style(
) )
def apply_cell_style(document: odfdo.Document, table: odfdo.Table): def apply_cell_style(document: odfdo.Document, table: odfdo.Table, currency_cols: list[int]):
document.insert_style(
style=create_currency_style(),
)
header_style = document.insert_style( header_style = document.insert_style(
create_cell_style( create_cell_style(
name="header-cells", name="header-cells",
@@ -143,7 +157,7 @@ def apply_cell_style(document: odfdo.Document, table: odfdo.Table):
name="body-style-even", name="body-style-even",
bold=False, bold=False,
background_color="#e8eaed", background_color="#e8eaed",
color="#000000" color="#000000",
) )
) )
@@ -152,7 +166,7 @@ def apply_cell_style(document: odfdo.Document, table: odfdo.Table):
name="body-style-odd", name="body-style-odd",
bold=False, bold=False,
background_color="#FFFFFF", background_color="#FFFFFF",
color="#000000" color="#000000",
) )
) )
@@ -164,17 +178,53 @@ def apply_cell_style(document: odfdo.Document, table: odfdo.Table):
) )
) )
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()): for index, row in enumerate(table.get_rows()):
style = body_style_even style = body_style_even
currency_style = body_style_even_currency
if index == 0 or index == 1: if index == 0 or index == 1:
style = header_style style = header_style
elif index % 2 == 0:
style = body_style_even
elif index == len(table.get_rows()) - 1: elif index == len(table.get_rows()) - 1:
style = footer_style style = footer_style
currency_style = footer_style_currency
elif index % 2 == 0:
style = body_style_even
currency_style = body_style_even_currency
else: else:
style = body_style_odd style = body_style_odd
for cell in row.get_cells(): 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 cell.style = style
@@ -191,6 +241,12 @@ def apply_column_height_style(document: odfdo.Document, table: odfdo.Table):
else: else:
row.style = body_style row.style = body_style
def apply_cell_style_by_column(table: odfdo.Table, style: odfdo.Style, col_index: int):
for cell in table.get_column_cells(col_index):
print(cell.style)
cell.style = style
print(cell.serialize())
def apply_column_width_style(document: odfdo.Document, table: odfdo.Table, widths: list[str]): def apply_column_width_style(document: odfdo.Document, table: odfdo.Table, widths: list[str]):
"""Apply column width style to a table. """Apply column width style to a table.
@@ -256,6 +312,31 @@ def compute_contract_prices(contract: models.Contract) -> dict:
return prices return prices
def transform_formula_cells(sheet: odfdo.Spreadsheet):
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]
):
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( def generate_recap(
contracts: list[models.Contract], contracts: list[models.Contract],
form: models.Form, form: models.Form,
@@ -266,13 +347,13 @@ def generate_recap(
'3': 'Piece' '3': 'Piece'
} }
recurrents = [ recurrents = [
f'{pr.name}({product_unit_map[pr.unit]})' f'{pr.name}{f' - {pr.quantity}{pr.quantity_unit}' if pr.quantity else ''} ({product_unit_map[pr.unit]})'
for pr in form.productor.products for pr in form.productor.products
if pr.type == models.ProductType.RECCURENT if pr.type == models.ProductType.RECCURENT
] ]
recurrents.sort() recurrents.sort()
occasionnals = [ occasionnals = [
f'{pr.name}({product_unit_map[pr.unit]})' f'{pr.name}{f' - {pr.quantity}{pr.quantity_unit}' if pr.quantity else ''} ({product_unit_map[pr.unit]})'
for pr in form.productor.products for pr in form.productor.products
if pr.type == models.ProductType.OCCASIONAL if pr.type == models.ProductType.OCCASIONAL
] ]
@@ -292,7 +373,6 @@ def generate_recap(
info_header + info_header +
payment_header payment_header
) )
suffix_header: list[str] = [ suffix_header: list[str] = [
'Total produits occasionnels', 'Total produits occasionnels',
'Remarques', 'Remarques',
@@ -326,9 +406,6 @@ def generate_recap(
len(info_header)+len(payment_formula_letters) + len(info_header)+len(payment_formula_letters) +
len(recurent_formula_letters)+len(occasionnals_header) + 1 len(recurent_formula_letters)+len(occasionnals_header) + 1
] ]
print(payment_formula_letters)
print(recurent_formula_letters)
print(occasionnals_formula_letters)
footer = ( footer = (
['', 'Total contrats', ''] + ['', 'Total contrats', ''] +
@@ -340,29 +417,39 @@ def generate_recap(
for letter in occasionnals_formula_letters] for letter in occasionnals_formula_letters]
) )
data = [ main_data = []
[''] * (len(prefix_header) + len(recurrents) + 1) + shipment_header, for index, contract in enumerate(contracts):
header, 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'{index + 1}',
f'{contract.firstname} {contract.lastname}', f'{contract.firstname} {contract.lastname}',
f'{contract.email}', f'{contract.email}',
*[float(contract.cheques[i].value) if len( *[float(contract.cheques[i].value)
contract.cheques) > i else '' for i in range(3)], if len(contract.cheques) > i
compute_contract_prices(contract)['total'], else ''
*[pr.quantity for pr in sorted( for i in range(3)],
contract.products, key=lambda x: x.product.name) prices['total'],
if pr.product.type == models.ProductType.RECCURENT], *[pr.quantity for pr in recurrent_sorted],
compute_contract_prices(contract)['recurrent'], prices['recurrent'],
*[pr.quantity for pr in sorted( *[pr.quantity for pr in occasionnal_sorted],
contract.products, key=lambda x: x.product.name) prices['occasionnal'],
if pr.product.type == models.ProductType.OCCASIONAL],
compute_contract_prices(contract)['occasionnal'],
'', '',
f'{contract.firstname} {contract.lastname}', f'{contract.firstname} {contract.lastname}',
] for index, contract in enumerate(contracts) ])
],
data = [
[''] * (len(prefix_header) + len(recurrents) + 1) + shipment_header,
header,
*main_data,
footer footer
] ]
@@ -371,41 +458,45 @@ def generate_recap(
sheet.name = 'Recap' sheet.name = 'Recap'
sheet.set_values(data) sheet.set_values(data)
index = len(prefix_header) + len(recurrents) + 1 if len(occasionnals) > 0:
for _ in enumerate(shipments): merge_shipment_cells(
startcol = index sheet,
endcol = index+len(occasionnals) - 1 prefix_header,
sheet.set_span((startcol, 0, endcol, 0), merge=True) recurrents,
index += len(occasionnals) occasionnals,
shipments
)
for row in sheet.get_rows(): transform_formula_cells(sheet)
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( apply_column_width_style(
doc, doc,
doc.body.get_table(0), doc.body.get_table(0),
['2cm'] + ['2cm'] +
['4cm'] * 2 + ['6cm'] * 2 +
['2.40cm'] * (len(payment_header) - 1) + ['2.40cm'] * (len(payment_header) - 1) +
['4cm'] * len(recurrents) + ['4cm'] * len(recurrents) +
['4cm'] + ['4cm'] +
['4cm'] * (len(occasionnals_header) + 1) + ['4cm'] * (len(occasionnals_header) + 1) +
['4cm', '8cm', '4cm'] ['4cm', '8cm', '6cm']
) )
apply_column_height_style( apply_column_height_style(
doc, doc,
doc.body.get_table(0), doc.body.get_table(0),
) )
apply_cell_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) doc.body.append(sheet)
buffer = io.BytesIO() buffer = io.BytesIO()
doc.save(buffer) doc.save(buffer)
# doc.save('test.ods')
return buffer.getvalue() return buffer.getvalue()

View File

@@ -81,8 +81,8 @@ def update_one(
return new_productor return new_productor
def delete_one(session: Session, id: int) -> models.ProductorPublic: def delete_one(session: Session, _id: int) -> models.ProductorPublic:
statement = select(models.Productor).where(models.Productor.id == id) statement = select(models.Productor).where(models.Productor.id == _id)
result = session.exec(statement) result = session.exec(statement)
productor = result.first() productor = result.first()
if not productor: if not productor: