add contract storage fix various bugs and translations
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
65
backend/src/contracts/service.py
Normal file
65
backend/src/contracts/service.py
Normal 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
|
||||
@@ -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 l’AMAP CROIX-LUIZET et est régi par les statuts et le règlement intérieur de l’Association.</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 l’AMAP CROIX-LUIZET et est régi par les
|
||||
statuts et le règlement intérieur de l’Association.
|
||||
</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>L’adhérent·e</th>
|
||||
<td>{{member_name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email de l’adhé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>L’adhérent·e</th>
|
||||
<td>{{member_name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email de l’adhé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 s’engagent à respecter le présent contrat, les statuts et le Règlement Intérieur de «l’AMAP CROIX LUIZET» et la charte des AMAP.
|
||||
L'adhérent-e et le-la producteur-trice s’engagent à respecter le
|
||||
présent contrat, les statuts et le Règlement Intérieur de «l’AMAP
|
||||
CROIX LUIZET» et la charte des AMAP.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
<div>
|
||||
<h5>Engagement réciproque</h5>
|
||||
<p>
|
||||
Le/La producteur·trice s’engage à fournir un panier <b>{{contract_type}}</b>, issu de son exploitation et de qualité en termes gustatifs. Il/Elle s’engage à mener son exploitation dans un esprit de respect de la nature et de l’environnement.
|
||||
Le/La membre adhérent·e s’engage à acheter 1 panier en acceptant les conséquences d’alé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 s’engage à fournir un panier
|
||||
<b>{{contract_type}}</b>, issu de son exploitation et de qualité en
|
||||
termes gustatifs. Il/Elle s’engage à mener son exploitation dans un
|
||||
esprit de respect de la nature et de l’environnement. Le/La membre
|
||||
adhérent·e s’engage à acheter 1 panier en acceptant les conséquences
|
||||
d’alé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 d’Administration peut modifier exceptionnellement le lieu, le jour ou l’horaire 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 d’Administration peut modifier
|
||||
exceptionnellement le lieu, le jour ou l’horaire de livraison.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5>
|
||||
En cas d’impossibilité
|
||||
</h5>
|
||||
</div>
|
||||
<div>
|
||||
<h5>En cas d’impossibilité</h5>
|
||||
<ul>
|
||||
<li>
|
||||
Pour le/la producteur·trice d’assurer une livraison, le Conseil d’Administration et le/la référent-e producteur·trice rechercheront, dans le respect des parties et de l’éthique de l’AMAP une solution compensatrice.
|
||||
</li>
|
||||
<li>
|
||||
Pour l’adhé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 d’assurer une livraison, le Conseil
|
||||
d’Administration et le/la référent-e producteur·trice rechercheront,
|
||||
dans le respect des parties et de l’éthique de l’AMAP une solution
|
||||
compensatrice.
|
||||
</li>
|
||||
<li>
|
||||
Pour l’adhé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, c’est au conseil d’administration 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, c’est au conseil d’administration 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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user