fix contract recap
This commit is contained in:
@@ -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
|
|
||||||
@@ -34,8 +34,6 @@ dependencies = [
|
|||||||
"pytest",
|
"pytest",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
"pytest-mock",
|
"pytest-mock",
|
||||||
"autopep8",
|
|
||||||
"prek",
|
|
||||||
"pylint",
|
"pylint",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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}'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user