[WIP] add styles

This commit is contained in:
Julien Aldon
2026-03-03 17:58:33 +01:00
125 changed files with 5762 additions and 622 deletions

View File

@@ -1,3 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: MIT

View File

@@ -21,10 +21,9 @@ router = APIRouter(prefix='/auth')
jwk_client = PyJWKClient(JWKS_URL)
security = HTTPBearer()
@router.get('/logout')
def logout(
refresh_token: Annotated[str | None, Cookie()] = None,
):
def logout():
params = {
'client_id': settings.keycloak_client_id,
'post_logout_redirect_uri': settings.origins,
@@ -34,26 +33,20 @@ def logout(
key='access_token',
path='/',
secure=not settings.debug,
samesite='lax',
samesite='strict',
)
response.delete_cookie(
key='refresh_token',
path='/',
secure=not settings.debug,
samesite='lax',
samesite='strict',
)
response.delete_cookie(
key='id_token',
path='/',
secure=not settings.debug,
samesite='lax',
samesite='strict',
)
# if refresh_token:
# requests.post(LOGOUT_URL, data={
# 'client_id': settings.keycloak_client_id,
# 'client_secret': settings.keycloak_client_secret,
# 'refresh_token': refresh_token
# })
return response
@@ -67,9 +60,11 @@ def login():
'redirect_uri': settings.keycloak_redirect_uri,
'state': state,
}
request_url = requests.Request('GET', AUTH_URL, params=params).prepare().url
request_url = requests.Request(
'GET', AUTH_URL, params=params).prepare().url
return RedirectResponse(request_url)
@router.get('/callback')
def callback(code: str, session: Session = Depends(get_session)):
data = {
@@ -85,15 +80,17 @@ def callback(code: str, session: Session = Depends(get_session)):
response = requests.post(TOKEN_URL, data=data, headers=headers)
if response.status_code != 200:
raise HTTPException(
status_code=400,
detail=messages.failtogettoken
status_code=404,
detail=messages.Messages.not_found('token')
)
token_data = response.json()
id_token = token_data['id_token']
decoded_token = jwt.decode(id_token, options={'verify_signature': False})
decoded_access_token = jwt.decode(token_data['access_token'], options={'verify_signature': False})
decoded_access_token = jwt.decode(
token_data['access_token'], options={
'verify_signature': False})
resource_access = decoded_access_token.get('resource_access')
if not resource_access:
data = {
@@ -117,7 +114,7 @@ def callback(code: str, session: Session = Depends(get_session)):
user_create = UserCreate(
email=decoded_token.get('email'),
name=decoded_token.get('preferred_username'),
name=decoded_token.get('name'),
role_names=roles['roles']
)
service.get_or_create_user(session, user_create)
@@ -127,7 +124,7 @@ def callback(code: str, session: Session = Depends(get_session)):
value=token_data['access_token'],
httponly=True,
secure=not settings.debug,
samesite='lax',
samesite='strict',
max_age=settings.max_age
)
response.set_cookie(
@@ -135,7 +132,7 @@ def callback(code: str, session: Session = Depends(get_session)):
value=token_data['refresh_token'] or '',
httponly=True,
secure=not settings.debug,
samesite='lax',
samesite='strict',
max_age=30 * 24 * settings.max_age
)
response.set_cookie(
@@ -143,47 +140,57 @@ def callback(code: str, session: Session = Depends(get_session)):
value=token_data['id_token'],
httponly=True,
secure=not settings.debug,
samesite='lax',
samesite='strict',
max_age=settings.max_age
)
return response
def verify_token(token: str):
try:
signing_key = jwk_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(token, options={'verify_signature': False})
payload = jwt.decode(
decoded = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
audience=settings.keycloak_client_id,
issuer=ISSUER,
leeway=60,
)
return payload
return decoded
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail=messages.tokenexipired)
raise HTTPException(status_code=401,
detail=messages.Messages.tokenexipired)
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail=messages.invalidtoken)
raise HTTPException(
status_code=401,
detail=messages.Messages.invalidtoken)
def get_current_user(request: Request, session: Session = Depends(get_session)):
def get_current_user(
request: Request,
session: Session = Depends(get_session)):
access_token = request.cookies.get('access_token')
if not access_token:
raise HTTPException(status_code=401, detail=messages.notauthenticated)
raise HTTPException(status_code=401,
detail=messages.Messages.notauthenticated)
payload = verify_token(access_token)
if not payload:
raise HTTPException(status_code=401, detail='aze')
email = payload.get('email')
if not email:
raise HTTPException(status_code=401, detail=messages.notauthenticated)
raise HTTPException(status_code=401,
detail=messages.Messages.notauthenticated)
user = session.exec(select(User).where(User.email == email)).first()
if not user:
raise HTTPException(status_code=401, detail=messages.usernotfound)
raise HTTPException(status_code=401,
detail=messages.Messages.not_found('user'))
return user
@router.post('/refresh')
def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
refresh = refresh_token
@@ -199,8 +206,8 @@ def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
result = requests.post(TOKEN_URL, data=data, headers=headers)
if result.status_code != 200:
raise HTTPException(
status_code=400,
detail=messages.failtogettoken
status_code=404,
detail=messages.Messages.not_found('token')
)
token_data = result.json()
@@ -210,7 +217,7 @@ def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
value=token_data['access_token'],
httponly=True,
secure=True if settings.debug == False else True,
samesite='lax',
samesite='strict',
max_age=settings.max_age
)
response.set_cookie(
@@ -218,11 +225,20 @@ def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None):
value=token_data['refresh_token'] or '',
httponly=True,
secure=True if settings.debug == False else True,
samesite='lax',
max_age=4
samesite='strict',
max_age=30 * 24 * settings.max_age
)
response.set_cookie(
key='id_token',
value=token_data['id_token'],
httponly=True,
secure=not settings.debug,
samesite='strict',
max_age=settings.max_age
)
return response
@router.get('/user/me')
def me(user: UserPublic = Depends(get_current_user)):
if not user:
@@ -235,4 +251,4 @@ def me(user: UserPublic = Depends(get_current_user)):
'id': user.id,
'roles': [role.name for role in user.roles]
}
}
}

View File

@@ -1,18 +1,27 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from src.database import get_session
from sqlmodel import Session
from src.contracts.generate_contract import generate_html_contract, generate_recap
from src.auth.auth import get_current_user
import src.models as models
import src.messages as messages
import src.contracts.service as service
import src.forms.service as form_service
"""Router for contract resource"""
import io
import zipfile
import src.contracts.service as service
import src.forms.service as form_service
import src.messages as messages
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.contracts.generate_contract import (generate_html_contract,
generate_recap)
from src.database import get_session
router = APIRouter(prefix='/contracts')
def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
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']
@@ -20,30 +29,50 @@ def compute_recurrent_prices(products_quantities: list[dict], nb_shipment: int):
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):
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_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',
result,
'shipment',
contract_product.shipment.id
)
if existing_id < 0:
@@ -69,71 +98,183 @@ def create_occasional_dict(contract_products: list[models.ContractProduct]):
)
return result
@router.post('/')
@router.post('')
async def create_contract(
contract: models.ContractCreate,
session: Session = Depends(get_session),
):
"""Create contract route"""
new_contract = service.create_one(session, contract)
occasional_contract_products = list(filter(lambda contract_product: contract_product.product.type == models.ProductType.OCCASIONAL, new_contract.products))
occasional_contract_products = list(
filter(
lambda contract_product: (
contract_product.product.type == models.ProductType.OCCASIONAL
),
new_contract.products
)
)
occasionals = create_occasional_dict(occasional_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))
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)
)
price = recurrent_price + compute_occasional_prices(occasionals)
total_price = '{:10.2f}'.format(price)
cheques = list(map(lambda x: {"name": x.name, "value": x.value}, new_contract.cheques))
# TODO: send contract to referer
cheques = list(
map(
lambda x: {'name': x.name, 'value': x.value},
new_contract.cheques
)
)
try:
pdf_bytes = generate_html_contract(
new_contract,
cheques,
occasionals,
recurrents,
recurrent_price,
total_price
'{:10.2f}'.format(recurrent_price),
'{:10.2f}'.format(price)
)
pdf_file = io.BytesIO(pdf_bytes)
contract_id = f'{new_contract.firstname}_{new_contract.lastname}_{new_contract.form.productor.type}_{new_contract.form.season}'
contract_id = (
f'{new_contract.firstname}_'
f'{new_contract.lastname}_'
f'{new_contract.form.productor.type}_'
f'{new_contract.form.season}'
)
service.add_contract_file(session, new_contract.id, pdf_bytes, price)
except Exception as e:
print(e)
raise HTTPException(status_code=400, detail=messages.pdferror)
except Exception as error:
raise HTTPException(
status_code=400,
detail=messages.pdferror
) from error
return StreamingResponse(
pdf_file,
media_type='application/pdf',
headers={
'Content-Disposition': f'attachment; filename=contract_{contract_id}.pdf'
'Content-Disposition': (
f'attachment; filename=contract_{contract_id}.pdf'
)
}
)
@router.get('/', response_model=list[models.ContractPublic])
@router.get('/{form_id}/base')
async def get_base_contract_template(
form_id: int,
session: Session = Depends(get_session),
):
"""Get contract template route"""
form = form_service.get_one(session, form_id)
recurrents = [
{'product': product, 'quantity': None}
for product in form.productor.products
if product.type == models.ProductType.RECCURENT
]
occasionals = [{
'shipment': sh,
'price': None,
'products': [{'product': pr, 'quantity': None} for pr in sh.products]
} for sh in form.shipments]
empty_contract = models.ContractPublic(
firstname='',
form=form,
lastname='',
email='',
phone='',
products=[],
payment_method='cheque',
cheque_quantity=3,
total_price=0,
id=1
)
cheques = [
{'name': None, 'value': None},
{'name': None, 'value': None},
{'name': None, 'value': None}
]
try:
pdf_bytes = generate_html_contract(
empty_contract,
cheques,
occasionals,
recurrents,
)
pdf_file = io.BytesIO(pdf_bytes)
contract_id = (
f'{empty_contract.form.productor.type}_'
f'{empty_contract.form.season}'
)
except Exception as error:
raise HTTPException(
status_code=400,
detail=messages.pdferror
) from error
return StreamingResponse(
pdf_file,
media_type='application/pdf',
headers={
'Content-Disposition': (
f'attachment; filename=contract_{contract_id}.pdf'
)
}
)
@router.get('', response_model=list[models.ContractPublic])
def get_contracts(
forms: list[str] = Query([]),
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Get all contracts route"""
return service.get_all(session, user, forms)
@router.get('/{id}/file')
@router.get('/{_id}/file')
def get_contract_file(
id: int,
_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
if not service.is_allowed(session, user, id):
raise HTTPException(status_code=403, detail=messages.notallowed)
contract = service.get_one(session, id)
"""Get a contract file (in pdf) route"""
if not service.is_allowed(session, user, _id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contract', 'get')
)
contract = service.get_one(session, _id)
if contract is None:
raise HTTPException(status_code=404, detail=messages.notfound)
filename = f'{contract.form.name.replace(' ', '_')}_{contract.form.season}_{contract.firstname}-{contract.lastname}'
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('contract')
)
filename = (
f'{contract.form.name.replace(' ', '_')}_'
f'{contract.form.season}_'
f'{contract.firstname}_'
f'{contract.lastname}'
)
return StreamingResponse(
io.BytesIO(contract.file),
media_type='application/pdf',
headers={
'Content-Disposition': f'attachment; filename={filename}.pdf'
}
)
)
@router.get('/{form_id}/files')
def get_contract_files(
@@ -141,17 +282,30 @@ def get_contract_files(
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Get all contract files for a given form"""
if not form_service.is_allowed(session, user, form_id):
raise HTTPException(status_code=403, detail=messages.notallowed)
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contracts', 'get')
)
form = form_service.get_one(session, form_id=form_id)
contracts = service.get_all(session, user, forms=[form.name])
zipped_contracts = io.BytesIO()
with zipfile.ZipFile(zipped_contracts, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
with zipfile.ZipFile(
zipped_contracts,
'a',
zipfile.ZIP_DEFLATED,
False
) as zip_file:
for contract in contracts:
contract_filename = f'{contract.form.name.replace(' ', '_')}_{contract.form.season}_{contract.firstname}-{contract.lastname}.pdf'
contract_filename = (
f'{contract.form.name.replace(' ', '_')}_'
f'{contract.form.season}_'
f'{contract.firstname}_'
f'{contract.lastname}'
)
zip_file.writestr(contract_filename, contract.file)
filename = f'{form.name.replace(" ", "_")}_{form.season}'
filename = f'{form.name.replace(' ', '_')}_{form.season}'
return StreamingResponse(
io.BytesIO(zipped_contracts.getvalue()),
media_type='application/zip',
@@ -160,39 +314,69 @@ def get_contract_files(
}
)
@router.get('/{form_id}/recap')
def get_contract_recap(
form_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Get a contract recap for a given form"""
if not form_service.is_allowed(session, user, form_id):
raise HTTPException(status_code=403, detail=messages.notallowed)
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contract recap', 'get')
)
form = form_service.get_one(session, form_id=form_id)
contracts = service.get_all(session, user, forms=[form.name])
return StreamingResponse(
io.BytesIO(generate_recap(contracts, form)),
media_type='application/zip',
headers={
'Content-Disposition': f'attachment; filename=filename.ods'
'Content-Disposition': (
'attachment; filename=filename.ods'
)
}
)
@router.get('/{id}', response_model=models.ContractPublic)
def get_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
if not service.is_allowed(session, user, id):
raise HTTPException(status_code=403, detail=messages.notallowed)
result = service.get_one(session, id)
@router.get('/{_id}', response_model=models.ContractPublic)
def get_contract(
_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Get a contract route"""
if not service.is_allowed(session, user, _id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contract', 'get')
)
result = service.get_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('contract')
)
return result
@router.delete('/{id}', response_model=models.ContractPublic)
def delete_contract(id: int, session: Session = Depends(get_session), user: models.User = Depends(get_current_user)):
if not service.is_allowed(session, user, id):
raise HTTPException(status_code=403, detail=messages.notallowed)
result = service.delete_one(session, id)
@router.delete('/{_id}', response_model=models.ContractPublic)
def delete_contract(
_id: int,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
"""Delete contract route"""
if not service.is_allowed(session, user, _id):
raise HTTPException(
status_code=403,
detail=messages.Messages.not_allowed('contract', 'delete')
)
result = service.delete_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('contract')
)
return result

View File

@@ -1,21 +1,28 @@
import html
import io
import pathlib
import jinja2
import src.models as models
import html
import odfdo
# from odfdo import Cell, Document, Row, Style, Table
from odfdo.element import Element
from src import models
from weasyprint import HTML
import io
def generate_html_contract(
contract: models.Contract,
cheques: list[dict],
occasionals: list[dict],
reccurents: list[dict],
recurrent_price: float,
total_price: float
):
template_dir = "./src/contracts/templates"
recurrent_price: float | None = None,
total_price: float | None = None
):
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_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(
@@ -26,95 +33,212 @@ def generate_html_contract(
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"},
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),
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
)
options = {
'page-size': 'Letter',
'margin-top': '0.5in',
'margin-right': '0.5in',
'margin-bottom': '0.5in',
'margin-left': '0.5in',
'encoding': "UTF-8",
'print-media-type': True,
"disable-javascript": True,
"disable-external-links": True,
'enable-local-file-access': False,
"disable-local-file-access": True,
"no-images": True,
}
contract_payment_method={
"cheque": "chèque",
"transfer": "virements"}[
contract.payment_method],
cheques=cheques)
return HTML(
string=output_text,
base_url=template_dir
base_url=template_dir,
).write_pdf()
def flatten(xss):
return [x for xs in xss for x in xs]
from odfdo import Document, Table, Row, Cell
from odfdo.element import Element
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_center_cell_style(name: str = "centered-cell") -> odfdo.Style:
return odfdo.Element.from_tag(
f'<style:style style:name="{name}" style:family="table-cell">'
'<style:table-cell-properties style:vertical-align="middle" fo:wrap-option="wrap"/>'
'<style:paragraph-properties fo:text-align="center"/>'
'</style:style>'
)
def create_cell_style_with_font(name: str = "font", font_size="14pt", bold: bool = False) -> odfdo.Style:
return odfdo.Element.from_tag(
f'<style:style style:name="{name}" style:family="table-cell" '
f'xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0">'
'<style:table-cell-properties style:vertical-align="middle" fo:wrap-option="wrap"/>'
f'<style:paragraph-properties fo:text-align="center" fo:font-size="{font_size}" '
f'{"fo:font-weight=\"bold\"" if bold else ""}/>'
'</style:style>'
)
def apply_center_cell_style(document: odfdo.Document, row: odfdo.Row):
style = document.insert_style(
create_center_cell_style()
)
for cell in row.get_cells():
cell.style = style
def apply_column_height_style(document: odfdo.Document, row: odfdo.Row, height: str):
style = document.insert_style(
style=create_row_style_height(height), name=height, automatic=True
)
row.style = style
def apply_font_style(document: odfdo.Document, table: odfdo.Table, size: str = "14pt"):
style_header = document.insert_style(
style=create_cell_style_with_font(
'header_font', font_size=size, bold=True
)
)
style_body = document.insert_style(
style=create_cell_style_with_font(
'body_font', font_size=size, bold=False
)
)
for position in range(table.height):
row = table.get_row(position)
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
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_recap(
contracts: list[models.Contract],
form: models.Form,
):
print(form.productor.products)
recurrents = [pr.name for pr in form.productor.products if pr.type == models.ProductType.RECCURENT]
recurrents = [pr.name 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 = [pr.name 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([[shipment.name] + ["" * len(occasionnals)] for shipment in 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"
}
header = (
["Nom", "Email"] +
["Tarif panier", "Total Paniers", "Total à payer"] +
["Cheque 1", "Cheque 2", "Cheque 3"] +
[f"Total {len(shipments)} livraisons + produits occasionnels"] +
recurrents +
occasionnals_header +
["Remarques", "Nom"]
)
data = [
["", ""] + ["" * len(recurrents)] + shipment_header,
["nom", "email"] + recurrents + occasionnals_header + ["remarques", "name"],
[""] * (9 + len(recurrents)) + shipment_header,
header,
*[
[
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],
"",
f'{contract.firstname} {contract.lastname}',
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],
"",
f'{contract.firstname} {contract.lastname}',
] for contract in contracts
]
]
doc = Document("spreadsheet")
sheet = Table(name="Recap")
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)
for _ in enumerate(shipments):
startcol = index
endcol = index+len(occasionnals) - 1
sheet.set_span((startcol, 0, endcol, 0), merge=True)
index += len(occasionnals)
offset = 0
index = 2 + len(recurrents)
for i in range(len(shipments)):
index = index + offset
print(index, index+len(occasionnals) - 1)
sheet.set_span((index, 0, index+len(occasionnals) - 1, 0), merge=True)
offset += len(occasionnals)
doc.body.append(sheet)
buffer = io.BytesIO()
doc.save(buffer)
doc.save('test.ods')
return buffer.getvalue()

View File

@@ -1,28 +1,57 @@
"""Contract service responsible for read, create, update and delete contracts"""
from sqlalchemy.orm import selectinload
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
user: models.User,
forms: list[str] = [],
forms: list[str] | None = None,
form_id: int | None = None,
) -> list[models.ContractPublic]:
statement = select(models.Contract)\
.join(models.Form, models.Contract.form_id == models.Form.id)\
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
.where(models.Productor.type.in_([r.name for r in user.roles]))\
"""Get all contracts"""
statement = (
select(models.Contract)
.join(
models.Form,
models.Contract.form_id == models.Form.id
)
.join(
models.Productor,
models.Form.productor_id == models.Productor.id
)
.where(
models.Productor.type.in_(
[r.name for r in user.roles]
)
)
.distinct()
if len(forms) > 0:
)
if forms:
statement = statement.where(models.Form.name.in_(forms))
if form_id:
statement = statement.where(models.Form.id == form_id)
return session.exec(statement.order_by(models.Contract.id)).all()
def get_one(session: Session, contract_id: int) -> models.ContractPublic:
def get_one(
session: Session,
contract_id: int
) -> models.ContractPublic:
"""Get one contract"""
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"])
def create_one(
session: Session,
contract: models.ContractCreate
) -> models.ContractPublic:
"""Create one contract"""
contract_create = contract.model_dump(
exclude_unset=True,
exclude=["products", "cheques"]
)
new_contract = models.Contract(**contract_create)
new_contract.cheques = [
@@ -45,10 +74,27 @@ def create_one(session: Session, contract: models.ContractCreate) -> models.Cont
session.add(new_contract)
session.commit()
session.refresh(new_contract)
return new_contract
def add_contract_file(session: Session, id: int, file: bytes, price: float):
statement = select(models.Contract).where(models.Contract.id == id)
statement = (
select(models.Contract)
.where(models.Contract.id == new_contract.id)
.options(
selectinload(models.Contract.form)
.selectinload(models.Form.productor)
)
)
return session.exec(statement).one()
def add_contract_file(
session: Session,
_id: int,
file: bytes,
price: float
):
"""Add a file to an existing contract"""
statement = select(models.Contract).where(models.Contract.id == _id)
result = session.exec(statement)
contract = result.first()
contract.total_price = price
@@ -58,8 +104,14 @@ def add_contract_file(session: Session, id: int, file: bytes, price: float):
session.refresh(contract)
return contract
def update_one(session: Session, id: int, contract: models.ContractUpdate) -> models.ContractPublic:
statement = select(models.Contract).where(models.Contract.id == id)
def update_one(
session: Session,
_id: int,
contract: models.ContractUpdate
) -> models.ContractPublic:
"""Update one contract"""
statement = select(models.Contract).where(models.Contract.id == _id)
result = session.exec(statement)
new_contract = result.first()
if not new_contract:
@@ -72,8 +124,13 @@ def update_one(session: Session, id: int, contract: models.ContractUpdate) -> mo
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)
def delete_one(
session: Session,
_id: int
) -> models.ContractPublic:
"""Delete one contract"""
statement = select(models.Contract).where(models.Contract.id == _id)
result = session.exec(statement)
contract = result.first()
if not contract:
@@ -83,11 +140,29 @@ def delete_one(session: Session, id: int) -> models.ContractPublic:
session.commit()
return result
def is_allowed(session: Session, user: models.User, id: int) -> bool:
statement = select(models.Contract)\
.join(models.Form, models.Contract.form_id == models.Form.id)\
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
.where(models.Contract.id == id)\
.where(models.Productor.type.in_([r.name for r in user.roles]))\
def is_allowed(
session: Session,
user: models.User,
_id: int
) -> bool:
"""Determine if a user is allowed to access a contract by id"""
statement = (
select(models.Contract)
.join(
models.Form,
models.Contract.form_id == models.Form.id
)
.join(
models.Productor,
models.Form.productor_id == models.Productor.id
)
.where(models.Contract.id == _id)
.where(
models.Productor.type.in_(
[r.name for r in user.roles]
)
)
.distinct()
return len(session.exec(statement).all()) > 0
)
return len(session.exec(statement).all()) > 0

View File

@@ -151,10 +151,6 @@
<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>
@@ -278,14 +274,14 @@
else ""}}
</td>
<td>
{{rec.quantity}}{{"g" if rec.product.unit == "1" else "kg" if
{{rec.quantity if rec.quantity != None else ""}}{{"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>
<td>{{recurrent_price if recurrent_price else ""}}€</td>
</tr>
</tbody>
</table>
@@ -321,14 +317,15 @@
product.product.quantity_unit != None else ""}}
</td>
<td>
{{product.quantity}}{{"g" if product.product.unit == "1" else
{{product.quantity if product.quantity != None
else ""}}{{"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>{{occasional.price}}€</td>
<td>{{occasional.price if occasional.price else ""}}€</td>
</tr>
</tbody>
</table>
@@ -337,7 +334,7 @@
{% endif %}
<div class="total-box">
<div class="total-label">Prix Total :</div>
<div class="total-price">{{total_price}}€</div>
<div class="total-price">{{total_price if total_price else ""}}€</div>
</div>
<h4>Paiement par {{contract_payment_method}}</h4>
{% if contract_payment_method == "chèque" %}
@@ -346,14 +343,14 @@
<thead>
<tr>
{% for cheque in cheques %}
<th>Cheque n°{{cheque.name}}</th>
<th>Cheque n°{{cheque.name if cheque.name else ""}}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for cheque in cheques %}
<td>{{cheque.value}}€</td>
<td>{{cheque.value if cheque.value else ""}}€</td>
{% endfor %}
</tr>
</tbody>

View File

@@ -1,11 +1,14 @@
from sqlmodel import create_engine, SQLModel, Session
from sqlmodel import Session, SQLModel, create_engine
from src.settings import settings
engine = create_engine(f'postgresql://{settings.db_user}:{settings.db_pass}@{settings.db_host}:54321/{settings.db_name}')
engine = create_engine(
f'postgresql://{settings.db_user}:{settings.db_pass}@{settings.db_host}:5432/{settings.db_name}')
def get_session():
with Session(engine) as session:
yield session
def create_all_tables():
SQLModel.metadata.create_all(engine)

View File

@@ -0,0 +1,26 @@
"""Forms module exceptions"""
import logging
class FormServiceError(Exception):
"""Form service exception"""
def __init__(self, message: str):
super().__init__(message)
logging.error('FormService : %s', message)
class UserNotFoundError(FormServiceError):
pass
class ProductorNotFoundError(FormServiceError):
pass
class FormNotFoundError(FormServiceError):
pass
class FormCreateError(FormServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,55 +1,89 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.forms.exceptions as exceptions
import src.forms.service as service
import src.messages as messages
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/forms')
@router.get('/', response_model=list[models.FormPublic])
@router.get('', response_model=list[models.FormPublic])
async def get_forms(
seasons: list[str] = Query([]),
productors: list[str] = Query([]),
current_season: bool = False,
session: Session = Depends(get_session)
session: Session = Depends(get_session),
):
return service.get_all(session, seasons, productors, current_season)
@router.get('/{id}', response_model=models.FormPublic)
async def get_form(id: int, session: Session = Depends(get_session)):
result = service.get_one(session, id)
@router.get('/referents', response_model=list[models.FormPublic])
async def get_forms_filtered(
seasons: list[str] = Query([]),
productors: list[str] = Query([]),
current_season: bool = False,
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user)
):
return service.get_all(session, seasons, productors, current_season, user)
@router.get('/{_id}', response_model=models.FormPublic)
async def get_form(_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)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('form')
)
return result
@router.post('/', response_model=models.FormPublic)
@router.post('', response_model=models.FormPublic)
async def create_form(
form: models.FormCreate,
form: models.FormCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, form)
try:
form = service.create_one(session, form)
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except exceptions.UserNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except exceptions.FormCreateError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return form
@router.put('/{id}', response_model=models.FormPublic)
@router.put('/{_id}', response_model=models.FormPublic)
async def update_form(
id: int, form: models.FormUpdate,
_id: int, form: models.FormUpdate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, form)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.update_one(session, _id, form)
except exceptions.FormNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
except exceptions.UserNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result
@router.delete('/{id}', response_model=models.FormPublic)
@router.delete('/{_id}', response_model=models.FormPublic)
async def delete_form(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.delete_one(session, _id)
except exceptions.FormNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result

View File

@@ -1,41 +1,69 @@
from sqlmodel import Session, select
import src.models as models
import src.forms.exceptions as exceptions
import src.messages as messages
from sqlalchemy import func
from sqlmodel import Session, select
from src import models
def get_all(
session: Session,
session: Session,
seasons: list[str],
productors: list[str],
current_season: bool,
user: models.User = None
) -> list[models.FormPublic]:
statement = select(models.Form)
if user:
statement = statement .join(
models.Productor,
models.Form.productor_id == models.Productor.id) .where(
models.Productor.type.in_(
[
r.name for r in user.roles])) .distinct()
if len(seasons) > 0:
statement = statement.where(models.Form.season.in_(seasons))
if len(productors) > 0:
statement = statement.join(models.Productor).where(models.Productor.name.in_(productors))
statement = statement.join(
models.Productor).where(
models.Productor.name.in_(productors))
if not user:
statement = statement.where(models.Form.visible)
if current_season:
subquery = (
select(
models.Productor.type,
models.Productor.type,
func.max(models.Form.start).label("max_start")
)
.join(models.Form)\
.group_by(models.Productor.type)\
.join(models.Form)
.group_by(models.Productor.type)
.subquery()
)
statement = select(models.Form)\
.join(models.Productor)\
.join(subquery,
(models.Productor.type == subquery.c.type) &
(models.Form.start == subquery.c.max_start)
)
.join(subquery,
(models.Productor.type == subquery.c.type) &
(models.Form.start == subquery.c.max_start)
)
if not user:
statement = statement.where(models.Form.visible)
return session.exec(statement.order_by(models.Form.name)).all()
return session.exec(statement.order_by(models.Form.name)).all()
def get_one(session: Session, form_id: int) -> models.FormPublic:
return session.get(models.Form, form_id)
def create_one(session: Session, form: models.FormCreate) -> models.FormPublic:
if not form:
raise exceptions.FormCreateError(
messages.Messages.invalid_input(
'form', 'input cannot be None'))
if not session.get(models.Productor, form.productor_id):
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
if not session.get(models.User, form.referer_id):
raise exceptions.UserNotFoundError(messages.Messages.not_found('user'))
form_create = form.model_dump(exclude_unset=True)
new_form = models.Form(**form_create)
session.add(new_form)
@@ -43,12 +71,22 @@ def create_one(session: Session, form: models.FormCreate) -> models.FormPublic:
session.refresh(new_form)
return new_form
def update_one(session: Session, id: int, form: models.FormUpdate) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == id)
def update_one(
session: Session,
_id: int,
form: models.FormUpdate) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == _id)
result = session.exec(statement)
new_form = result.first()
if not new_form:
return None
raise exceptions.FormNotFoundError(messages.Messages.not_found('form'))
if form.productor_id and not session.get(
models.Productor, form.productor_id):
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
if form.referer_id and not session.get(models.User, form.referer_id):
raise exceptions.UserNotFoundError(messages.Messages.not_found('user'))
form_updates = form.model_dump(exclude_unset=True)
for key, value in form_updates.items():
setattr(new_form, key, value)
@@ -57,21 +95,31 @@ def update_one(session: Session, id: int, form: models.FormUpdate) -> models.For
session.refresh(new_form)
return new_form
def delete_one(session: Session, id: int) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == id)
def delete_one(session: Session, _id: int) -> models.FormPublic:
statement = select(models.Form).where(models.Form.id == _id)
result = session.exec(statement)
form = result.first()
if not form:
return None
raise exceptions.FormNotFoundError(messages.Messages.not_found('form'))
result = models.FormPublic.model_validate(form)
session.delete(form)
session.commit()
return result
def is_allowed(session: Session, user: models.User, id: int) -> bool:
statement = select(models.Form)\
.join(models.Productor, models.Form.productor_id == models.Productor.id)\
.where(models.Form.id == id)\
.where(models.Productor.type.in_([r.name for r in user.roles]))\
def is_allowed(session: Session, user: models.User, _id: int) -> bool:
statement = (
select(models.Form)
.join(
models.Productor,
models.Form.productor_id == models.Productor.id)
.where(models.Form.id == _id)
.where(
models.Productor.type.in_(
[r.name for r in user.roles]
)
)
.distinct()
return len(session.exec(statement).all()) > 0
)
return len(session.exec(statement).all()) > 0

View File

@@ -1,21 +1,18 @@
from sqlmodel import SQLModel
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.templates.templates import router as template_router
from src.auth.auth import router as auth_router
from src.contracts.contracts import router as contracts_router
from src.forms.forms import router as forms_router
from src.productors.productors import router as productors_router
from src.products.products import router as products_router
from src.users.users import router as users_router
from src.auth.auth import router as auth_router
from src.shipments.shipments import router as shipment_router
from src.settings import settings
from src.database import engine
from src.shipments.shipments import router as shipment_router
from src.templates.templates import router as template_router
from src.users.users import router as users_router
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
@@ -27,14 +24,11 @@ app.add_middleware(
expose_headers=['x-nbpage', 'Content-Disposition']
)
app.include_router(template_router)
app.include_router(contracts_router)
app.include_router(forms_router)
app.include_router(productors_router)
app.include_router(products_router)
app.include_router(users_router)
app.include_router(auth_router)
app.include_router(shipment_router)
SQLModel.metadata.create_all(engine)
app.include_router(template_router, prefix="/api")
app.include_router(contracts_router, prefix="/api")
app.include_router(forms_router, prefix="/api")
app.include_router(productors_router, prefix="/api")
app.include_router(products_router, prefix="/api")
app.include_router(users_router, prefix="/api")
app.include_router(auth_router, prefix="/api")
app.include_router(shipment_router, prefix="/api")

View File

@@ -1,10 +1,20 @@
notfound = "Resource was not found."
pdferror = "An error occured during PDF generation please contact administrator"
tokenexipired = "Token expired"
invalidtoken = "Invalid token"
notauthenticated = "Not authenticated"
usernotfound = "User not found"
userloggedout = "User logged out"
failtogettoken = "Failed to get token"
unauthorized = "Unauthorized"
notallowed = "Not Allowed"
pdferror = 'An error occured during PDF generation please contact administrator'
class Messages:
unauthorized = 'User is Unauthorized'
notauthenticated = 'User is not authenticated'
tokenexipired = 'Token has expired'
invalidtoken = 'Token is invalid'
@staticmethod
def not_found(resource: str) -> str:
return f'{resource.capitalize()} not found'
@staticmethod
def invalid_input(resource: str, reason: str = "") -> str:
return f'Invalid {resource} input {':' if reason else ""} {reason}'
@staticmethod
def not_allowed(resource: str, action: str) -> str:
return f'User is not allowed to {action} this {resource}'

View File

@@ -1,98 +1,136 @@
from sqlmodel import Field, SQLModel, Relationship, Column, LargeBinary
import datetime
from enum import StrEnum
from typing import Optional
import datetime
from sqlmodel import Column, Field, LargeBinary, Relationship, SQLModel
class ContractType(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
id: int | None = Field(
default=None,
primary_key=True
)
name: str
class UserContractTypeLink(SQLModel, table=True):
user_id: int = Field(foreign_key="user.id", primary_key=True)
contract_type_id: int = Field(foreign_key="contracttype.id", primary_key=True)
user_id: int = Field(
foreign_key='user.id',
primary_key=True
)
contract_type_id: int = Field(
foreign_key='contracttype.id',
primary_key=True
)
class UserBase(SQLModel):
name: str
email: str
class UserPublic(UserBase):
id: int
roles: list[ContractType]
class User(UserBase, table=True):
id: int | None = Field(default=None, primary_key=True)
roles: list[ContractType] = Relationship(
link_model=UserContractTypeLink
)
class UserUpdate(SQLModel):
name: str | None
email: str | None
role_names: list[str] | None
class UserCreate(UserBase):
role_names: list[str] | None
class PaymentMethodBase(SQLModel):
name: str
details: str
max: int | None
class PaymentMethod(PaymentMethodBase, table=True):
id: int | None = Field(default=None, primary_key=True)
productor_id: int = Field(foreign_key="productor.id", ondelete="CASCADE")
productor: Optional["Productor"] = Relationship(
back_populates="payment_methods",
productor_id: int = Field(foreign_key='productor.id', ondelete='CASCADE')
productor: Optional['Productor'] = Relationship(
back_populates='payment_methods',
)
class PaymentMethodPublic(PaymentMethodBase):
id: int
productor: Optional["Productor"]
productor: Optional['Productor']
class ProductorBase(SQLModel):
name: str
address: str
type: str
class ProductorPublic(ProductorBase):
id: int
products: list["Product"] = []
payment_methods: list["PaymentMethod"] = []
products: list['Product'] = Field(default_factory=list)
payment_methods: list['PaymentMethod'] = Field(default_factory=list)
class Productor(ProductorBase, table=True):
id: int | None = Field(default=None, primary_key=True)
products: list["Product"] = Relationship(
products: list['Product'] = Relationship(
back_populates='productor',
sa_relationship_kwargs={
"order_by": "Product.name"
'order_by': 'Product.name'
},
)
payment_methods: list["PaymentMethod"] = Relationship(
back_populates="productor",
payment_methods: list['PaymentMethod'] = Relationship(
back_populates='productor',
cascade_delete=True
)
class ProductorUpdate(SQLModel):
name: str | None
address: str | None
payment_methods: list["PaymentMethod"] = []
payment_methods: list['PaymentMethod'] = Field(default_factory=list)
type: str | None
class ProductorCreate(ProductorBase):
payment_methods: list["PaymentMethod"] = []
payment_methods: list['PaymentMethod'] = Field(default_factory=list)
class Unit(StrEnum):
GRAMS = "1"
KILO = "2"
PIECE = "3"
GRAMS = '1'
KILO = '2'
PIECE = '3'
class ProductType(StrEnum):
OCCASIONAL = "1"
RECCURENT = "2"
OCCASIONAL = '1'
RECCURENT = '2'
class ShipmentProductLink(SQLModel, table=True):
shipment_id: Optional[int] = Field(default=None, foreign_key="shipment.id", primary_key=True)
product_id: Optional[int] = Field(default=None, foreign_key="product.id", primary_key=True)
shipment_id: Optional[int] = Field(
default=None,
foreign_key='shipment.id',
primary_key=True
)
product_id: Optional[int] = Field(
default=None,
foreign_key='product.id',
primary_key=True
)
class ProductBase(SQLModel):
name: str
@@ -102,17 +140,31 @@ class ProductBase(SQLModel):
quantity: float | None
quantity_unit: str | None
type: ProductType
productor_id: int | None = Field(default=None, foreign_key="productor.id")
productor_id: int | None = Field(
default=None,
foreign_key='productor.id'
)
class ProductPublic(ProductBase):
id: int
productor: Productor | None
shipments: list["Shipment"] | None
shipments: list['Shipment'] | None
class Product(ProductBase, table=True):
id: int | None = Field(default=None, primary_key=True)
shipments: list["Shipment"] = Relationship(back_populates="products", link_model=ShipmentProductLink)
productor: Optional[Productor] = Relationship(back_populates="products")
id: int | None = Field(
default=None,
primary_key=True
)
shipments: list['Shipment'] = Relationship(
back_populates='products',
link_model=ShipmentProductLink
)
productor: Optional[Productor] = Relationship(
back_populates='products'
)
class ProductUpdate(SQLModel):
name: str | None
@@ -124,40 +176,46 @@ class ProductUpdate(SQLModel):
productor_id: int | None
type: ProductType | None
class ProductCreate(ProductBase):
pass
class FormBase(SQLModel):
name: str
productor_id: int | None = Field(default=None, foreign_key="productor.id")
referer_id: int | None = Field(default=None, foreign_key="user.id")
productor_id: int | None = Field(default=None, foreign_key='productor.id')
referer_id: int | None = Field(default=None, foreign_key='user.id')
season: str
start: datetime.date
end: datetime.date
minimum_shipment_value: float | None
visible: bool
class FormPublic(FormBase):
id: int
productor: ProductorPublic | None
referer: User | None
shipments: list["ShipmentPublic"] = []
shipments: list['ShipmentPublic'] = Field(default_factory=list)
class Form(FormBase, table=True):
id: int | None = Field(default=None, primary_key=True)
productor: Optional['Productor'] = Relationship()
referer: Optional['User'] = Relationship()
shipments: list["Shipment"] = Relationship(
back_populates="form",
shipments: list['Shipment'] = Relationship(
back_populates='form',
cascade_delete=True,
sa_relationship_kwargs={
"order_by": "Shipment.name"
'order_by': 'Shipment.name'
},
)
contracts: list["Contract"] = Relationship(
back_populates="form",
contracts: list['Contract'] = Relationship(
back_populates='form',
cascade_delete=True
)
class FormUpdate(SQLModel):
name: str | None
productor_id: int | None
@@ -166,36 +224,46 @@ class FormUpdate(SQLModel):
start: datetime.date | None
end: datetime.date | None
minimum_shipment_value: float | None
visible: bool | None
class FormCreate(FormBase):
pass
class TemplateBase(SQLModel):
pass
class TemplatePublic(TemplateBase):
id: int
class Template(TemplateBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class TemplateUpdate(SQLModel):
pass
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",
contract_id: int = Field(foreign_key='contract.id', ondelete='CASCADE')
contract: Optional['Contract'] = Relationship(
back_populates='cheques',
)
class ContractBase(SQLModel):
firstname: str
lastname: str
@@ -204,105 +272,122 @@ class ContractBase(SQLModel):
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",
foreign_key='form.id',
nullable=False,
ondelete="CASCADE"
ondelete='CASCADE'
)
products: list["ContractProduct"] = Relationship(
back_populates="contract",
products: list['ContractProduct'] = Relationship(
back_populates='contract',
cascade_delete=True
)
form: Optional[Form] = Relationship(back_populates="contracts")
form: Form = Relationship(back_populates='contracts')
cheques: list[Cheque] = Relationship(
back_populates="contract",
back_populates='contract',
cascade_delete=True
)
file: bytes = Field(sa_column=Column(LargeBinary))
total_price: float | None
class ContractCreate(ContractBase):
products: list["ContractProductCreate"] = []
cheques: list["Cheque"] = []
products: list['ContractProductCreate'] = Field(default_factory=list)
cheques: list['Cheque'] = Field(default_factory=list)
form_id: int
class ContractUpdate(SQLModel):
file: bytes
class ContractPublic(ContractBase):
id: int
products: list["ContractProduct"] = []
products: list['ContractProduct'] = Field(default_factory=list)
form: Form
total_price: float | None
# file: bytes
class ContractProductBase(SQLModel):
product_id: int = Field(
foreign_key="product.id",
foreign_key='product.id',
nullable=False,
ondelete="CASCADE"
ondelete='CASCADE'
)
shipment_id: int | None = Field(
default=None,
foreign_key="shipment.id",
foreign_key='shipment.id',
nullable=True,
ondelete="CASCADE"
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",
foreign_key='contract.id',
nullable=False,
ondelete="CASCADE"
ondelete='CASCADE'
)
contract: Optional["Contract"] = Relationship(back_populates="products")
product: Optional["Product"] = Relationship()
shipment: Optional["Shipment"] = Relationship()
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"]
shipment: Optional['Shipment']
class ContractProductCreate(ContractProductBase):
pass
class ContractProductUpdate(ContractProductBase):
pass
class ShipmentBase(SQLModel):
name: str
date: datetime.date
form_id: int | None = Field(default=None, foreign_key="form.id", ondelete="CASCADE")
form_id: int | None = Field(
default=None,
foreign_key='form.id',
ondelete='CASCADE')
class ShipmentPublic(ShipmentBase):
id: int
products: list[Product] = []
products: list[Product] = Field(default_factory=list)
form: Form | None
class Shipment(ShipmentBase, table=True):
id: int | None = Field(default=None, primary_key=True)
products: list[Product] = Relationship(
back_populates="shipments",
back_populates='shipments',
link_model=ShipmentProductLink,
sa_relationship_kwargs={
"order_by": "Product.name"
'order_by': 'Product.name'
},
)
form: Optional[Form] = Relationship(back_populates="shipments")
form: Optional[Form] = Relationship(back_populates='shipments')
class ShipmentUpdate(SQLModel):
name: str | None
date: str | None
product_ids: list[int] | None = []
date: datetime.date | None
product_ids: list[int] | None = Field(default_factory=list)
class ShipmentCreate(ShipmentBase):
product_ids: list[int] = []
form_id: int
product_ids: list[int] = Field(default_factory=list)
form_id: int

View File

@@ -1,3 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,17 @@
import logging
class ProductorServiceError(Exception):
def __init__(self, message: str):
super().__init__(message)
logging.error('ProductorService : %s', message)
class ProductorNotFoundError(ProductorServiceError):
pass
class ProductorCreateError(ProductorServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,59 +1,74 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.productors.exceptions as exceptions
import src.productors.service as service
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/productors')
@router.get('/', response_model=list[models.ProductorPublic])
@router.get('', response_model=list[models.ProductorPublic])
def get_productors(
names: list[str] = Query([]),
types: list[str] = Query([]),
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.get_all(session, names, types)
return service.get_all(session, user, names, types)
@router.get('/{id}', response_model=models.ProductorPublic)
@router.get('/{_id}', response_model=models.ProductorPublic)
def get_productor(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.get_one(session, id)
result = service.get_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('productor')
)
return result
@router.post('/', response_model=models.ProductorPublic)
@router.post('', response_model=models.ProductorPublic)
def create_productor(
productor: models.ProductorCreate,
productor: models.ProductorCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, productor)
try:
result = service.create_one(session, productor)
except exceptions.ProductorCreateError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return result
@router.put('/{id}', response_model=models.ProductorPublic)
@router.put('/{_id}', response_model=models.ProductorPublic)
def update_productor(
id: int, productor: models.ProductorUpdate,
_id: int, productor: models.ProductorUpdate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, productor)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.update_one(session, _id, productor)
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result
@router.delete('/{id}', response_model=models.ProductorPublic)
@router.delete('/{_id}', response_model=models.ProductorPublic)
def delete_productor(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.delete_one(session, _id)
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result

View File

@@ -1,23 +1,38 @@
import src.messages as messages
import src.productors.exceptions as exceptions
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
names: list[str],
session: Session,
user: models.User,
names: list[str],
types: list[str]
) -> list[models.ProductorPublic]:
statement = select(models.Productor)
statement = select(models.Productor)\
.where(models.Productor.type.in_([r.name for r in user.roles]))\
.distinct()
if len(names) > 0:
statement = statement.where(models.Productor.name.in_(names))
if len(types) > 0:
statement = statement.where(models.Productor.type.in_(types))
return session.exec(statement.order_by(models.Productor.name)).all()
def get_one(session: Session, productor_id: int) -> models.ProductorPublic:
return session.get(models.Productor, productor_id)
def create_one(session: Session, productor: models.ProductorCreate) -> models.ProductorPublic:
productor_create = productor.model_dump(exclude_unset=True, exclude="payment_methods")
def create_one(
session: Session,
productor: models.ProductorCreate) -> models.ProductorPublic:
if not productor:
raise exceptions.ProductorCreateError(
messages.Messages.invalid_input(
'productor', 'input cannot be None'))
productor_create = productor.model_dump(
exclude_unset=True, exclude='payment_methods')
new_productor = models.Productor(**productor_create)
new_productor.payment_methods = [
@@ -32,25 +47,31 @@ def create_one(session: Session, productor: models.ProductorCreate) -> models.Pr
session.refresh(new_productor)
return new_productor
def update_one(session: Session, id: int, productor: models.ProductorUpdate) -> models.ProductorPublic:
def update_one(
session: Session,
id: int,
productor: models.ProductorUpdate) -> models.ProductorPublic:
statement = select(models.Productor).where(models.Productor.id == id)
result = session.exec(statement)
new_productor = result.first()
if not new_productor:
return None
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
productor_updates = productor.model_dump(exclude_unset=True)
if "payment_methods" in productor_updates:
if 'payment_methods' in productor_updates:
new_productor.payment_methods.clear()
for pm in productor_updates["payment_methods"]:
for pm in productor_updates['payment_methods']:
new_productor.payment_methods.append(
models.PaymentMethod(
name=pm["name"],
details=pm["details"],
productor_id=id
name=pm['name'],
details=pm['details'],
productor_id=id,
max=pm['max']
)
)
del productor_updates["payment_methods"]
del productor_updates['payment_methods']
for key, value in productor_updates.items():
setattr(new_productor, key, value)
@@ -59,12 +80,14 @@ def update_one(session: Session, id: int, productor: models.ProductorUpdate) ->
session.refresh(new_productor)
return new_productor
def delete_one(session: Session, id: int) -> models.ProductorPublic:
statement = select(models.Productor).where(models.Productor.id == id)
result = session.exec(statement)
productor = result.first()
if not productor:
return None
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
result = models.ProductorPublic.model_validate(productor)
session.delete(productor)
session.commit()

View File

@@ -1,3 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,17 @@
class ProductServiceError(Exception):
def __init__(self, message: str):
super().__init__(message)
class ProductorNotFoundError(ProductServiceError):
pass
class ProductNotFoundError(ProductServiceError):
pass
class ProductCreateError(ProductServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,64 +1,83 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.products.exceptions as exceptions
import src.products.service as service
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/products')
#user=Depends(get_current_user)
@router.get('/', response_model=list[models.ProductPublic], )
@router.get('', response_model=list[models.ProductPublic], )
def get_products(
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session),
session: Session = Depends(get_session),
names: list[str] = Query([]),
types: list[str] = Query([]),
productors: list[str] = Query([]),
):
return service.get_all(
session,
names,
productors,
session,
user,
names,
productors,
types,
)
@router.get('/{id}', response_model=models.ProductPublic)
def get_product(
id: int,
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('product'))
return result
@router.post('/', response_model=models.ProductPublic)
@router.post('', response_model=models.ProductPublic)
def create_product(
product: models.ProductCreate,
product: models.ProductCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, product)
try:
result = service.create_one(session, product)
except exceptions.ProductCreateError as error:
raise HTTPException(status_code=400, detail=str(error))
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error))
return result
@router.put('/{id}', response_model=models.ProductPublic)
def update_product(
id: int, product: models.ProductUpdate,
id: int, product: models.ProductUpdate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, product)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.update_one(session, id, product)
except exceptions.ProductNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error))
except exceptions.ProductorNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error))
return result
@router.delete('/{id}', response_model=models.ProductPublic)
def delete_product(
id: int,
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.delete_one(session, id)
except exceptions.ProductNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error))
return result

View File

@@ -1,25 +1,46 @@
import src.messages as messages
import src.products.exceptions as exceptions
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
names: list[str],
session: Session,
user: models.User,
names: list[str],
productors: list[str],
types: list[str],
) -> list[models.ProductPublic]:
statement = select(models.Product)
statement = select(
models.Product) .join(
models.Productor,
models.Product.productor_id == models.Productor.id) .where(
models.Productor.type.in_(
[
r.name for r in user.roles])) .distinct()
if len(names) > 0:
statement = statement.where(models.Product.name.in_(names))
if len(productors) > 0:
statement = statement.join(models.Productor).where(models.Productor.name.in_(productors))
statement = statement.where(models.Productor.name.in_(productors))
if len(types) > 0:
statement = statement.where(models.Product.type.in_(types))
return session.exec(statement.order_by(models.Product.name)).all()
def get_one(session: Session, product_id: int) -> models.ProductPublic:
return session.get(models.Product, product_id)
def create_one(session: Session, product: models.ProductCreate) -> models.ProductPublic:
def create_one(
session: Session,
product: models.ProductCreate) -> models.ProductPublic:
if not product:
raise exceptions.ProductCreateError(
messages.Messages.invalid_input(
'product', 'input cannot be None'))
if not session.get(models.Productor, product.productor_id):
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
product_create = product.model_dump(exclude_unset=True)
new_product = models.Product(**product_create)
session.add(new_product)
@@ -27,12 +48,22 @@ def create_one(session: Session, product: models.ProductCreate) -> models.Produc
session.refresh(new_product)
return new_product
def update_one(session: Session, id: int, product: models.ProductUpdate) -> models.ProductPublic:
def update_one(
session: Session,
id: int,
product: models.ProductUpdate) -> models.ProductPublic:
statement = select(models.Product).where(models.Product.id == id)
result = session.exec(statement)
new_product = result.first()
if not new_product:
return None
raise exceptions.ProductNotFoundError(
messages.Messages.not_found('product'))
if product.productor_id and not session.get(
models.Productor, product.productor_id):
raise exceptions.ProductorNotFoundError(
messages.Messages.not_found('productor'))
product_updates = product.model_dump(exclude_unset=True)
for key, value in product_updates.items():
setattr(new_product, key, value)
@@ -42,12 +73,14 @@ def update_one(session: Session, id: int, product: models.ProductUpdate) -> mode
session.refresh(new_product)
return new_product
def delete_one(session: Session, id: int) -> models.ProductPublic:
statement = select(models.Product).where(models.Product.id == id)
result = session.exec(statement)
product = result.first()
if not product:
return None
raise exceptions.ProductNotFoundError(
messages.Messages.not_found('product'))
result = models.ProductPublic.model_validate(product)
session.delete(product)
session.commit()

View File

@@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
origins: str
@@ -16,13 +17,25 @@ class Settings(BaseSettings):
max_age: int
debug: bool
class Config:
env_file = "../.env"
model_config = SettingsConfigDict(
env_file='../.env'
)
settings = Settings()
AUTH_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/auth"
TOKEN_URL = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/token"
ISSUER = f"{settings.keycloak_server}/realms/{settings.keycloak_realm}"
JWKS_URL = f"{ISSUER}/protocol/openid-connect/certs"
LOGOUT_URL = f'{settings.keycloak_server}/realms/{settings.keycloak_realm}/protocol/openid-connect/logout'
AUTH_URL = (
f'{settings.keycloak_server}/realms/'
f'{settings.keycloak_realm}/protocol/openid-connect/auth'
)
TOKEN_URL = (
f'{settings.keycloak_server}/realms/'
f'{settings.keycloak_realm}/protocol/openid-connect/token'
)
ISSUER = f'{settings.keycloak_server}/realms/{settings.keycloak_realm}'
JWKS_URL = f'{ISSUER}/protocol/openid-connect/certs'
LOGOUT_URL = (
f'{settings.keycloak_server}/realms/'
f'{settings.keycloak_realm}/protocol/openid-connect/logout'
)

View File

@@ -1,3 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,17 @@
import logging
class ShipmentServiceError(Exception):
def __init__(self, message: str):
super().__init__(message)
logging.error('ShipmentService : %s', message)
class ShipmentNotFoundError(ShipmentServiceError):
pass
class ShipmentCreateError(ShipmentServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,46 +1,111 @@
# pylint: disable=E1101
import datetime
import src.messages as messages
import src.shipments.exceptions as exceptions
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
names: list[str],
dates: list[str],
forms: list[int]
user: models.User,
names: list[str] = None,
dates: list[str] = None,
forms: list[str] = None
) -> list[models.ShipmentPublic]:
statement = select(models.Shipment)
if len(names) > 0:
statement = (
select(models.Shipment)
.join(
models.Form,
models.Shipment.form_id == models.Form.id)
.join(
models.Productor,
models.Form.productor_id == models.Productor.id)
.where(
models.Productor.type.in_(
[r.name for r in user.roles]
)
)
.distinct()
)
if names and len(names) > 0:
statement = statement.where(models.Shipment.name.in_(names))
if len(dates) > 0:
statement = statement.where(models.Shipment.date.in_(list(map(lambda x: datetime.strptime(x, '%Y-%m-%d'), dates))))
if len(forms) > 0:
statement = statement.join(models.Form).where(models.Form.name.in_(forms))
if dates and len(dates) > 0:
statement = statement.where(
models.Shipment.date.in_(
list(map(
lambda x: datetime.datetime.strptime(
x, '%Y-%m-%d').date(),
dates
))
)
)
if forms and len(forms) > 0:
statement = statement.where(models.Form.name.in_(forms))
return session.exec(statement.order_by(models.Shipment.name)).all()
def get_one(session: Session, shipment_id: int) -> models.ShipmentPublic:
return session.get(models.Shipment, shipment_id)
def create_one(session: Session, shipment: models.ShipmentCreate) -> models.ShipmentPublic:
products = session.exec(select(models.Product).where(models.Product.id.in_(shipment.product_ids))).all()
shipment_create = shipment.model_dump(exclude_unset=True, exclude={'product_ids'})
def create_one(
session: Session,
shipment: models.ShipmentCreate) -> models.ShipmentPublic:
if shipment is None:
raise exceptions.ShipmentCreateError(
messages.Messages.invalid_input(
'shipment', 'input cannot be None'))
products = session.exec(
select(models.Product)
.where(
models.Product.id.in_(
shipment.product_ids
)
)
).all()
shipment_create = shipment.model_dump(
exclude_unset=True, exclude={'product_ids'}
)
new_shipment = models.Shipment(**shipment_create, products=products)
session.add(new_shipment)
session.commit()
session.refresh(new_shipment)
return new_shipment
def update_one(session: Session, id: int, shipment: models.ShipmentUpdate) -> models.ShipmentPublic:
statement = select(models.Shipment).where(models.Shipment.id == id)
def update_one(
session: Session,
_id: int,
shipment: models.ShipmentUpdate) -> models.ShipmentPublic:
if shipment is None:
raise exceptions.ShipmentCreateError(
messages.Messages.invalid_input(
'shipment', 'input cannot be None'))
statement = select(models.Shipment).where(models.Shipment.id == _id)
result = session.exec(statement)
new_shipment = result.first()
if not new_shipment:
return None
raise exceptions.ShipmentNotFoundError(
messages.Messages.not_found('shipment'))
products_to_add = session.exec(select(models.Product).where(models.Product.id.in_(shipment.product_ids))).all()
products_to_add = session.exec(
select(
models.Product
).where(
models.Product.id.in_(
shipment.product_ids
)
)
).all()
new_shipment.products.clear()
for add in products_to_add:
new_shipment.products.append(add)
shipment_updates = shipment.model_dump(exclude_unset=True, exclude={"product_ids"})
shipment_updates = shipment.model_dump(
exclude_unset=True, exclude={"product_ids"}
)
for key, value in shipment_updates.items():
setattr(new_shipment, key, value)
@@ -49,13 +114,16 @@ def update_one(session: Session, id: int, shipment: models.ShipmentUpdate) -> mo
session.refresh(new_shipment)
return new_shipment
def delete_one(session: Session, id: int) -> models.ShipmentPublic:
statement = select(models.Shipment).where(models.Shipment.id == id)
def delete_one(session: Session, _id: int) -> models.ShipmentPublic:
statement = select(models.Shipment).where(models.Shipment.id == _id)
result = session.exec(statement)
shipment = result.first()
if not shipment:
return None
raise exceptions.ShipmentNotFoundError(
messages.Messages.not_found('shipment'))
result = models.ShipmentPublic.model_validate(shipment)
session.delete(shipment)
session.commit()
return result
return result

View File

@@ -1,64 +1,82 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.shipments.exceptions as exceptions
import src.shipments.service as service
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/shipments')
@router.get('/', response_model=list[models.ShipmentPublic], )
@router.get('', response_model=list[models.ShipmentPublic], )
def get_shipments(
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user),
names: list[str] = Query([]),
dates: list[str] = Query([]),
forms: list[str] = Query([]),
):
return service.get_all(
session,
user,
names,
dates,
forms,
)
@router.get('/{id}', response_model=models.ShipmentPublic)
@router.get('/{_id}', response_model=models.ShipmentPublic)
def get_shipment(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.get_one(session, id)
result = service.get_one(session, _id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(
status_code=404,
detail=messages.Messages.not_found('shipment')
)
return result
@router.post('/', response_model=models.ShipmentPublic)
@router.post('', response_model=models.ShipmentPublic)
def create_shipment(
shipment: models.ShipmentCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, shipment)
try:
result = service.create_one(session, shipment)
except exceptions.ShipmentCreateError as error:
raise HTTPException(status_code=400, detail=str(error)) from error
return result
@router.put('/{id}', response_model=models.ShipmentPublic)
@router.put('/{_id}', response_model=models.ShipmentPublic)
def update_shipment(
id: int, shipment: models.ShipmentUpdate,
_id: int,
shipment: models.ShipmentUpdate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, shipment)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.update_one(session, _id, shipment)
except exceptions.ShipmentNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result
@router.delete('/{id}', response_model=models.ShipmentPublic)
@router.delete('/{_id}', response_model=models.ShipmentPublic)
def delete_shipment(
id: int,
_id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.delete_one(session, _id)
except exceptions.ShipmentNotFoundError as error:
raise HTTPException(status_code=404, detail=str(error)) from error
return result

View File

@@ -1,3 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: MIT

View File

@@ -1,14 +1,19 @@
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(session: Session) -> list[models.TemplatePublic]:
statement = select(models.Template)
return session.exec(statement.order_by(models.Template.name)).all()
def get_one(session: Session, template_id: int) -> models.TemplatePublic:
return session.get(models.Template, template_id)
def create_one(session: Session, template: models.TemplateCreate) -> models.TemplatePublic:
def create_one(
session: Session,
template: models.TemplateCreate) -> models.TemplatePublic:
template_create = template.model_dump(exclude_unset=True)
new_template = models.Template(**template_create)
session.add(new_template)
@@ -16,7 +21,11 @@ def create_one(session: Session, template: models.TemplateCreate) -> models.Temp
session.refresh(new_template)
return new_template
def update_one(session: Session, id: int, template: models.TemplateUpdate) -> models.TemplatePublic:
def update_one(
session: Session,
id: int,
template: models.TemplateUpdate) -> models.TemplatePublic:
statement = select(models.Template).where(models.Template.id == id)
result = session.exec(statement)
new_template = result.first()
@@ -30,6 +39,7 @@ def update_one(session: Session, id: int, template: models.TemplateUpdate) -> mo
session.refresh(new_template)
return new_template
def delete_one(session: Session, id: int) -> models.TemplatePublic:
statement = select(models.Template).where(models.Template.id == id)
result = session.exec(statement)

View File

@@ -1,57 +1,65 @@
from fastapi import APIRouter, HTTPException, Depends
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.templates.service as service
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/templates')
@router.get('/', response_model=list[models.TemplatePublic])
@router.get('', response_model=list[models.TemplatePublic])
def get_templates(
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.get_all(session)
@router.get('/{id}', response_model=models.TemplatePublic)
def get_template(
id: int,
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('template'))
return result
@router.post('/', response_model=models.TemplatePublic)
@router.post('', response_model=models.TemplatePublic)
def create_template(
template: models.TemplateCreate,
template: models.TemplateCreate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, template)
@router.put('/{id}', response_model=models.TemplatePublic)
def update_template(
id: int, template: models.TemplateUpdate,
id: int, template: models.TemplateUpdate,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, template)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('template'))
return result
@router.delete('/{id}', response_model=models.TemplatePublic)
def delete_template(
id: int,
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('template'))
return result

View File

@@ -1,3 +1,3 @@
# SPDX-FileCopyrightText: 2026-present Julien Aldon <julien.aldon@wanadoo.fr>
#
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: MIT

View File

@@ -0,0 +1,17 @@
import logging
class UserServiceError(Exception):
def __init__(self, message: str):
super().__init__(message)
logging.error('UserService : %s', message)
class UserNotFoundError(UserServiceError):
pass
class UserCreateError(UserServiceError):
def __init__(self, message: str, field: str | None = None):
super().__init__(message)
self.field = field

View File

@@ -1,5 +1,8 @@
import src.messages as messages
import src.users.exceptions as exceptions
from sqlmodel import Session, select
import src.models as models
from src import models
def get_all(
session: Session,
@@ -13,11 +16,15 @@ def get_all(
statement = statement.where(models.User.email.in_(emails))
return session.exec(statement.order_by(models.User.name)).all()
def get_one(session: Session, user_id: int) -> models.UserPublic:
return session.get(models.User, user_id)
def get_or_create_roles(session: Session, role_names: list[str]) -> list[models.ContractType]:
statement = select(models.ContractType).where(models.ContractType.name.in_(role_names))
def get_or_create_roles(session: Session,
role_names: list[str]) -> list[models.ContractType]:
statement = select(models.ContractType).where(
models.ContractType.name.in_(role_names))
existing = session.exec(statement).all()
existing_roles = {role.name for role in existing}
missing_role = set(role_names) - existing_roles
@@ -33,8 +40,11 @@ def get_or_create_roles(session: Session, role_names: list[str]) -> list[models.
session.refresh(role)
return existing + new_roles
def get_or_create_user(session: Session, user_create: models.UserCreate):
statement = select(models.User).where(models.User.email == user_create.email)
statement = select(
models.User).where(
models.User.email == user_create.email)
user = session.exec(statement).first()
if user:
user_role_names = [r.name for r in user.roles]
@@ -44,11 +54,17 @@ def get_or_create_user(session: Session, user_create: models.UserCreate):
user = create_one(session, user_create)
return user
def get_roles(session: Session):
statement = select(models.ContractType)
return session.exec(statement.order_by(models.ContractType.name)).all()
def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
if user is None:
raise exceptions.UserCreateError(
messages.Messages.invalid_input(
'user', 'input cannot be None'))
new_user = models.User(
name=user.name,
email=user.email
@@ -62,32 +78,38 @@ def create_one(session: Session, user: models.UserCreate) -> models.UserPublic:
session.refresh(new_user)
return new_user
def update_one(session: Session, id: int, user: models.UserCreate) -> models.UserPublic:
def update_one(
session: Session,
id: int,
user: models.UserCreate) -> models.UserPublic:
if user is None:
raise exceptions.UserCreateError(
messages.s.invalid_input(
'user', 'input cannot be None'))
statement = select(models.User).where(models.User.id == id)
result = session.exec(statement)
new_user = result.first()
if not new_user:
return None
user_updates = user.model_dump(exclude="role_names")
for key, value in user_updates.items():
setattr(new_user, key, value)
raise exceptions.UserNotFoundError(f'User {id} not found')
new_user.email = user.email
new_user.name = user.name
roles = get_or_create_roles(session, user.role_names)
new_user.roles = roles
session.add(new_user)
session.commit()
session.refresh(new_user)
return new_user
def delete_one(session: Session, id: int) -> models.UserPublic:
statement = select(models.User).where(models.User.id == id)
result = session.exec(statement)
user = result.first()
if not user:
return None
raise exceptions.UserNotFoundError(f'User {id} not found')
result = models.UserPublic.model_validate(user)
session.delete(user)
session.commit()
return result
return result

View File

@@ -1,14 +1,16 @@
from fastapi import APIRouter, HTTPException, Depends, Query
import src.messages as messages
import src.models as models
from src.database import get_session
from sqlmodel import Session
import src.users.exceptions as exceptions
import src.users.service as service
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session
from src import models
from src.auth.auth import get_current_user
from src.database import get_session
router = APIRouter(prefix='/users')
@router.get('/', response_model=list[models.UserPublic])
@router.get('', response_model=list[models.UserPublic])
def get_users(
session: Session = Depends(get_session),
user: models.User = Depends(get_current_user),
@@ -21,6 +23,7 @@ def get_users(
emails,
)
@router.get('/roles', response_model=list[models.ContractType])
def get_roles(
user: models.User = Depends(get_current_user),
@@ -28,44 +31,57 @@ def get_roles(
):
return service.get_roles(session)
@router.get('/{id}', response_model=models.UserPublic)
def get_users(
id: int,
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.get_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('user'))
return result
@router.post('/', response_model=models.UserPublic)
@router.post('', response_model=models.UserPublic)
def create_user(
user: models.UserCreate,
user: models.UserCreate,
logged_user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
return service.create_one(session, user)
try:
user = service.create_one(session, user)
except exceptions.UserCreateError as error:
raise HTTPException(status_code=400, detail=str(error))
return user
@router.put('/{id}', response_model=models.UserPublic)
def update_user(
id: int,
user: models.UserUpdate,
id: int,
user: models.UserUpdate,
logged_user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.update_one(session, id, user)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.update_one(session, id, user)
except exceptions.UserNotFoundError as error:
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('user'))
return result
@router.delete('/{id}', response_model=models.UserPublic)
def delete_user(
id: int,
id: int,
user: models.User = Depends(get_current_user),
session: Session = Depends(get_session)
):
result = service.delete_one(session, id)
if result is None:
raise HTTPException(status_code=404, detail=messages.notfound)
try:
result = service.delete_one(session, id)
except exceptions.UserNotFoundError as error:
raise HTTPException(status_code=404,
detail=messages.Messages.not_found('user'))
return result