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);