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-cov",
"pytest-mock",
"autopep8",
"prek",
"pylint",
]

View File

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

View File

@@ -1,14 +1,11 @@
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
@@ -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(
name: str = "centered-cell",
font_size: str = '10pt',
bold: bool = False,
background_color: str = '#FFFFFF',
color: str = '#000000'
color: str = '#000000',
currency: bool = False,
) -> odfdo.Style:
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"""<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
fo:border="0.75pt solid #000000"
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(
create_cell_style(
name="header-cells",
@@ -143,7 +157,7 @@ def apply_cell_style(document: odfdo.Document, table: odfdo.Table):
name="body-style-even",
bold=False,
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",
bold=False,
background_color="#FFFFFF",
color="#000000"
color="#000000",
)
)
@@ -164,18 +178,54 @@ 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()):
style = body_style_even
currency_style = body_style_even_currency
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
currency_style = footer_style_currency
elif index % 2 == 0:
style = body_style_even
currency_style = body_style_even_currency
else:
style = body_style_odd
for cell in row.get_cells():
cell.style = style
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):
@@ -191,6 +241,12 @@ def apply_column_height_style(document: odfdo.Document, table: odfdo.Table):
else:
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]):
"""Apply column width style to a table.
@@ -256,6 +312,31 @@ def compute_contract_prices(contract: models.Contract) -> dict:
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(
contracts: list[models.Contract],
form: models.Form,
@@ -266,13 +347,13 @@ def generate_recap(
'3': 'Piece'
}
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
if pr.type == models.ProductType.RECCURENT
]
recurrents.sort()
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
if pr.type == models.ProductType.OCCASIONAL
]
@@ -292,7 +373,6 @@ def generate_recap(
info_header +
payment_header
)
suffix_header: list[str] = [
'Total produits occasionnels',
'Remarques',
@@ -326,9 +406,6 @@ def generate_recap(
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', ''] +
@@ -340,29 +417,39 @@ def generate_recap(
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,
*[
[
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)
],
*main_data,
footer
]
@@ -371,41 +458,45 @@ def generate_recap(
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)
if len(occasionnals) > 0:
merge_shipment_cells(
sheet,
prefix_header,
recurrents,
occasionnals,
shipments
)
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
transform_formula_cells(sheet)
apply_column_width_style(
doc,
doc.body.get_table(0),
['2cm'] +
['4cm'] * 2 +
['6cm'] * 2 +
['2.40cm'] * (len(payment_header) - 1) +
['4cm'] * len(recurrents) +
['4cm'] +
['4cm'] * (len(occasionnals_header) + 1) +
['4cm', '8cm', '4cm']
['4cm', '8cm', '6cm']
)
apply_column_height_style(
doc,
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)
buffer = io.BytesIO()
doc.save(buffer)
# doc.save('test.ods')
return buffer.getvalue()

View File

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