add functionnal recap ready for tests

This commit is contained in:
Julien Aldon
2026-03-05 17:17:23 +01:00
parent 3cfa60507e
commit ff19448991
5 changed files with 359 additions and 169 deletions

View File

@@ -17,88 +17,6 @@ from src.database import get_session
router = APIRouter(prefix='/contracts') 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('') @router.post('')
async def create_contract( async def create_contract(
contract: models.ContractCreate, contract: models.ContractCreate,
@@ -114,7 +32,7 @@ async def create_contract(
new_contract.products new_contract.products
) )
) )
occasionals = create_occasional_dict(occasional_contract_products) occasionals = service.create_occasional_dict(occasional_contract_products)
recurrents = list( recurrents = list(
map( map(
lambda x: {'product': x.product, 'quantity': x.quantity}, 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, 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( cheques = list(
map( map(
lambda x: {'name': x.name, 'value': x.value}, lambda x: {'name': x.name, 'value': x.value},
@@ -145,7 +65,7 @@ async def create_contract(
occasionals, occasionals,
recurrents, recurrents,
'{:10.2f}'.format(recurrent_price), '{:10.2f}'.format(recurrent_price),
'{:10.2f}'.format(price) '{:10.2f}'.format(total_price)
) )
pdf_file = io.BytesIO(pdf_bytes) pdf_file = io.BytesIO(pdf_bytes)
contract_id = ( contract_id = (
@@ -154,7 +74,8 @@ async def create_contract(
f'{new_contract.form.productor.type}_' f'{new_contract.form.productor.type}_'
f'{new_contract.form.season}' 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: except Exception as error:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,

View File

@@ -1,13 +1,16 @@
import html import html
import io import io
import math
import pathlib import pathlib
import string
import jinja2 import jinja2
import odfdo import odfdo
# from odfdo import Cell, Document, Row, Style, Table # from odfdo import Cell, Document, Row, Style, Table
from odfdo.element import Element from odfdo.element import Element
from src import models from src import models
from src.contracts import service
from weasyprint import HTML 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( 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">
'<style:table-cell-properties style:vertical-align="middle" fo:wrap-option="wrap"/>' <style:table-cell-properties
'<style:paragraph-properties fo:text-align="center"/>' fo:border="0.75pt solid #000000"
'</style:style>' style:vertical-align="middle"
fo:wrap-option="wrap"
fo:background-color="{background_color}"/>
<style:paragraph-properties fo:text-align="center"/>
<style:text-properties
{bold_attr}
fo:font-size="{font_size}"
fo:color="{color}"/>
</style:style>"""
) )
def create_cell_style_with_font(name: str = "font", font_size="14pt", bold: bool = False) -> odfdo.Style: def apply_cell_style(document: odfdo.Document, table: odfdo.Table):
return odfdo.Element.from_tag( header_style = document.insert_style(
f'<style:style style:name="{name}" style:family="table-cell" ' create_cell_style(
f'xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0">' name="header-cells",
'<style:table-cell-properties style:vertical-align="middle" fo:wrap-option="wrap"/>' bold=True,
f'<style:paragraph-properties fo:text-align="center" fo:font-size="{font_size}" ' font_size='12pt',
f'{"fo:font-weight=\"bold\"" if bold else ""}/>' background_color="#3480eb",
'</style:style>' color="#FFF"
)
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
) )
) )
style_body = document.insert_style( body_style_even = document.insert_style(
style=create_cell_style_with_font( create_cell_style(
'body_font', font_size=size, bold=False name="body-style-even",
bold=False,
background_color="#e8eaed",
color="#000000"
) )
) )
for position in range(table.height): body_style_odd = document.insert_style(
row = table.get_row(position) 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(): for cell in row.get_cells():
cell.style = style_header if position == 0 or position == 1 else style_body cell.style = style
for paragraph in cell.get_paragraphs():
paragraph.style = cell.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]): 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 = [] styles = []
for w in widths: for w in widths:
styles.append(document.insert_style( 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): for position in range(table.width):
col = table.get_column(position) 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) 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( def generate_recap(
contracts: list[models.Contract], contracts: list[models.Contract],
form: models.Form, form: models.Form,
): ):
recurrents = [pr.name for pr in form.productor.products if pr.type == product_unit_map = {
models.ProductType.RECCURENT] '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() recurrents.sort()
occasionnals = [pr.name for pr in form.productor.products if pr.type == occasionnals = [
models.ProductType.OCCASIONAL] f'{pr.name}({product_unit_map[pr.unit]})'
for pr in form.productor.products
if pr.type == models.ProductType.OCCASIONAL
]
occasionnals.sort() occasionnals.sort()
shipments = form.shipments shipments = form.shipments
occasionnals_header = [ occasionnals_header = [
occ for shipment in shipments for occ in occasionnals] 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"
}
header = ( info_header: list[str] = ['', 'Nom', 'Email']
["Nom", "Email"] + cheque_header: list[str] = ['Cheque 1', 'Cheque 2', 'Cheque 3']
["Tarif panier", "Total Paniers", "Total à payer"] + payment_header = (
["Cheque 1", "Cheque 2", "Cheque 3"] + cheque_header +
[f"Total {len(shipments)} livraisons + produits occasionnels"] + [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 + recurrents +
['Total produits récurrents'] +
occasionnals_header + 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 = [ data = [
[""] * (9 + len(recurrents)) + shipment_header, [''] * (len(prefix_header) + len(recurrents) + 1) + shipment_header,
header, header,
*[ *[
[ [
f'{index + 1}',
f'{contract.firstname} {contract.lastname}', f'{contract.firstname} {contract.lastname}',
f'{contract.email}', f'{contract.email}',
*[f'{pr.quantity} {product_unit_map[pr.product.unit]}' for pr in sorted( *[float(contract.cheques[i].value) if len(
contract.products, key=lambda x: x.product.name) if pr.product.type == models.ProductType.RECCURENT], contract.cheques) > i else '' for i in range(3)],
*[f'{pr.quantity} {product_unit_map[pr.product.unit]}' for pr in sorted( compute_contract_prices(contract)['total'],
contract.products, key=lambda x: x.product.name) if pr.product.type == models.ProductType.OCCASIONAL], *[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}', 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 = doc.body.get_sheet(0)
sheet.name = 'Recap' sheet.name = 'Recap'
sheet.set_values(data) sheet.set_values(data)
apply_column_width_style(doc, doc.body.get_table(0), ["4cm"] * len(header))
apply_column_height_style( index = len(prefix_header) + len(recurrents) + 1
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)
for _ in enumerate(shipments): for _ in enumerate(shipments):
startcol = index startcol = index
endcol = index+len(occasionnals) - 1 endcol = index+len(occasionnals) - 1
sheet.set_span((startcol, 0, endcol, 0), merge=True) sheet.set_span((startcol, 0, endcol, 0), merge=True)
index += len(occasionnals) 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) doc.body.append(sheet)
buffer = io.BytesIO() buffer = io.BytesIO()
doc.save('test.ods') doc.save(buffer)
# doc.save('test.ods')
return buffer.getvalue() return buffer.getvalue()

View File

@@ -166,3 +166,103 @@ def is_allowed(
.distinct() .distinct()
) )
return len(session.exec(statement).all()) > 0 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
}

Binary file not shown.

View File

@@ -27,6 +27,8 @@ export default function Contracts() {
const { data: allContracts } = useGetContracts(); const { data: allContracts } = useGetContracts();
const forms = useMemo(() => { const forms = useMemo(() => {
if (!allContracts)
return [];
return allContracts return allContracts
?.map((contract: Contract) => contract.form.name) ?.map((contract: Contract) => contract.form.name)
.filter((contract, index, array) => array.indexOf(contract) === index); .filter((contract, index, array) => array.indexOf(contract) === index);