add contract storage fix various bugs and translations

This commit is contained in:
2026-02-16 01:23:31 +01:00
parent 627ddfc464
commit be8e32ebed
28 changed files with 1225 additions and 401 deletions

View File

@@ -1,65 +1,16 @@
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from src.database import get_session
from sqlmodel import Session
import src.forms.service as form_service
import src.shipments.service as shipment_service
import src.products.service as product_service
from src.contracts.generate_contract import generate_html_contract
import src.models as models
from src.messages import PDFerrorOccured
import src.contracts.service as service
import io
router = APIRouter(prefix='/contracts')
def find_dict_in_list(lst, key, value):
for i, dic in enumerate(lst):
if dic[key].id == value:
return i
return -1
def extract_products(session: Session, contract: dict):
planned = []
recurrent = []
for key in contract.keys():
key_list = key.split("-")
if "planned" in key:
shipment_id = int(key_list[1])
product_id = int(key_list[2])
shipment = shipment_service.get_one(session, shipment_id)
product = product_service.get_one(session, product_id)
existing_id = find_dict_in_list(planned, "shipment", shipment_id)
if existing_id >= 0:
planned[existing_id]["products"].append({
"product": product,
"quantity": contract[key],
})
planned[existing_id]['price'] += compute_product_price(product, contract[key])
else:
planned.append({
"shipment": shipment,
"price": compute_product_price(product, contract[key]),
"products": [{
"product": product,
"quantity": contract[key],
}]
})
if "recurrent" in key:
product_id = int(key_list[1])
product = product_service.get_one(session, product_id)
recurrent.append({
"product": product,
"quantity": contract[key]
})
return planned, recurrent
def compute_product_price(product: models.Product, quantity: int, nb_shipment: int = 1):
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 compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
result = 0
for product_quantity in products_quantities:
@@ -68,41 +19,106 @@ def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
result += compute_product_price(product, quantity, nb_shipment)
return result
def compute_planned_prices(planned: list[dict]):
def compute_planned_prices(planneds: list[dict]):
result = 0
for plan in planned:
result += plan['price']
for planned in planneds:
result += planned['price']
return result
def compute_product_price(product: models.Product, quantity: int, nb_shipment: int = 1):
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):
for i, dic in enumerate(lst):
if dic[key].id == value:
return i
return -1
def create_planned_dict(contract_products: list[models.ContractProduct]):
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.ContractBase,
contract: models.ContractCreate,
session: Session = Depends(get_session)
):
form = form_service.get_one(session, contract.form_id)
planned, recurrent = extract_products(session, contract.contract)
recurrent_price = compute_recurrent_prices(recurrent, len(form.shipments))
total_price = '{:10.2f}'.format(recurrent_price + compute_planned_prices(planned))
# TODO: Store contract
new_contract = service.create_one(session, contract)
planned_contract_products = list(filter(lambda contract_product: contract_product.product.type == models.ProductType.PLANNED, new_contract.products))
planneds = create_planned_dict(planned_contract_products)
recurrents = list(map(lambda x: {"product": x.product, "quantity": x.quantity}, filter(lambda contract_product: contract_product.product.type == models.ProductType.RECCURENT, new_contract.products)))
recurrent_price = compute_recurrent_prices(recurrents, len(new_contract.form.shipments))
total_price = '{:10.2f}'.format(recurrent_price + compute_planned_prices(planneds))
cheques = list(map(lambda x: {"name": x.name, "value": x.value}, new_contract.cheques))
# TODO: send contract to referer
# TODO: Store contract informations ?
try:
pdf_bytes = generate_html_contract(
form,
contract.contract,
planned,
recurrent,
'{:10.2f}'.format(recurrent_price),
new_contract,
cheques,
planneds,
recurrents,
recurrent_price,
total_price
)
pdf_file = io.BytesIO(pdf_bytes)
contract_id = f'{contract.contract['firstname']}_{contract.contract['lastname']}_{form.productor.type}_{form.season}'
contract_id = f'{new_contract.firstname}_{new_contract.lastname}_{new_contract.form.productor.type}_{new_contract.form.season}'
except:
raise HTTPException(status_code=400, detail=PDFerrorOccured)
return StreamingResponse(
pdf_file,
media_type="application/pdf",
media_type='application/pdf',
headers={
"Content-Disposition": f"attachement; filename=contract_{contract_id}.pdf"
'Content-Disposition': f'attachement; filename=contract_{contract_id}.pdf'
}
)
)
@router.get('/', response_model=list[models.ContractPublic])
def get_contracts(
forms: list[str] = Query([]),
session: Session = Depends(get_session)
):
return service.get_all(session, forms)
@router.get('/{id}', response_model=models.ContractPublic)
def get_contract(id: int, session: Session = Depends(get_session)):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result
@router.delete('/{id}', response_model=models.ContractPublic)
def delete_contract(id: int, session: Session = Depends(get_session)):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
return result

View File

@@ -5,37 +5,39 @@ import html
from weasyprint import HTML
def generate_html_contract(
form: models.Form,
contract_informations: dict,
planned: list[dict],
recurrent: list[dict],
contract: models.Contract,
cheques: list[dict],
planneds: list[dict],
reccurents: list[dict],
recurrent_price: float,
total_price: float,
):
total_price: float
):
template_dir = "./src/contracts/templates"
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=form.name,
contract_type=form.productor.type,
contract_season=form.season,
referer_name=form.referer.name,
referer_email=form.referer.email,
productor_name=form.productor.name,
productor_address=form.productor.address,
payment_methods_map={"cheque": "Ordre du chèque", "transfer": "IBAN (paiement par virements)"},
productor_payment_methods=form.productor.payment_methods,
member_name=f'{html.escape(contract_informations["firstname"])} {html.escape(contract_informations["lastname"])}',
member_email=html.escape(contract_informations["email"]),
member_phone=html.escape(contract_informations["phone"]),
contract_start_date=form.start,
contract_end_date=form.end,
planned=planned,
recurrent=recurrent,
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,
planneds=planneds,
recurrents=reccurents,
recurrent_price=recurrent_price,
total_price=total_price,
contract_payment_method={"cheque": "chèque", "transfer": "virements"}[contract.payment_method],
cheques=cheques
)
options = {
'page-size': 'Letter',

View File

@@ -0,0 +1,65 @@
from sqlmodel import Session, select
import src.models as models
def get_all(
session: Session,
forms: list[str]
) -> list[models.ContractPublic]:
statement = select(models.Contract)
if len(forms) > 0:
statement = statement.join(models.Form).where(models.Form.name.in_(forms))
return session.exec(statement.order_by(models.Contract.id)).all()
def get_one(session: Session, contract_id: int) -> models.ContractPublic:
return session.get(models.Contract, contract_id)
def create_one(session: Session, contract: models.ContractCreate) -> models.ContractPublic:
contract_create = contract.model_dump(exclude_unset=True, exclude=["products", "cheques"])
new_contract = models.Contract(**contract_create)
new_contract.cheques = [
models.Cheque(
name=cheque.name,
value=cheque.value,
contract_id=new_contract.id
) for cheque in contract.cheques
]
new_contract.products = [
models.ContractProduct(
contract_id=new_contract.id,
product_id=contract_product.product_id,
shipment_id=contract_product.shipment_id,
quantity=contract_product.quantity
) for contract_product in contract.products
]
session.add(new_contract)
session.commit()
session.refresh(new_contract)
return new_contract
def update_one(session: Session, id: int, contract: models.ContractUpdate) -> models.ContractPublic:
statement = select(models.Contract).where(models.Contract.id == id)
result = session.exec(statement)
new_contract = result.first()
if not new_contract:
return None
contract_updates = contract.model_dump(exclude_unset=True)
for key, value in contract_updates.items():
setattr(new_contract, key, value)
session.add(new_contract)
session.commit()
session.refresh(new_contract)
return new_contract
def delete_one(session: Session, id: int) -> models.ContractPublic:
statement = select(models.Contract).where(models.Contract.id == id)
result = session.exec(statement)
contract = result.first()
if not contract:
return None
result = models.ContractPublic.model_validate(contract)
session.delete(contract)
session.commit()
return result

View File

@@ -1,106 +1,111 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>{{contract_name}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
@page {
<head>
<title>{{contract_name}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style type="text/css">
@page {
size: A4;
margin: 1cm;
@bottom-center {
content: "Page " counter(page);
content: "Page " counter(page);
}
@top-center {
content: "AMAP Croix Luizet - Contrat";
font-size: 10px;
color: #666;
content: "AMAP Croix Luizet - Contrat";
font-size: 10px;
color: #666;
}
}
table {
}
table {
border-collapse: collapse;
border: 2px solid rgb(140 140 140);
font-family: sans-serif;
font-size: 0.8rem;
letter-spacing: 1px;
}
h1, h2, h3, h4, h5 {
}
h1,
h2,
h3,
h4,
h5 {
page-break-after: avoid;
}
h1 {
}
h1 {
font-size: 22px;
margin-bottom: 5px;
}
h2 {
}
h2 {
font-size: 16px;
margin-bottom: 15px;
}
}
h3 {
h3 {
font-size: 15px;
margin-top: 30px;
margin-bottom: 10px;
}
}
h4 {
h4 {
font-size: 14px;
margin-top: 25px;
margin-bottom: 10px;
}
h5 {
}
h5 {
font-size: 13px;
margin-top: 20px;
margin-bottom: 5px;
}
p {
}
p {
margin: 8px 0;
text-align: justify;
}
table {
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
margin-bottom: 20px;
font-size: 11px;
}
}
th,
td {
th,
td {
border: 1px solid #999;
padding: 6px 8px;
vertical-align: top;
}
}
th {
th {
background-color: #f0f0f0;
text-align: left;
}
}
tbody tr:nth-child(even) {
tbody tr:nth-child(even) {
background-color: #f7f7f7;
}
table {
}
table {
page-break-inside: avoid;
}
}
tr {
tr {
page-break-inside: avoid;
}
.container {
}
.container {
width: 100%;
}
html, body {
}
html,
body {
font-family: sans-serif;
font-size: 12px;
line-height: 1.5;
color: #222;
}
body {
}
body {
margin: 0;
}
.signature {
}
.signature {
height: 80px;
}
.total-box {
}
.total-box {
display: flex;
flex-direction: column;
justify-content: space-between;
@@ -108,202 +113,269 @@
padding: 10px;
width: 200px;
margin-left: auto;
}
}
.total-label {
.total-label {
font-size: 18px;
font-weight: bold;
}
}
.total-price {
.total-price {
font-size: 18px;
font-weight: bold;
text-align: right;
}
</style>
</head>
<body>
<div class="container">
<h1>AMAP Croix Luizet</h1>
<h2>67 rue Octavie Villeurbanne - <a href="https://amapcroixluizet.eu">https://amapcroixluizet.eu</a></h2>
<h3>Contrat d'engagement solidaire</h3>
<h4>Informations contractuelles</h4>
}
</style>
</head>
<body>
<div class="container">
<p>Ce contrat est organisé par lAMAP CROIX-LUIZET et est régi par les statuts et le règlement intérieur de lAssociation.</p>
<h1>AMAP Croix Luizet</h1>
<h2>
67 rue Octavie Villeurbanne -
<a href="https://amapcroixluizet.eu">https://amapcroixluizet.eu</a>
</h2>
<h3>Contrat d'engagement solidaire</h3>
<h4>Informations contractuelles</h4>
<div class="container">
<p>
Ce contrat est organisé par lAMAP CROIX-LUIZET et est régi par les
statuts et le règlement intérieur de lAssociation.
</p>
<table>
<tbody>
<tr>
<th>Type de contrat</th>
<td>{{contract_type}}</td>
</tr>
<tr>
<th>Saison du contrat</th>
<td>{{contract_season}}</td>
</tr>
<tr>
<th>Type de contrat</th>
<td>{{contract_type}}</td>
</tr>
<tr>
<th>Référent·e</th>
<td>{{referer_name}}</td>
</tr>
<tr>
<th>Email référent·e</th>
<td>{{referer_email}}</td>
</tr>
<tr>
<th>Le/La producteur·trice</th>
<td>{{productor_name}}</td>
</tr>
<tr>
<th>Adresse du producteur·trice</th>
<td>{{productor_address}}</td>
</tr>
{% for method in productor_payment_methods %}
<tr>
<th>{{payment_methods_map[method.name]}}</th>
<td>{{method.details}}</td>
</tr>
{% endfor %}
<tr>
<th>Ladhérent·e</th>
<td>{{member_name}}</td>
</tr>
<tr>
<th>Email de ladhérent·e</th>
<td>{{member_email}}</td>
</tr>
<tr>
<th>Téléphone de l'adhérent·e</th>
<td>{{member_phone}}</td>
</tr>
</tbody>
<tbody>
<tr>
<th>Type de contrat</th>
<td>{{contract_type}}</td>
</tr>
<tr>
<th>Saison du contrat</th>
<td>{{contract_season}}</td>
</tr>
<tr>
<th>Type de contrat</th>
<td>{{contract_type}}</td>
</tr>
<tr>
<th>Référent·e</th>
<td>{{referer_name}}</td>
</tr>
<tr>
<th>Email référent·e</th>
<td>{{referer_email}}</td>
</tr>
<tr>
<th>Le/La producteur·trice</th>
<td>{{productor_name}}</td>
</tr>
<tr>
<th>Adresse du producteur·trice</th>
<td>{{productor_address}}</td>
</tr>
{% for method in productor_payment_methods %} {% if method.details
!= "" %}
<tr>
<th>{{payment_methods_map[method.name]}}</th>
<td>{{method.details}}</td>
</tr>
{% endif %} {% endfor %}
<tr>
<th>Ladhérent·e</th>
<td>{{member_name}}</td>
</tr>
<tr>
<th>Email de ladhérent·e</th>
<td>{{member_email}}</td>
</tr>
<tr>
<th>Téléphone de l'adhérent·e</th>
<td>{{member_phone}}</td>
</tr>
</tbody>
</table>
<p>
L'adhérent-e et le-la producteur-trice sengagent à respecter le présent contrat, les statuts et le Règlement Intérieur de «lAMAP CROIX LUIZET» et la charte des AMAP.
L'adhérent-e et le-la producteur-trice sengagent à respecter le
présent contrat, les statuts et le Règlement Intérieur de «lAMAP
CROIX LUIZET» et la charte des AMAP.
</p>
</div>
<div>
</div>
<div>
<h5>Engagement réciproque</h5>
<p>
Le/La producteur·trice sengage à fournir un panier <b>{{contract_type}}</b>, issu de son exploitation et de qualité en termes gustatifs. Il/Elle sengage à mener son exploitation dans un esprit de respect de la nature et de lenvironnement.
Le/La membre adhérent·e sengage à acheter 1 panier en acceptant les conséquences daléas climatiques ou autres évènements ayant un impact sur la qualité ou la quantité de produits dans le panier.
Le contrat commence le <b>{{contract_start_date}}</b> et termine le <b>{{contract_end_date}}</b>.
Le/La producteur·trice sengage à fournir un panier
<b>{{contract_type}}</b>, issu de son exploitation et de qualité en
termes gustatifs. Il/Elle sengage à mener son exploitation dans un
esprit de respect de la nature et de lenvironnement. Le/La membre
adhérent·e sengage à acheter 1 panier en acceptant les conséquences
daléas climatiques ou autres évènements ayant un impact sur la
qualité ou la quantité de produits dans le panier. Le contrat commence
le <b>{{contract_start_date}}</b> et termine le
<b>{{contract_end_date}}</b>.
</p>
</div>
<div>
</div>
<div>
<h5>
<b>Modalités de livraison</b>
<b>Modalités de livraison</b>
</h5>
<p>
Les livraisons sont effectuées exclusivement à la Maison du Citoyen, 67 rue Octavie 69100 VILLEURBANNE, les jeudis soir de 19h00 à 20h00. Toutefois en accord avec le producteur, et suivant les mesures sanitaires en vigueur, le Conseil dAdministration peut modifier exceptionnellement le lieu, le jour ou lhoraire de livraison.
Les livraisons sont effectuées exclusivement à la Maison du Citoyen,
67 rue Octavie 69100 VILLEURBANNE, les jeudis soir de 19h00 à 20h00.
Toutefois en accord avec le producteur, et suivant les mesures
sanitaires en vigueur, le Conseil dAdministration peut modifier
exceptionnellement le lieu, le jour ou lhoraire de livraison.
</p>
</div>
<div>
<h5>
En cas dimpossibilité
</h5>
</div>
<div>
<h5>En cas dimpossibilité</h5>
<ul>
<li>
Pour le/la producteur·trice dassurer une livraison, le Conseil dAdministration et le/la référent-e producteur·trice rechercheront, dans le respect des parties et de léthique de lAMAP une solution compensatrice.
</li>
<li>
Pour ladhérent·e de respecter le calendrier et de venir récupérer sa commande, les membres chargés de la distribution disposeront des paniers restants qui seront donnés à une association caritative ou distribués aux Amapien·ennes présent·es. Aucun panier ne sera remboursé.
</li>
<li>
Pour le/la producteur·trice dassurer une livraison, le Conseil
dAdministration et le/la référent-e producteur·trice rechercheront,
dans le respect des parties et de léthique de lAMAP une solution
compensatrice.
</li>
<li>
Pour ladhérent·e de respecter le calendrier et de venir récupérer
sa commande, les membres chargés de la distribution disposeront des
paniers restants qui seront donnés à une association caritative ou
distribués aux Amapien·ennes présent·es. Aucun panier ne sera
remboursé.
</li>
</ul>
</div>
<div>
</div>
<div>
<h5>Rupture du contrat</h5>
<p>
Ce contrat peut être interrompu unilatéralement par le/la membre adhérent, si et seulement si, un/une remplaçant·e est trouvé immédiatement, de sorte que le/la producteur·trice ne soit pas pénalisé financièrement. Ce contrat peut être rompu bilatéralement à tout moment. En cas de désaccord, cest au conseil dadministration de statuer.
Ce contrat peut être interrompu unilatéralement par le/la membre
adhérent, si et seulement si, un/une remplaçant·e est trouvé
immédiatement, de sorte que le/la producteur·trice ne soit pas
pénalisé financièrement. Ce contrat peut être rompu bilatéralement à
tout moment. En cas de désaccord, cest au conseil dadministration de
statuer.
</p>
</div>
{% if recurrent|length > 0 %}
<div class="container">
</div>
{% if recurrents|length > 0 %}
<div class="container">
<h4>Produits récurents (pour chaques livraisons)</h4>
<table>
<thead>
<tr>
<th>Nom du produit</th>
<th>Prix (€)</th>
<th>Prix (€/kg)</th>
<th>Poids</th>
<th>Quantité</th>
</tr>
</thead>
<tbody>
{% for rec in recurrent %}
<tr>
<td>{{rec.product.name}}</td>
<td>{{rec.product.price if rec.product.price else ""}}</td>
<td>{{rec.product.price_kg if rec.product.price_kg else ""}}</td>
<td>{{rec.product.quantity if rec.product.quantity != None else ""}} {{rec.product.quantity_unit if rec.product.quantity_unit != None else ""}}</td>
<td>{{rec.quantity}}{{"g" if rec.product.unit == "1" else "kg" if rec.product.unit == "2" else "p" }}</td>
</tr>
<thead>
<tr>
<th>Nom du produit</th>
<th>Prix (€)</th>
<th>Prix (€/kg)</th>
<th>Poids</th>
<th>Quantité</th>
</tr>
</thead>
<tbody>
{% for rec in recurrents %}
<tr>
<td>{{rec.product.name}}</td>
<td>{{rec.product.price if rec.product.price else ""}}</td>
<td>{{rec.product.price_kg if rec.product.price_kg else ""}}</td>
<td>
{{rec.product.quantity if rec.product.quantity != None else ""}}
{{rec.product.quantity_unit if rec.product.quantity_unit != None
else ""}}
</td>
<td>
{{rec.quantity}}{{"g" if rec.product.unit == "1" else "kg" if
rec.product.unit == "2" else "p" }}
</td>
</tr>
{% endfor %}
<tr>
<th scope="row" colspan="4">Total</th>
<td>{{recurrent_price}}€</td>
<th scope="row" colspan="4">Total</th>
<td>{{recurrent_price}}€</td>
</tr>
</tbody>
</tbody>
</table>
</div>
{% endif %}
{% if planned|length > 0 %}
<div class="container">
</div>
{% endif %} {% if planneds|length > 0 %}
<div class="container">
<h4>Produits planifiés (par livraison)</h4>
{% for plan in planned %}
{% for plan in planneds %}
<h5>{{plan.shipment.name}} {{plan.shipment.date}}</h5>
<table>
<thead>
<tr>
<th>Nom du produit</th>
<th>Prix (€)</th>
<th>Prix (€/kg)</th>
<th>Poids</th>
<th>Quantité</th>
</tr>
</thead>
<tbody>
{% for product in plan.products %}
<tr>
<td>{{product.product.name}}</td>
<td>{{product.product.price if product.product.price else ""}}</td>
<td>{{product.product.price_kg if product.product.price_kg else ""}}</td>
<td>{{product.product.quantity if product.product.quantity != None else ""}} {{product.product.quantity_unit if product.product.quantity_unit != None else ""}}</td>
<td>{{product.quantity}}{{"g" if product.product.unit == "1" else "kg" if product.product.unit == "2" else "p" }}</td>
</tr>
{% endfor%}
<tr>
<th scope="row" colspan="4">Total</th>
<td>{{plan.price}}€</td>
</tr>
</tbody>
<thead>
<tr>
<th>Nom du produit</th>
<th>Prix (€)</th>
<th>Prix (€/kg)</th>
<th>Poids</th>
<th>Quantité</th>
</tr>
</thead>
<tbody>
{% for product in plan.products %}
<tr>
<td>{{product.product.name}}</td>
<td>
{{product.product.price if product.product.price else ""}}
</td>
<td>
{{product.product.price_kg if product.product.price_kg else ""}}
</td>
<td>
{{product.product.quantity if product.product.quantity != None
else ""}} {{product.product.quantity_unit if
product.product.quantity_unit != None else ""}}
</td>
<td>
{{product.quantity}}{{"g" if product.product.unit == "1" else
"kg" if product.product.unit == "2" else "p" }}
</td>
</tr>
{% endfor%}
<tr>
<th scope="row" colspan="4">Total</th>
<td>{{plan.price}}€</td>
</tr>
</tbody>
</table>
{% endfor %}
</div>
{% endif %}
<div class="total-box">
</div>
{% endif %}
<div class="total-box">
<div class="total-label">Prix Total :</div>
<div class="total-price">{{total_price}}€</div>
</div>
<div class="container">
</div>
<h4>Paiement par {{contract_payment_method}}</h4>
{% if contract_payment_method == "chèque" %}
<div class="container">
<table>
<thead>
<tr>
<th>Signature producteur-trice</th>
<th>Signature adhérent-e</th>
</tr>
</thead>
<tbody>
<tr>
<td class="signature"></td>
<td class="signature"></td>
</tr>
</tbody>
<thead>
<tr>
{% for cheque in cheques %}
<th>Cheque n°{{cheque.name}}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for cheque in cheques %}
<td>{{cheque.value}}€</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
{% endif %}
<div class="container">
<table>
<thead>
<tr>
<th>Signature producteur-trice</th>
<th>Signature adhérent-e</th>
</tr>
</thead>
<tbody>
<tr>
<td class="signature"></td>
<td class="signature"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
</body>
</html>

View File

@@ -140,6 +140,10 @@ class Form(FormBase, table=True):
"order_by": "Shipment.name"
},
)
contracts: list["Contract"] = Relationship(
back_populates="form",
cascade_delete=True
)
class FormUpdate(SQLModel):
name: str | None
@@ -168,20 +172,92 @@ class TemplateUpdate(SQLModel):
class TemplateCreate(TemplateBase):
pass
class ChequeBase(SQLModel):
name: str
value: str
class Cheque(ChequeBase, table=True):
id: int | None = Field(default=None, primary_key=True)
contract_id: int = Field(foreign_key="contract.id", ondelete="CASCADE")
contract: Optional["Contract"] = Relationship(
back_populates="cheques",
)
class ContractBase(SQLModel):
firstname: str
lastname: str
email: str
phone: str
payment_method: str
cheque_quantity: int
class Contract(ContractBase, table=True):
id: int | None = Field(default=None, primary_key=True)
form_id: int = Field(
foreign_key="form.id",
nullable=False,
ondelete="CASCADE"
)
products: list["ContractProduct"] = Relationship(
back_populates="contract",
cascade_delete=True
)
form: Optional[Form] = Relationship(back_populates="contracts")
cheques: list[Cheque] = Relationship(
back_populates="contract",
cascade_delete=True
)
class ContractCreate(ContractBase):
products: list["ContractProductCreate"] = []
cheques: list["Cheque"] = []
form_id: int
contract: dict
class ContractPublic(ContractBase):
id: int
# class Contract(ContractBase, table=True):
# id: int | None = Field(default=None, primary_key=True)
class ContractUpdate(SQLModel):
pass
class ContractCreate(ContractBase):
class ContractPublic(ContractBase):
id: int
products: list["ContractProduct"] = []
form: Form
class ContractProductBase(SQLModel):
product_id: int = Field(
foreign_key="product.id",
nullable=False,
ondelete="CASCADE"
)
shipment_id: int | None = Field(
default=None,
foreign_key="shipment.id",
nullable=True,
ondelete="CASCADE"
)
quantity: float
class ContractProduct(ContractProductBase, table=True):
id: int | None = Field(default=None, primary_key=True)
contract_id: int = Field(
foreign_key="contract.id",
nullable=False,
ondelete="CASCADE"
)
contract: Optional["Contract"] = Relationship(back_populates="products")
product: Optional["Product"] = Relationship()
shipment: Optional["Shipment"] = Relationship()
class ContractProductPublic(ContractProductBase):
id: int
quantity: float
contract: Contract
product: Product
shipment: Optional["Shipment"]
class ContractProductCreate(ContractProductBase):
pass
class ContractProductUpdate(ContractProductBase):
pass
class ShipmentBase(SQLModel):