592 lines
18 KiB
Python
592 lines
18 KiB
Python
|
|
import html
|
|
import io
|
|
import pathlib
|
|
import string
|
|
|
|
import jinja2
|
|
import odfdo
|
|
from src import models
|
|
from src.contracts import service
|
|
from weasyprint import HTML
|
|
|
|
|
|
def generate_html_contract(
|
|
contract: models.Contract,
|
|
cheques: list[dict],
|
|
occasionals: list[dict],
|
|
reccurents: list[dict],
|
|
recurrent_price: float | None = None,
|
|
total_price: float | None = None
|
|
) -> bytes:
|
|
"""Generate a html contract
|
|
Arguments:
|
|
contract(models.Contract): Contract source.
|
|
cheques(list[dict]): cheques formated in dict.
|
|
occasionals(list[dict]): occasional products.
|
|
reccurents(list[dict]): recurrent products.
|
|
recurrent_price(float | None = None): total price of recurent products.
|
|
total_price(float | None = Non): total price.
|
|
Return:
|
|
result(bytes): contract file in pdf as bytes.
|
|
"""
|
|
template_dir = pathlib.Path("./src/contracts/templates").resolve()
|
|
template_loader = jinja2.FileSystemLoader(searchpath=template_dir)
|
|
template_env = jinja2.Environment(
|
|
loader=template_loader,
|
|
autoescape=jinja2.select_autoescape(["html", "xml"])
|
|
)
|
|
template_file = "layout.html"
|
|
template = template_env.get_template(template_file)
|
|
output_text = template.render(
|
|
contract_name=contract.form.name,
|
|
contract_type=contract.form.productor.type,
|
|
contract_season=contract.form.season,
|
|
referer_name=contract.form.referer.name,
|
|
referer_email=contract.form.referer.email,
|
|
productor_name=contract.form.productor.name,
|
|
productor_address=contract.form.productor.address,
|
|
payment_methods_map={
|
|
"cheque": "Ordre du chèque",
|
|
"transfer": "virements"},
|
|
productor_payment_methods=contract.form.productor.payment_methods,
|
|
member_name=f'{
|
|
html.escape(
|
|
contract.firstname)} {
|
|
html.escape(
|
|
contract.lastname)}',
|
|
member_email=html.escape(
|
|
contract.email),
|
|
member_phone=html.escape(
|
|
contract.phone),
|
|
contract_start_date=contract.form.start,
|
|
contract_end_date=contract.form.end,
|
|
occasionals=occasionals,
|
|
recurrents=reccurents,
|
|
recurrent_price=recurrent_price,
|
|
total_price=total_price,
|
|
contract_payment_method={
|
|
"cheque": "chèque",
|
|
"transfer": "virements"}[
|
|
contract.payment_method],
|
|
cheques=cheques)
|
|
|
|
return HTML(
|
|
string=output_text,
|
|
base_url=template_dir,
|
|
).write_pdf()
|
|
|
|
|
|
def flatten(xss):
|
|
"""flatten a list of list.
|
|
"""
|
|
return [x for xs in xss for x in xs]
|
|
|
|
|
|
def create_column_style_width(size: str) -> odfdo.Style:
|
|
"""Create a table columm style for a given width.
|
|
Paramenters:
|
|
size(str): size of the style (format <number><unit>)
|
|
unit can be in, cm... see odfdo documentation.
|
|
Returns:
|
|
odfdo.Style with the correct column-width attribute.
|
|
"""
|
|
return odfdo.Element.from_tag(
|
|
'<style:style style:name="product-table.A" style:family="table-column">'
|
|
f'<style:table-column-properties style:column-width="{size}"/>'
|
|
'</style:style>'
|
|
)
|
|
|
|
|
|
def create_row_style_height(size: str) -> odfdo.Style:
|
|
"""Create a table height style for a given height.
|
|
Paramenters:
|
|
size(str): size of the style (format <number><unit>)
|
|
unit can be in, cm... see odfdo documentation.
|
|
Returns:
|
|
odfdo.Style with the correct column-height attribute.
|
|
"""
|
|
return odfdo.Element.from_tag(
|
|
'<style:style style:name="product-table.A" style:family="table-row">'
|
|
f'<style:table-row-properties style:row-height="{size}"/>'
|
|
'</style:style>'
|
|
)
|
|
|
|
|
|
def create_currency_style(name: str = 'currency-euro'):
|
|
"""Create a table currency style.
|
|
Paramenters:
|
|
name(str): name of the style (default to `currency-euro`).
|
|
Returns:
|
|
odfdo.Style with the correct column-height attribute.
|
|
"""
|
|
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',
|
|
currency: bool = False,
|
|
) -> odfdo.Style:
|
|
"""Create a cell style
|
|
Paramenters:
|
|
name(str): name of the style (default to `centered-cell`).
|
|
font_size(str): font_size of the cell (default to `10pt`).
|
|
bold(str): is the text bold (default to `False`).
|
|
background_color(str): background_color of the cell
|
|
(default to `#FFFFFF`).
|
|
color(str): color of the text of the cell (default to `#000000`).
|
|
currency(str): is the cell a currency (default to `False`).
|
|
Returns:
|
|
odfdo.Style with the correct column-height attribute.
|
|
"""
|
|
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"
|
|
{currency_attr}>
|
|
<style:table-cell-properties
|
|
fo:border="0.75pt solid #000000"
|
|
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 apply_cell_style(
|
|
document: odfdo.Document,
|
|
table: odfdo.Table,
|
|
currency_cols: list[int]
|
|
):
|
|
"""Apply cell style
|
|
"""
|
|
document.insert_style(
|
|
style=create_currency_style(),
|
|
)
|
|
header_style = document.insert_style(
|
|
create_cell_style(
|
|
name="header-cells",
|
|
bold=True,
|
|
font_size='12pt',
|
|
background_color="#3480eb",
|
|
color="#FFF"
|
|
)
|
|
)
|
|
body_style_even = document.insert_style(
|
|
create_cell_style(
|
|
name="body-style-even",
|
|
bold=False,
|
|
background_color="#e8eaed",
|
|
color="#000000",
|
|
)
|
|
)
|
|
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',
|
|
)
|
|
)
|
|
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 == 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
|
|
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
|
|
):
|
|
"""Apply column height for a given 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_cell_style_by_column(
|
|
table: odfdo.Table,
|
|
style: odfdo.Style,
|
|
col_index: int
|
|
):
|
|
"""Apply cell style for a given table
|
|
"""
|
|
for cell in table.get_column_cells(col_index):
|
|
cell.style = style
|
|
|
|
|
|
def apply_column_width_style(
|
|
document: odfdo.Document,
|
|
table: odfdo.Table,
|
|
widths: list[str]
|
|
):
|
|
"""Apply column width style to a table.
|
|
Parameters:
|
|
document(odfdo.Document): Document where the table is located.
|
|
table(odfdo.Table): Table to apply columns widths.
|
|
widths(list[str]): list of width in format <number><unit> unit ca be
|
|
in, cm... see odfdo documentation.
|
|
"""
|
|
styles = []
|
|
for w in widths:
|
|
styles.append(document.insert_style(
|
|
style=create_column_style_width(w), name=w, automatic=True)
|
|
)
|
|
|
|
for position in range(table.width):
|
|
col = table.get_column(position)
|
|
col.style = styles[position]
|
|
table.set_column(position, col)
|
|
|
|
|
|
def generate_ods_letters(n: int):
|
|
"""Generate letters following excel format.
|
|
Arguments:
|
|
n(int): `n` letters to generate.
|
|
Return:
|
|
result(list[str]): list of `n` letters that follow excel pattern.
|
|
"""
|
|
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:
|
|
"""Compute price for a give contract.
|
|
"""
|
|
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 transform_formula_cells(sheet: odfdo.Spreadsheet):
|
|
"""Transform cell value to a formula using odfdo.
|
|
"""
|
|
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]
|
|
):
|
|
"""Merge cells for shipment header.
|
|
"""
|
|
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,
|
|
):
|
|
"""Generate excel recap for a list of contracts.
|
|
"""
|
|
product_unit_map = {
|
|
'1': 'g',
|
|
'2': 'Kg',
|
|
'3': 'Piece'
|
|
}
|
|
if len(contracts) <= 0:
|
|
# TODO: raise correct exception
|
|
return None
|
|
first_contract = contracts[0]
|
|
reccurents_sorted = sorted(
|
|
[
|
|
product for product in first_contract.products
|
|
if product.product.type == models.ProductType.RECCURENT
|
|
],
|
|
key=lambda x: x.product.name
|
|
)
|
|
recurrents = [
|
|
f'{pr.product.name}{f' - {pr.product.quantity}{pr.product.quantity_unit}'
|
|
if pr.product.quantity else ''} ({product_unit_map[pr.product.unit]})'
|
|
for pr in reccurents_sorted
|
|
]
|
|
occasionnals_sorted = sorted(
|
|
[
|
|
product for product in first_contract.products
|
|
if product.product.type == models.ProductType.OCCASIONAL
|
|
],
|
|
key=lambda x: (x.shipment.name, x.product.name)
|
|
)
|
|
occasionnals = [
|
|
f'{pr.product.name}{f' - {pr.product.quantity}{pr.product.quantity_unit}'
|
|
if pr.product.quantity else ''} ({product_unit_map[pr.product.unit]})'
|
|
for pr in occasionnals_sorted
|
|
]
|
|
shipments = form.shipments
|
|
occasionnals_header = [
|
|
occ for shipment in shipments for occ in occasionnals
|
|
]
|
|
|
|
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 +
|
|
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
|
|
]
|
|
|
|
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]
|
|
)
|
|
|
|
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,
|
|
*main_data,
|
|
footer
|
|
]
|
|
|
|
doc = odfdo.Document('spreadsheet')
|
|
sheet = doc.body.get_sheet(0)
|
|
sheet.name = 'Recap'
|
|
sheet.set_values(data)
|
|
|
|
if len(occasionnals) > 0:
|
|
merge_shipment_cells(
|
|
sheet,
|
|
prefix_header,
|
|
recurrents,
|
|
occasionnals,
|
|
shipments
|
|
)
|
|
|
|
transform_formula_cells(sheet)
|
|
|
|
apply_column_width_style(
|
|
doc,
|
|
doc.body.get_table(0),
|
|
['2cm'] +
|
|
['6cm'] * 2 +
|
|
['2.40cm'] * (len(payment_header) - 1) +
|
|
['4cm'] * len(recurrents) +
|
|
['4cm'] +
|
|
['4cm'] * (len(occasionnals_header) + 1) +
|
|
['4cm', '8cm', '6cm']
|
|
)
|
|
apply_column_height_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)
|
|
return buffer.getvalue()
|