From 1bd0583c70bbdfe9b59ace4ff30f554dcaaa9270 Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Thu, 19 Feb 2026 16:20:45 +0100 Subject: [PATCH 01/56] fix auth login / logout / refresh token --- backend/src/auth/auth.py | 10 +++++++++- frontend/src/services/api.ts | 9 +++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/backend/src/auth/auth.py b/backend/src/auth/auth.py index 6ac89ac..02f2fc9 100644 --- a/backend/src/auth/auth.py +++ b/backend/src/auth/auth.py @@ -219,7 +219,15 @@ def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None): httponly=True, secure=True if settings.debug == False else True, samesite='lax', - max_age=4 + 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='lax', + max_age=settings.max_age ) return response diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 3034c8e..cc29a8f 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -29,7 +29,7 @@ export async function refreshToken() { return await fetch(`${Config.backend_uri}/auth/refresh`, {method: "POST", credentials: "include"}); } -export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) { +export async function fetchWithAuth(input: RequestInfo, options?: RequestInit, redirect: boolean = true) { const res = await fetch(input, { credentials: "include", ...options, @@ -38,7 +38,8 @@ export async function fetchWithAuth(input: RequestInfo, options?: RequestInit) { if (res.status === 401) { const refresh = await refreshToken(); if (refresh.status == 400 || refresh.status == 401) { - window.location.href = `/?sessionExpired=True`; + if (redirect) + window.location.href = `/?sessionExpired=True`; const error = new Error("Unauthorized"); error.cause = 401 @@ -836,9 +837,9 @@ export function useCurrentUser() { return useQuery({ queryKey: ["currentUser"], queryFn: () => { - return fetch(`${Config.backend_uri}/auth/user/me`, { + return fetchWithAuth(`${Config.backend_uri}/auth/user/me`, { credentials: "include", - }).then((res) => res.json()); + }, false).then((res) => res.json()); }, retry: false, }); From 7574626e52177260e6495e8a4b1142f898edc4f9 Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Thu, 19 Feb 2026 17:34:15 +0100 Subject: [PATCH 02/56] add docker compose --- backend/Dockerfile | 13 ++++ backend/requirements.txt | 65 ++++++++++++++++++ backend/src/database.py | 2 +- backend/src/main.py | 16 ++--- docker-compose.yaml | 66 ++++++++++--------- frontend/Dockerfile | 20 ++++++ frontend/locales/en.json | 1 + frontend/locales/fr.json | 1 + frontend/nginx/default.conf | 26 ++++++++ frontend/src/components/Users/Modal/index.tsx | 1 - frontend/src/components/Users/Row/index.tsx | 27 +++++++- frontend/src/pages/Contracts/index.tsx | 1 + frontend/src/pages/Users/index.tsx | 1 + frontend/src/services/resources/users.ts | 2 +- 14 files changed, 199 insertions(+), 43 deletions(-) create mode 100644 backend/Dockerfile create mode 100644 backend/requirements.txt create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx/default.conf diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..15938b7 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12 + +WORKDIR /code + +RUN apt update && apt install -y weasyprint + +COPY ./backend/requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./backend /code/app + +CMD ["fastapi", "run", "app/src/main.py", "--port", "8000"] \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..7cad117 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,65 @@ +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.12.1 +brotli==1.2.0 +certifi==2026.1.4 +cffi==2.0.0 +charset-normalizer==3.4.4 +click==8.3.1 +cryptography==46.0.5 +cssselect2==0.9.0 +dnspython==2.8.0 +email-validator==2.3.0 +fastapi==0.129.0 +fastapi-cli==0.0.23 +fastapi-cloud-cli==0.13.0 +fastar==0.8.0 +fonttools==4.61.1 +greenlet==3.3.1 +h11==0.16.0 +httpcore==1.0.9 +httptools==0.7.1 +httpx==0.28.1 +idna==3.11 +Jinja2==3.1.6 +lxml==6.0.2 +markdown-it-py==4.0.0 +MarkupSafe==3.0.3 +mdurl==0.1.2 +odfdo==3.20.2 +pillow==12.1.1 +psycopg2-binary==2.9.11 +pycparser==3.0 +pydantic==2.12.5 +pydantic-extra-types==2.11.0 +pydantic-settings==2.13.1 +pydantic_core==2.41.5 +pydyf==0.12.1 +Pygments==2.19.2 +PyJWT==2.11.0 +pyphen==0.17.2 +python-dotenv==1.2.1 +python-multipart==0.0.22 +PyYAML==6.0.3 +requests==2.32.5 +rich==14.3.2 +rich-toolkit==0.19.4 +rignore==0.7.6 +sentry-sdk==2.53.0 +shellingham==1.5.4 +SQLAlchemy==2.0.46 +sqlmodel==0.0.34 +starlette==0.52.1 +tinycss2==1.5.1 +tinyhtml5==2.0.0 +typer==0.24.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.6.3 +uvicorn==0.41.0 +uvloop==0.22.1 +watchfiles==1.1.1 +weasyprint==68.1 +webencodings==0.5.1 +websockets==16.0 +zopfli==0.4.1 diff --git a/backend/src/database.py b/backend/src/database.py index e536ccf..c9eaae1 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -1,7 +1,7 @@ from sqlmodel import create_engine, SQLModel, Session 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: diff --git a/backend/src/main.py b/backend/src/main.py index 29b48f0..e931423 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -28,13 +28,13 @@ app.add_middleware( ) -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) +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") SQLModel.metadata.create_all(engine) \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index ea142cb..0ca4f3d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,32 +1,39 @@ -version: "3.9" services: - # nginx: - # restart: always - # build: - # context: . - # dockerfile: front/Dockerfile - # args: - # VUE_APP_ROOT_FQDN: ${SERVICE_ROOT_FQDN} - # ports: - # - 80:80 - # depends_on: - # - back - # back: - # build: - # context: . - # dockerfile: back/Dockerfile - # restart: always - # environment: - # SERVICE_ORIGIN: ${SERVICE_ORIGIN} - # DB_HOST: database - # MARIADB_USER: ${MARIADB_USER} - # MARIADB_PASSWORD: ${MARIADB_PASSWORD} - # MARIADB_DATABASE: ${MARIADB_DATABASE} - # SERVICE_SECRET_KEY: ${SERVICE_SECRET_KEY} - # ports: - # - 8000:8000 - # depends_on: - # - database + nginx: + restart: always + build: + context: . + dockerfile: frontend/Dockerfile + args: + VITE_API_URL: ${VITE_API_URL} + ports: + - 80:80 + depends_on: + - back + back: + build: + context: . + dockerfile: backend/Dockerfile + restart: always + environment: + ORIGINS: ${ORIGINS} + DB_HOST: database + DB_USER: ${DB_USER} + DB_PASS: ${DB_PASS} + DB_NAME: ${DB_NAME} + SECRET_KEY: ${SECRET_KEY} + VITE_API_URL: ${VITE_API_URL} + KEYCLOAK_SERVER: ${KEYCLOAK_SERVER} + KEYCLOAK_REALM: ${KEYCLOAK_REALM} + KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET} + KEYCLOAK_REDIRECT_URI: ${KEYCLOAK_REDIRECT_URI} + DEBUG: ${DEBUG} + MAX_AGE: ${MAX_AGE} + ports: + - 8000:8000 + depends_on: + - database database: image: postgres restart: always @@ -35,8 +42,5 @@ services: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_DB: ${DB_NAME} - ROOT_FQDN: ${ROOT_FQDN} - ports: - - "54321:5432" volumes: db: \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..11abfe0 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,20 @@ +FROM node:20.19-alpine AS build + +WORKDIR /app + +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL + +COPY frontend/package.json frontend/package-lock.json /app/ +RUN npm install + +COPY frontend/ . + +RUN npm run build + +FROM nginx:latest + +COPY --from=build /app/dist /srv/www/frontend + +RUN rm /etc/nginx/conf.d/default.conf +COPY --from=build /app/nginx/default.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/frontend/locales/en.json b/frontend/locales/en.json index e7370f7..99c9744 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -199,6 +199,7 @@ "your session has expired please log in again": "your session has expired please log in again", "session expired": "session expired", "user not allowed": "user not allowed", + "roles": "roles", "your keycloak user has no roles, please contact your administrator": "your keycloak user has no roles, please contact your administrator", "choose payment method": "choose your payment method (you do not need to pay now).", "the product unit will be assigned to the quantity requested in the form": "the product unit defines the unit used in the contract form.", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index 0da6741..cc60ae3 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -199,6 +199,7 @@ "your session has expired please log in again": "votre session a expiré veuillez vous reconnecter.", "session expired": "session expirée", "user not allowed": "utilisateur non authorisé", + "roles": "roles", "your keycloak user has no roles, please contact your administrator": "votre utilisateur keycloak n'a pas de roles configurés, contactez votre administrateur.", "choose payment method": "choisissez votre méthode de paiement (vous n'avez pas à payer tout de suite, uniquement renseigner comment vous souhaitez régler votre commande).", "the product unit will be assigned to the quantity requested in the form": "l'unité de vente du produit définit l'unité associée à la quantité demandée dans le formulaire des amapiens.", diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf new file mode 100644 index 0000000..5785728 --- /dev/null +++ b/frontend/nginx/default.conf @@ -0,0 +1,26 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + server_name localhost; + root /srv/www/frontend; + index index.html; + + location / { + try_files $uri /index.html; + } + + location /api/ { + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_redirect off; + proxy_buffering off; + proxy_pass http://back; + } +} + +upstream backend { + server back:8000; +} \ No newline at end of file diff --git a/frontend/src/components/Users/Modal/index.tsx b/frontend/src/components/Users/Modal/index.tsx index e533ef4..ba32179 100644 --- a/frontend/src/components/Users/Modal/index.tsx +++ b/frontend/src/components/Users/Modal/index.tsx @@ -3,7 +3,6 @@ import { Group, Modal, MultiSelect, - Select, TextInput, Title, type ModalBaseProps, diff --git a/frontend/src/components/Users/Row/index.tsx b/frontend/src/components/Users/Row/index.tsx index 6e5f7be..989875e 100644 --- a/frontend/src/components/Users/Row/index.tsx +++ b/frontend/src/components/Users/Row/index.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Table, Tooltip } from "@mantine/core"; +import { ActionIcon, Badge, Box, Table, Tooltip } from "@mantine/core"; import { t } from "@/config/i18n"; import { IconEdit, IconX } from "@tabler/icons-react"; import { type User } from "@/services/resources/users"; @@ -18,6 +18,31 @@ export default function UserRow({ user }: UserRowProps) { {user.name} {user.email} + + + {user.roles.slice(0, 3).map((value) => ( + + {t(value.name, { capfirst: true })} + + ))} + { + user.roles.length > 3 && ( + `${role.name} `)} + > + + +{user.roles.length - 3} + + + ) + } + + { e.stopPropagation(); navigate( diff --git a/frontend/src/pages/Users/index.tsx b/frontend/src/pages/Users/index.tsx index 4406e5d..6bedaf8 100644 --- a/frontend/src/pages/Users/index.tsx +++ b/frontend/src/pages/Users/index.tsx @@ -127,6 +127,7 @@ export default function Users() { {t("name", { capfirst: true })} {t("email", { capfirst: true })} + {t("roles", { capfirst: true })} {t("actions", { capfirst: true })} diff --git a/frontend/src/services/resources/users.ts b/frontend/src/services/resources/users.ts index e03a72b..a092282 100644 --- a/frontend/src/services/resources/users.ts +++ b/frontend/src/services/resources/users.ts @@ -15,7 +15,7 @@ export type User = { name: string; email: string; products: Product[]; - roles: string[]; + roles: Role[]; }; export type UserInputs = { From 242e29c8a6d86937c8a7f1e48deb262958cb08f8 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Thu, 19 Feb 2026 23:38:26 +0100 Subject: [PATCH 03/56] add workflow --- .gitea/workflows/deploy.yaml | 17 ++ backend/README.md | 13 ++ backend/alembic.ini | 147 +++++++++++++++++ backend/alembic/README | 1 + backend/alembic/env.py | 81 +++++++++ backend/alembic/script.py.mako | 29 ++++ .../370137544ee6_initial_repository.py | 155 ++++++++++++++++++ backend/src/auth/auth.py | 34 ++-- backend/src/contracts/generate_contract.py | 7 +- backend/src/main.py | 5 +- backend/src/settings.py | 1 - {amapcontract => bruno}/bruno.json | 2 +- {amapcontract => bruno}/collection.bru | 0 {amapcontract => bruno}/forms/Create.bru | 0 {amapcontract => bruno}/forms/Delete one.bru | 0 {amapcontract => bruno}/forms/Get all.bru | 0 {amapcontract => bruno}/forms/Get one.bru | 0 {amapcontract => bruno}/forms/Update one.bru | 0 {amapcontract => bruno}/forms/folder.bru | 0 {amapcontract => bruno}/productors/Create.bru | 0 .../productors/Delete one.bru | 0 .../productors/Get all.bru | 0 .../productors/Get one.bru | 0 .../productors/Update one.bru | 0 {amapcontract => bruno}/productors/folder.bru | 0 {amapcontract => bruno}/products/Create.bru | 0 .../products/Delete one.bru | 0 {amapcontract => bruno}/products/Get all.bru | 0 {amapcontract => bruno}/products/Get one.bru | 0 .../products/Update one.bru | 0 {amapcontract => bruno}/products/folder.bru | 0 {amapcontract => bruno}/shipments/Create.bru | 0 .../shipments/Delete one.bru | 0 {amapcontract => bruno}/shipments/Get all.bru | 0 {amapcontract => bruno}/shipments/Get one.bru | 0 .../shipments/Update one.bru | 0 {amapcontract => bruno}/shipments/folder.bru | 0 {amapcontract => bruno}/users/Create.bru | 0 {amapcontract => bruno}/users/Delete one.bru | 0 {amapcontract => bruno}/users/Get all.bru | 0 {amapcontract => bruno}/users/Get one.bru | 0 {amapcontract => bruno}/users/Update one.bru | 0 {amapcontract => bruno}/users/folder.bru | 0 docker-compose.dev.yaml | 55 +++++++ docker-compose.yaml | 7 +- frontend/Dockerfile.dev | 12 ++ 46 files changed, 536 insertions(+), 30 deletions(-) create mode 100644 .gitea/workflows/deploy.yaml create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/README create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/370137544ee6_initial_repository.py rename {amapcontract => bruno}/bruno.json (77%) rename {amapcontract => bruno}/collection.bru (100%) rename {amapcontract => bruno}/forms/Create.bru (100%) rename {amapcontract => bruno}/forms/Delete one.bru (100%) rename {amapcontract => bruno}/forms/Get all.bru (100%) rename {amapcontract => bruno}/forms/Get one.bru (100%) rename {amapcontract => bruno}/forms/Update one.bru (100%) rename {amapcontract => bruno}/forms/folder.bru (100%) rename {amapcontract => bruno}/productors/Create.bru (100%) rename {amapcontract => bruno}/productors/Delete one.bru (100%) rename {amapcontract => bruno}/productors/Get all.bru (100%) rename {amapcontract => bruno}/productors/Get one.bru (100%) rename {amapcontract => bruno}/productors/Update one.bru (100%) rename {amapcontract => bruno}/productors/folder.bru (100%) rename {amapcontract => bruno}/products/Create.bru (100%) rename {amapcontract => bruno}/products/Delete one.bru (100%) rename {amapcontract => bruno}/products/Get all.bru (100%) rename {amapcontract => bruno}/products/Get one.bru (100%) rename {amapcontract => bruno}/products/Update one.bru (100%) rename {amapcontract => bruno}/products/folder.bru (100%) rename {amapcontract => bruno}/shipments/Create.bru (100%) rename {amapcontract => bruno}/shipments/Delete one.bru (100%) rename {amapcontract => bruno}/shipments/Get all.bru (100%) rename {amapcontract => bruno}/shipments/Get one.bru (100%) rename {amapcontract => bruno}/shipments/Update one.bru (100%) rename {amapcontract => bruno}/shipments/folder.bru (100%) rename {amapcontract => bruno}/users/Create.bru (100%) rename {amapcontract => bruno}/users/Delete one.bru (100%) rename {amapcontract => bruno}/users/Get all.bru (100%) rename {amapcontract => bruno}/users/Get one.bru (100%) rename {amapcontract => bruno}/users/Update one.bru (100%) rename {amapcontract => bruno}/users/folder.bru (100%) create mode 100644 docker-compose.dev.yaml create mode 100644 frontend/Dockerfile.dev diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..74c9d6f --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,17 @@ +name: Deploy Amap +on: + push: + branches: + - main + workflow_dispatch: +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Build & deploy + run: | + docker compose -f docker-compose.yaml up -d --build + docker compose -f docker-compose.yaml exec backend alembic upgrade head \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 216cc47..a017a4d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -18,6 +18,19 @@ hatch shell fastapi dev src/main.py ``` +### Migration +This repository use `alembic` for migrations + +On first installation copy the `alembic.ini.example` to `alembic.ini` +#### Create migration +```console +alembic revision --autogenerate -m "message" +``` +#### Apply migration +```console +alembic upgrade head +``` + ## License `backend` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..91fd6de --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,147 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s +# Or organize into date-based subdirectories (requires recursive_version_locations = true) +# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. +prepend_sys_path = . + + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the tzdata library which can be installed by adding +# `alembic[tz]` to the pip requirements. +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to /versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "path_separator" +# below. +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# database URL. This is consumed by the user-maintained env.py script only. +# other means of configuring database URLs may be customized within the env.py +# file. + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module +# hooks = ruff +# ruff.type = module +# ruff.module = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Alternatively, use the exec runner to execute a binary found on your PATH +# hooks = ruff +# ruff.type = exec +# ruff.executable = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration. This is also consumed by the user-maintained +# env.py script only. +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/README b/backend/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..567f10a --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,81 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +from sqlmodel import SQLModel +from src.settings import settings + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = SQLModel.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +config.set_main_option("sqlalchemy.url", f'postgresql://{settings.db_user}:{settings.db_pass}@{settings.db_host}:5432/{settings.db_name}') +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..5008fb8 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,29 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} +import sqlmodel.sql.sqltypes + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/370137544ee6_initial_repository.py b/backend/alembic/versions/370137544ee6_initial_repository.py new file mode 100644 index 0000000..089e8f4 --- /dev/null +++ b/backend/alembic/versions/370137544ee6_initial_repository.py @@ -0,0 +1,155 @@ +"""Initial repository + +Revision ID: 370137544ee6 +Revises: +Create Date: 2026-02-19 22:55:19.804879 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +import sqlmodel.sql.sqltypes + +# revision identifiers, used by Alembic. +revision: str = '370137544ee6' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('template') + op.drop_table('user') + op.drop_table('form') + op.drop_table('usercontracttypelink') + op.drop_table('shipment') + op.drop_table('cheque') + op.drop_table('productor') + op.drop_table('paymentmethod') + op.drop_table('contract') + op.drop_table('contractproduct') + op.drop_table('contracttype') + op.drop_table('product') + op.drop_table('shipmentproductlink') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('shipmentproductlink', + sa.Column('shipment_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], name=op.f('shipmentproductlink_product_id_fkey')), + sa.ForeignKeyConstraint(['shipment_id'], ['shipment.id'], name=op.f('shipmentproductlink_shipment_id_fkey')), + sa.PrimaryKeyConstraint('shipment_id', 'product_id', name=op.f('shipmentproductlink_pkey')) + ) + op.create_table('product', + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('unit', postgresql.ENUM('GRAMS', 'KILO', 'PIECE', name='unit'), autoincrement=False, nullable=False), + sa.Column('price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('price_kg', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('quantity', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('quantity_unit', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column('type', postgresql.ENUM('OCCASIONAL', 'RECCURENT', name='producttype'), autoincrement=False, nullable=False), + sa.Column('productor_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], name=op.f('product_productor_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('product_pkey')) + ) + op.create_table('contracttype', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('contracttype_pkey')) + ) + op.create_table('contractproduct', + sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('shipment_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('quantity', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('contract_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['contract_id'], ['contract.id'], name=op.f('contractproduct_contract_id_fkey'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], name=op.f('contractproduct_product_id_fkey'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['shipment_id'], ['shipment.id'], name=op.f('contractproduct_shipment_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('contractproduct_pkey')) + ) + op.create_table('contract', + sa.Column('firstname', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('lastname', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('phone', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('payment_method', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('cheque_quantity', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('form_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('file', postgresql.BYTEA(), autoincrement=False, nullable=True), + sa.Column('total_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['form_id'], ['form.id'], name=op.f('contract_form_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('contract_pkey')) + ) + op.create_table('paymentmethod', + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('details', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('productor_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], name=op.f('paymentmethod_productor_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('paymentmethod_pkey')) + ) + op.create_table('productor', + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('address', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('productor_pkey')) + ) + op.create_table('cheque', + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('value', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('contract_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['contract_id'], ['contract.id'], name=op.f('cheque_contract_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('cheque_pkey')) + ) + op.create_table('shipment', + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('date', sa.DATE(), autoincrement=False, nullable=False), + sa.Column('form_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['form_id'], ['form.id'], name=op.f('shipment_form_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('shipment_pkey')) + ) + op.create_table('usercontracttypelink', + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('contract_type_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['contract_type_id'], ['contracttype.id'], name=op.f('usercontracttypelink_contract_type_id_fkey')), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('usercontracttypelink_user_id_fkey')), + sa.PrimaryKeyConstraint('user_id', 'contract_type_id', name=op.f('usercontracttypelink_pkey')) + ) + op.create_table('form', + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('productor_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('referer_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('season', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('start', sa.DATE(), autoincrement=False, nullable=False), + sa.Column('end', sa.DATE(), autoincrement=False, nullable=False), + sa.Column('minimum_shipment_value', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], name=op.f('form_productor_id_fkey')), + sa.ForeignKeyConstraint(['referer_id'], ['user.id'], name=op.f('form_referer_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('form_pkey')) + ) + op.create_table('user', + sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('user_pkey')) + ) + op.create_table('template', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('template_pkey')) + ) + # ### end Alembic commands ### diff --git a/backend/src/auth/auth.py b/backend/src/auth/auth.py index 02f2fc9..d092f7b 100644 --- a/backend/src/auth/auth.py +++ b/backend/src/auth/auth.py @@ -22,9 +22,7 @@ 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 +32,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 @@ -127,7 +119,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 +127,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,7 +135,7 @@ 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 ) @@ -152,15 +144,15 @@ def callback(code: str, session: Session = Depends(get_session)): 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) except jwt.InvalidTokenError: @@ -210,7 +202,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,7 +210,7 @@ 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', + samesite='strict', max_age=30 * 24 * settings.max_age ) response.set_cookie( @@ -226,7 +218,7 @@ def refresh_token(refresh_token: Annotated[str | None, Cookie()] = None): value=token_data['id_token'], httponly=True, secure=not settings.debug, - samesite='lax', + samesite='strict', max_age=settings.max_age ) return response diff --git a/backend/src/contracts/generate_contract.py b/backend/src/contracts/generate_contract.py index 9e27046..9f00540 100644 --- a/backend/src/contracts/generate_contract.py +++ b/backend/src/contracts/generate_contract.py @@ -5,6 +5,8 @@ import html from weasyprint import HTML import io +import pathlib + def generate_html_contract( contract: models.Contract, cheques: list[dict], @@ -13,7 +15,7 @@ def generate_html_contract( recurrent_price: float, total_price: float ): - template_dir = "./src/contracts/templates" + template_dir = pathlib.Path().resolve() + "/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" @@ -57,7 +59,8 @@ def generate_html_contract( return HTML( string=output_text, - base_url=template_dir + base_url=template_dir, + **options ).write_pdf() from odfdo import Document, Table, Row, Cell diff --git a/backend/src/main.py b/backend/src/main.py index e931423..5b1e638 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -12,7 +12,7 @@ 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.database import engine, create_all_tables app = FastAPI() @@ -37,4 +37,5 @@ app.include_router(users_router, prefix="/api") app.include_router(auth_router, prefix="/api") app.include_router(shipment_router, prefix="/api") -SQLModel.metadata.create_all(engine) \ No newline at end of file +if settings.debug == True: + create_all_tables() \ No newline at end of file diff --git a/backend/src/settings.py b/backend/src/settings.py index 22e703d..e646bbd 100644 --- a/backend/src/settings.py +++ b/backend/src/settings.py @@ -12,7 +12,6 @@ class Settings(BaseSettings): keycloak_client_id: str keycloak_client_secret: str keycloak_redirect_uri: str - vite_api_url: str max_age: int debug: bool diff --git a/amapcontract/bruno.json b/bruno/bruno.json similarity index 77% rename from amapcontract/bruno.json rename to bruno/bruno.json index b314673..b271425 100644 --- a/amapcontract/bruno.json +++ b/bruno/bruno.json @@ -1,6 +1,6 @@ { "version": "1", - "name": "amapcontract", + "name": "bruno", "type": "collection", "ignore": [ "node_modules", diff --git a/amapcontract/collection.bru b/bruno/collection.bru similarity index 100% rename from amapcontract/collection.bru rename to bruno/collection.bru diff --git a/amapcontract/forms/Create.bru b/bruno/forms/Create.bru similarity index 100% rename from amapcontract/forms/Create.bru rename to bruno/forms/Create.bru diff --git a/amapcontract/forms/Delete one.bru b/bruno/forms/Delete one.bru similarity index 100% rename from amapcontract/forms/Delete one.bru rename to bruno/forms/Delete one.bru diff --git a/amapcontract/forms/Get all.bru b/bruno/forms/Get all.bru similarity index 100% rename from amapcontract/forms/Get all.bru rename to bruno/forms/Get all.bru diff --git a/amapcontract/forms/Get one.bru b/bruno/forms/Get one.bru similarity index 100% rename from amapcontract/forms/Get one.bru rename to bruno/forms/Get one.bru diff --git a/amapcontract/forms/Update one.bru b/bruno/forms/Update one.bru similarity index 100% rename from amapcontract/forms/Update one.bru rename to bruno/forms/Update one.bru diff --git a/amapcontract/forms/folder.bru b/bruno/forms/folder.bru similarity index 100% rename from amapcontract/forms/folder.bru rename to bruno/forms/folder.bru diff --git a/amapcontract/productors/Create.bru b/bruno/productors/Create.bru similarity index 100% rename from amapcontract/productors/Create.bru rename to bruno/productors/Create.bru diff --git a/amapcontract/productors/Delete one.bru b/bruno/productors/Delete one.bru similarity index 100% rename from amapcontract/productors/Delete one.bru rename to bruno/productors/Delete one.bru diff --git a/amapcontract/productors/Get all.bru b/bruno/productors/Get all.bru similarity index 100% rename from amapcontract/productors/Get all.bru rename to bruno/productors/Get all.bru diff --git a/amapcontract/productors/Get one.bru b/bruno/productors/Get one.bru similarity index 100% rename from amapcontract/productors/Get one.bru rename to bruno/productors/Get one.bru diff --git a/amapcontract/productors/Update one.bru b/bruno/productors/Update one.bru similarity index 100% rename from amapcontract/productors/Update one.bru rename to bruno/productors/Update one.bru diff --git a/amapcontract/productors/folder.bru b/bruno/productors/folder.bru similarity index 100% rename from amapcontract/productors/folder.bru rename to bruno/productors/folder.bru diff --git a/amapcontract/products/Create.bru b/bruno/products/Create.bru similarity index 100% rename from amapcontract/products/Create.bru rename to bruno/products/Create.bru diff --git a/amapcontract/products/Delete one.bru b/bruno/products/Delete one.bru similarity index 100% rename from amapcontract/products/Delete one.bru rename to bruno/products/Delete one.bru diff --git a/amapcontract/products/Get all.bru b/bruno/products/Get all.bru similarity index 100% rename from amapcontract/products/Get all.bru rename to bruno/products/Get all.bru diff --git a/amapcontract/products/Get one.bru b/bruno/products/Get one.bru similarity index 100% rename from amapcontract/products/Get one.bru rename to bruno/products/Get one.bru diff --git a/amapcontract/products/Update one.bru b/bruno/products/Update one.bru similarity index 100% rename from amapcontract/products/Update one.bru rename to bruno/products/Update one.bru diff --git a/amapcontract/products/folder.bru b/bruno/products/folder.bru similarity index 100% rename from amapcontract/products/folder.bru rename to bruno/products/folder.bru diff --git a/amapcontract/shipments/Create.bru b/bruno/shipments/Create.bru similarity index 100% rename from amapcontract/shipments/Create.bru rename to bruno/shipments/Create.bru diff --git a/amapcontract/shipments/Delete one.bru b/bruno/shipments/Delete one.bru similarity index 100% rename from amapcontract/shipments/Delete one.bru rename to bruno/shipments/Delete one.bru diff --git a/amapcontract/shipments/Get all.bru b/bruno/shipments/Get all.bru similarity index 100% rename from amapcontract/shipments/Get all.bru rename to bruno/shipments/Get all.bru diff --git a/amapcontract/shipments/Get one.bru b/bruno/shipments/Get one.bru similarity index 100% rename from amapcontract/shipments/Get one.bru rename to bruno/shipments/Get one.bru diff --git a/amapcontract/shipments/Update one.bru b/bruno/shipments/Update one.bru similarity index 100% rename from amapcontract/shipments/Update one.bru rename to bruno/shipments/Update one.bru diff --git a/amapcontract/shipments/folder.bru b/bruno/shipments/folder.bru similarity index 100% rename from amapcontract/shipments/folder.bru rename to bruno/shipments/folder.bru diff --git a/amapcontract/users/Create.bru b/bruno/users/Create.bru similarity index 100% rename from amapcontract/users/Create.bru rename to bruno/users/Create.bru diff --git a/amapcontract/users/Delete one.bru b/bruno/users/Delete one.bru similarity index 100% rename from amapcontract/users/Delete one.bru rename to bruno/users/Delete one.bru diff --git a/amapcontract/users/Get all.bru b/bruno/users/Get all.bru similarity index 100% rename from amapcontract/users/Get all.bru rename to bruno/users/Get all.bru diff --git a/amapcontract/users/Get one.bru b/bruno/users/Get one.bru similarity index 100% rename from amapcontract/users/Get one.bru rename to bruno/users/Get one.bru diff --git a/amapcontract/users/Update one.bru b/bruno/users/Update one.bru similarity index 100% rename from amapcontract/users/Update one.bru rename to bruno/users/Update one.bru diff --git a/amapcontract/users/folder.bru b/bruno/users/folder.bru similarity index 100% rename from amapcontract/users/folder.bru rename to bruno/users/folder.bru diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..5ff3599 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,55 @@ +services: + frontend: + build: + context: . + dockerfile: frontend/Dockerfile.dev + volumes: + - ./frontend:/app + - /app/node_modules + environment: + VITE_API_URL: ${VITE_API_URL} + ports: + - "5173:5173" + depends_on: + - backend + backend: + build: + context: . + dockerfile: backend/Dockerfile + volumes: + - ./backend:/code/app + command: > + sh -c "fastapi run app/src/main.py --reload --port 8000" + environment: + ORIGINS: ${ORIGINS} + DB_HOST: database + DB_USER: ${DB_USER} + DB_PASS: ${DB_PASS} + DB_NAME: ${DB_NAME} + SECRET_KEY: ${SECRET_KEY} + KEYCLOAK_SERVER: ${KEYCLOAK_SERVER} + KEYCLOAK_REALM: ${KEYCLOAK_REALM} + KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET} + KEYCLOAK_REDIRECT_URI: ${KEYCLOAK_REDIRECT_URI} + DEBUG: ${DEBUG} + MAX_AGE: ${MAX_AGE} + ports: + - "8000:8000" + depends_on: + - database + + database: + image: postgres + restart: always + shm_size: 128mb + volumes: + - db:/var/lib/postgresql + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASS} + POSTGRES_DB: ${DB_NAME} + ports: + - 5432:5432 +volumes: + db: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 0ca4f3d..e75faba 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,8 +9,8 @@ services: ports: - 80:80 depends_on: - - back - back: + - backend + backend: build: context: . dockerfile: backend/Dockerfile @@ -22,7 +22,6 @@ services: DB_PASS: ${DB_PASS} DB_NAME: ${DB_NAME} SECRET_KEY: ${SECRET_KEY} - VITE_API_URL: ${VITE_API_URL} KEYCLOAK_SERVER: ${KEYCLOAK_SERVER} KEYCLOAK_REALM: ${KEYCLOAK_REALM} KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} @@ -42,5 +41,7 @@ services: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_DB: ${DB_NAME} + ports: + - 5432:5432 volumes: db: \ No newline at end of file diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..cc35bb2 --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,12 @@ +FROM node:20 AS dev + +WORKDIR /app + +COPY frontend/package.json ./ +COPY frontend/package-lock.json ./ + +RUN npm install + +COPY frontend . + +CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file From a854fc028ef847fe90199ffe1a2988af85dfe8d6 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Thu, 19 Feb 2026 23:50:00 +0100 Subject: [PATCH 04/56] fix generate contract path and add alembic to dependencies --- backend/requirements.txt | 1 + backend/src/contracts/generate_contract.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 7cad117..68f36e9 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -63,3 +63,4 @@ weasyprint==68.1 webencodings==0.5.1 websockets==16.0 zopfli==0.4.1 +alembic==1.18.4 \ No newline at end of file diff --git a/backend/src/contracts/generate_contract.py b/backend/src/contracts/generate_contract.py index 9f00540..aa4478b 100644 --- a/backend/src/contracts/generate_contract.py +++ b/backend/src/contracts/generate_contract.py @@ -15,7 +15,7 @@ def generate_html_contract( recurrent_price: float, total_price: float ): - template_dir = pathlib.Path().resolve() + "/src/contracts/templates" + template_dir = pathlib.Path("./src/contracts/templates").resolve() template_loader = jinja2.FileSystemLoader(searchpath=template_dir) template_env = jinja2.Environment(loader=template_loader, autoescape=jinja2.select_autoescape(["html", "xml"])) template_file = "layout.html" From f6101a251a5924f854994747afedb2ff3cbeef36 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Thu, 19 Feb 2026 23:55:39 +0100 Subject: [PATCH 05/56] fix app dockerfile --- backend/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 15938b7..23bf7d5 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -8,6 +8,6 @@ COPY ./backend/requirements.txt /code/requirements.txt RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt -COPY ./backend /code/app +COPY ./backend /code -CMD ["fastapi", "run", "app/src/main.py", "--port", "8000"] \ No newline at end of file +CMD ["fastapi", "run", "src/main.py", "--port", "8000"] \ No newline at end of file From a16c940452e9e8c815684989e462a03c6bc41220 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 00:10:36 +0100 Subject: [PATCH 06/56] fix alembic first migration --- backend/README.md | 1 - backend/alembic/env.py | 2 +- .../370137544ee6_initial_repository.py | 155 ------------------ .../c0b1073a8394_initial_repository.py | 155 ++++++++++++++++++ backend/src/main.py | 5 +- backend/src/settings.py | 1 + docker-compose.dev.yaml | 5 +- docker-compose.yaml | 3 +- 8 files changed, 163 insertions(+), 164 deletions(-) delete mode 100644 backend/alembic/versions/370137544ee6_initial_repository.py create mode 100644 backend/alembic/versions/c0b1073a8394_initial_repository.py diff --git a/backend/README.md b/backend/README.md index a017a4d..286c544 100644 --- a/backend/README.md +++ b/backend/README.md @@ -21,7 +21,6 @@ fastapi dev src/main.py ### Migration This repository use `alembic` for migrations -On first installation copy the `alembic.ini.example` to `alembic.ini` #### Create migration ```console alembic revision --autogenerate -m "message" diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 567f10a..7c92e0d 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -7,7 +7,7 @@ from alembic import context from sqlmodel import SQLModel from src.settings import settings - +from src.models import * # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config diff --git a/backend/alembic/versions/370137544ee6_initial_repository.py b/backend/alembic/versions/370137544ee6_initial_repository.py deleted file mode 100644 index 089e8f4..0000000 --- a/backend/alembic/versions/370137544ee6_initial_repository.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Initial repository - -Revision ID: 370137544ee6 -Revises: -Create Date: 2026-02-19 22:55:19.804879 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql -import sqlmodel.sql.sqltypes - -# revision identifiers, used by Alembic. -revision: str = '370137544ee6' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('template') - op.drop_table('user') - op.drop_table('form') - op.drop_table('usercontracttypelink') - op.drop_table('shipment') - op.drop_table('cheque') - op.drop_table('productor') - op.drop_table('paymentmethod') - op.drop_table('contract') - op.drop_table('contractproduct') - op.drop_table('contracttype') - op.drop_table('product') - op.drop_table('shipmentproductlink') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('shipmentproductlink', - sa.Column('shipment_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['product_id'], ['product.id'], name=op.f('shipmentproductlink_product_id_fkey')), - sa.ForeignKeyConstraint(['shipment_id'], ['shipment.id'], name=op.f('shipmentproductlink_shipment_id_fkey')), - sa.PrimaryKeyConstraint('shipment_id', 'product_id', name=op.f('shipmentproductlink_pkey')) - ) - op.create_table('product', - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('unit', postgresql.ENUM('GRAMS', 'KILO', 'PIECE', name='unit'), autoincrement=False, nullable=False), - sa.Column('price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('price_kg', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('quantity', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('quantity_unit', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('type', postgresql.ENUM('OCCASIONAL', 'RECCURENT', name='producttype'), autoincrement=False, nullable=False), - sa.Column('productor_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], name=op.f('product_productor_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('product_pkey')) - ) - op.create_table('contracttype', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('contracttype_pkey')) - ) - op.create_table('contractproduct', - sa.Column('product_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('shipment_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('quantity', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('contract_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['contract_id'], ['contract.id'], name=op.f('contractproduct_contract_id_fkey'), ondelete='CASCADE'), - sa.ForeignKeyConstraint(['product_id'], ['product.id'], name=op.f('contractproduct_product_id_fkey'), ondelete='CASCADE'), - sa.ForeignKeyConstraint(['shipment_id'], ['shipment.id'], name=op.f('contractproduct_shipment_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('contractproduct_pkey')) - ) - op.create_table('contract', - sa.Column('firstname', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('lastname', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('phone', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('payment_method', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('cheque_quantity', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('form_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('file', postgresql.BYTEA(), autoincrement=False, nullable=True), - sa.Column('total_price', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['form_id'], ['form.id'], name=op.f('contract_form_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('contract_pkey')) - ) - op.create_table('paymentmethod', - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('details', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('productor_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], name=op.f('paymentmethod_productor_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('paymentmethod_pkey')) - ) - op.create_table('productor', - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('address', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('productor_pkey')) - ) - op.create_table('cheque', - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('value', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('contract_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['contract_id'], ['contract.id'], name=op.f('cheque_contract_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('cheque_pkey')) - ) - op.create_table('shipment', - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('date', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('form_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.ForeignKeyConstraint(['form_id'], ['form.id'], name=op.f('shipment_form_id_fkey'), ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id', name=op.f('shipment_pkey')) - ) - op.create_table('usercontracttypelink', - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('contract_type_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint(['contract_type_id'], ['contracttype.id'], name=op.f('usercontracttypelink_contract_type_id_fkey')), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('usercontracttypelink_user_id_fkey')), - sa.PrimaryKeyConstraint('user_id', 'contract_type_id', name=op.f('usercontracttypelink_pkey')) - ) - op.create_table('form', - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('productor_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('referer_id', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('season', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('start', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('end', sa.DATE(), autoincrement=False, nullable=False), - sa.Column('minimum_shipment_value', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], name=op.f('form_productor_id_fkey')), - sa.ForeignKeyConstraint(['referer_id'], ['user.id'], name=op.f('form_referer_id_fkey')), - sa.PrimaryKeyConstraint('id', name=op.f('form_pkey')) - ) - op.create_table('user', - sa.Column('name', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('email', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('user_pkey')) - ) - op.create_table('template', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('template_pkey')) - ) - # ### end Alembic commands ### diff --git a/backend/alembic/versions/c0b1073a8394_initial_repository.py b/backend/alembic/versions/c0b1073a8394_initial_repository.py new file mode 100644 index 0000000..0d280fb --- /dev/null +++ b/backend/alembic/versions/c0b1073a8394_initial_repository.py @@ -0,0 +1,155 @@ +"""Initial repository + +Revision ID: c0b1073a8394 +Revises: +Create Date: 2026-02-20 00:09:35.920486 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +import sqlmodel.sql.sqltypes + +# revision identifiers, used by Alembic. +revision: str = 'c0b1073a8394' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('contracttype', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('productor', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('address', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('type', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('template', + sa.Column('id', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('form', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('productor_id', sa.Integer(), nullable=True), + sa.Column('referer_id', sa.Integer(), nullable=True), + sa.Column('season', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('start', sa.Date(), nullable=False), + sa.Column('end', sa.Date(), nullable=False), + sa.Column('minimum_shipment_value', sa.Float(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], ), + sa.ForeignKeyConstraint(['referer_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('paymentmethod', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('details', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('productor_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('product', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('unit', sa.Enum('GRAMS', 'KILO', 'PIECE', name='unit'), nullable=False), + sa.Column('price', sa.Float(), nullable=True), + sa.Column('price_kg', sa.Float(), nullable=True), + sa.Column('quantity', sa.Float(), nullable=True), + sa.Column('quantity_unit', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('type', sa.Enum('OCCASIONAL', 'RECCURENT', name='producttype'), nullable=False), + sa.Column('productor_id', sa.Integer(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['productor_id'], ['productor.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('usercontracttypelink', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('contract_type_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['contract_type_id'], ['contracttype.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('user_id', 'contract_type_id') + ) + op.create_table('contract', + sa.Column('firstname', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('lastname', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('email', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('phone', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('payment_method', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('cheque_quantity', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('form_id', sa.Integer(), nullable=False), + sa.Column('file', sa.LargeBinary(), nullable=True), + sa.Column('total_price', sa.Float(), nullable=True), + sa.ForeignKeyConstraint(['form_id'], ['form.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('shipment', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('form_id', sa.Integer(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['form_id'], ['form.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('cheque', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('value', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('contract_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['contract_id'], ['contract.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('contractproduct', + sa.Column('product_id', sa.Integer(), nullable=False), + sa.Column('shipment_id', sa.Integer(), nullable=True), + sa.Column('quantity', sa.Float(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('contract_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['contract_id'], ['contract.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['shipment_id'], ['shipment.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('shipmentproductlink', + sa.Column('shipment_id', sa.Integer(), nullable=False), + sa.Column('product_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['product_id'], ['product.id'], ), + sa.ForeignKeyConstraint(['shipment_id'], ['shipment.id'], ), + sa.PrimaryKeyConstraint('shipment_id', 'product_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('shipmentproductlink') + op.drop_table('contractproduct') + op.drop_table('cheque') + op.drop_table('shipment') + op.drop_table('contract') + op.drop_table('usercontracttypelink') + op.drop_table('product') + op.drop_table('paymentmethod') + op.drop_table('form') + op.drop_table('user') + op.drop_table('template') + op.drop_table('productor') + op.drop_table('contracttype') + # ### end Alembic commands ### diff --git a/backend/src/main.py b/backend/src/main.py index 5b1e638..9de6475 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -35,7 +35,4 @@ 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") - -if settings.debug == True: - create_all_tables() \ No newline at end of file +app.include_router(shipment_router, prefix="/api") \ No newline at end of file diff --git a/backend/src/settings.py b/backend/src/settings.py index e646bbd..22e703d 100644 --- a/backend/src/settings.py +++ b/backend/src/settings.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): keycloak_client_id: str keycloak_client_secret: str keycloak_redirect_uri: str + vite_api_url: str max_age: int debug: bool diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 5ff3599..c07e388 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -17,9 +17,9 @@ services: context: . dockerfile: backend/Dockerfile volumes: - - ./backend:/code/app + - ./backend:/code command: > - sh -c "fastapi run app/src/main.py --reload --port 8000" + sh -c "fastapi run src/main.py --reload --port 8000" environment: ORIGINS: ${ORIGINS} DB_HOST: database @@ -27,6 +27,7 @@ services: DB_PASS: ${DB_PASS} DB_NAME: ${DB_NAME} SECRET_KEY: ${SECRET_KEY} + VITE_API_URL: ${VITE_API_URL} KEYCLOAK_SERVER: ${KEYCLOAK_SERVER} KEYCLOAK_REALM: ${KEYCLOAK_REALM} KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} diff --git a/docker-compose.yaml b/docker-compose.yaml index e75faba..ad4f6de 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,6 +22,7 @@ services: DB_PASS: ${DB_PASS} DB_NAME: ${DB_NAME} SECRET_KEY: ${SECRET_KEY} + VITE_API_URL: ${VITE_API_URL} KEYCLOAK_SERVER: ${KEYCLOAK_SERVER} KEYCLOAK_REALM: ${KEYCLOAK_REALM} KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} @@ -41,7 +42,7 @@ services: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_DB: ${DB_NAME} - ports: + ports: - 5432:5432 volumes: db: \ No newline at end of file From b52ac35593dab34fc155a06ced3bda354060f6cd Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 00:11:37 +0100 Subject: [PATCH 07/56] fix dockercompose --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index ad4f6de..30baf72 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -42,7 +42,7 @@ services: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASS} POSTGRES_DB: ${DB_NAME} - ports: + ports: - 5432:5432 volumes: db: \ No newline at end of file From 63ea2ff806866294799d45bdd78a22fb5889b27a Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 00:14:51 +0100 Subject: [PATCH 08/56] fix nginx internal rooting --- frontend/nginx/default.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf index 5785728..656b791 100644 --- a/frontend/nginx/default.conf +++ b/frontend/nginx/default.conf @@ -17,10 +17,10 @@ server { proxy_redirect off; proxy_buffering off; - proxy_pass http://back; + proxy_pass http://backend; } } upstream backend { - server back:8000; + server backend:8000; } \ No newline at end of file From e478b26943a259d86813858aa3e9188351cce725 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 00:30:10 +0100 Subject: [PATCH 09/56] fix nginx conf --- frontend/nginx/default.conf | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf index 656b791..c56f1c1 100644 --- a/frontend/nginx/default.conf +++ b/frontend/nginx/default.conf @@ -6,10 +6,6 @@ server { root /srv/www/frontend; index index.html; - location / { - try_files $uri /index.html; - } - location /api/ { proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -17,10 +13,10 @@ server { proxy_redirect off; proxy_buffering off; - proxy_pass http://backend; + proxy_pass http://backend:8000/; } -} -upstream backend { - server backend:8000; + location / { + try_files $uri /index.html; + } } \ No newline at end of file From 4fbc1e66211d7d82ac1ea7781665562cc9760290 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 00:40:42 +0100 Subject: [PATCH 10/56] fix nginx --- frontend/nginx/default.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf index c56f1c1..1500375 100644 --- a/frontend/nginx/default.conf +++ b/frontend/nginx/default.conf @@ -13,7 +13,7 @@ server { proxy_redirect off; proxy_buffering off; - proxy_pass http://backend:8000/; + proxy_pass http://backend:8000/api/; } location / { From eb19efe2257317d7d5bf5c6e6ac25a795dfcb5bb Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 00:48:39 +0100 Subject: [PATCH 11/56] fix nginx --- frontend/nginx/default.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf index 1500375..e817159 100644 --- a/frontend/nginx/default.conf +++ b/frontend/nginx/default.conf @@ -6,7 +6,7 @@ server { root /srv/www/frontend; index index.html; - location /api/ { + location /api { proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; From 63934e6287a8f4ccb791c53ce879e44c9fa1ca55 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 00:52:57 +0100 Subject: [PATCH 12/56] fix nginx --- frontend/nginx/default.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/nginx/default.conf b/frontend/nginx/default.conf index e817159..9fcd03d 100644 --- a/frontend/nginx/default.conf +++ b/frontend/nginx/default.conf @@ -6,14 +6,14 @@ server { root /srv/www/frontend; index index.html; - location /api { + location /api/ { proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; proxy_buffering off; - proxy_pass http://backend:8000/api/; + proxy_pass http://backend:8000; } location / { From 53490c86f0d2234efbfdbc4abbb3b1743ae7e558 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 01:09:12 +0100 Subject: [PATCH 13/56] fix path --- backend/src/main.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/src/main.py b/backend/src/main.py index 9de6475..51258cc 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -28,11 +28,11 @@ app.add_middleware( ) -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") \ No newline at end of file +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) \ No newline at end of file From a530e711039c24e7a4050b946b26461f4de9f3bd Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 01:12:23 +0100 Subject: [PATCH 14/56] add dockerfile --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 23bf7d5..ab60b30 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,4 +10,4 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./backend /code -CMD ["fastapi", "run", "src/main.py", "--port", "8000"] \ No newline at end of file +CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--root-path", "/api", "--forwarded-allow-ip='*'"] \ No newline at end of file From 26f087ea8b4dd84584e4e8b22f36264965352228 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 01:13:48 +0100 Subject: [PATCH 15/56] fix typo --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index ab60b30..eab00a6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,4 +10,4 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./backend /code -CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--root-path", "/api", "--forwarded-allow-ip='*'"] \ No newline at end of file +CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--root-path", "/api", "--forwarded-allow-ips='*'"] \ No newline at end of file From 0ff2d4bb01b6fbd89013b46842956ad84e1b64d1 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 01:36:22 +0100 Subject: [PATCH 16/56] fix dockerfile --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index eab00a6..7537909 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,4 +10,4 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./backend /code -CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--root-path", "/api", "--forwarded-allow-ips='*'"] \ No newline at end of file +CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--forwarded-allow-ips='*'"] \ No newline at end of file From 9a7217a54edfea8ec8069ab8cbfa287763569a77 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 01:38:17 +0100 Subject: [PATCH 17/56] fix prefix --- backend/src/main.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/src/main.py b/backend/src/main.py index 51258cc..9de6475 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -28,11 +28,11 @@ app.add_middleware( ) -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) \ No newline at end of file +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") \ No newline at end of file From 754922cbf0529a8d7a3a6323d41bdb466b3d6a34 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 01:43:07 +0100 Subject: [PATCH 18/56] fix dockerfile --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 7537909..d4bc8b3 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,4 +10,4 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./backend /code -CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--forwarded-allow-ips='*'"] \ No newline at end of file +CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--forwarded-allow-ips", "*"] \ No newline at end of file From c67e59d5db0baccd26b189d2bd54ebf6512f48b7 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 01:48:34 +0100 Subject: [PATCH 19/56] fix dockerfile --- backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index d4bc8b3..596a573 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -10,4 +10,4 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./backend /code -CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--forwarded-allow-ips", "*"] \ No newline at end of file +CMD ["fastapi", "run", "src/main.py", "--port", "8000", "--forwarded-allow-ips", "*", "--proxy-headers"] \ No newline at end of file From b05276eeebcb751466e2907efabb0da4e7407d14 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 01:59:33 +0100 Subject: [PATCH 20/56] remove trailing --- backend/src/contracts/contracts.py | 14 +++++++------- backend/src/forms/forms.py | 10 +++++----- backend/src/main.py | 3 +-- backend/src/productors/productors.py | 10 +++++----- backend/src/products/products.py | 10 +++++----- backend/src/shipments/shipments.py | 10 +++++----- backend/src/templates/templates.py | 10 +++++----- backend/src/users/users.py | 12 ++++++------ 8 files changed, 39 insertions(+), 40 deletions(-) diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index 62f2ea2..e490a5f 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -69,7 +69,7 @@ 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), @@ -107,7 +107,7 @@ async def create_contract( } ) -@router.get('/', response_model=list[models.ContractPublic]) +@router.get('', response_model=list[models.ContractPublic]) def get_contracts( forms: list[str] = Query([]), session: Session = Depends(get_session), @@ -115,7 +115,7 @@ def get_contracts( ): return service.get_all(session, user, forms) -@router.get('/{id}/file') +@router.get('{id}/file') def get_contract_file( id: int, session: Session = Depends(get_session), @@ -135,7 +135,7 @@ def get_contract_file( } ) -@router.get('/{form_id}/files') +@router.get('{form_id}/files') def get_contract_files( form_id: int, session: Session = Depends(get_session), @@ -160,7 +160,7 @@ def get_contract_files( } ) -@router.get('/{form_id}/recap') +@router.get('{form_id}/recap') def get_contract_recap( form_id: int, session: Session = Depends(get_session), @@ -179,7 +179,7 @@ def get_contract_recap( } ) -@router.get('/{id}', response_model=models.ContractPublic) +@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) @@ -188,7 +188,7 @@ def get_contract(id: int, session: Session = Depends(get_session), user: models. raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('/{id}', response_model=models.ContractPublic) +@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) diff --git a/backend/src/forms/forms.py b/backend/src/forms/forms.py index 03c65da..d527491 100644 --- a/backend/src/forms/forms.py +++ b/backend/src/forms/forms.py @@ -8,7 +8,7 @@ from src.auth.auth import get_current_user 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([]), @@ -17,14 +17,14 @@ async def get_forms( ): return service.get_all(session, seasons, productors, current_season) -@router.get('/{id}', response_model=models.FormPublic) +@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) return result -@router.post('/', response_model=models.FormPublic) +@router.post('', response_model=models.FormPublic) async def create_form( form: models.FormCreate, user: models.User = Depends(get_current_user), @@ -32,7 +32,7 @@ async def create_form( ): return service.create_one(session, form) -@router.put('/{id}', response_model=models.FormPublic) +@router.put('{id}', response_model=models.FormPublic) async def update_form( id: int, form: models.FormUpdate, user: models.User = Depends(get_current_user), @@ -43,7 +43,7 @@ async def update_form( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('/{id}', response_model=models.FormPublic) +@router.delete('{id}', response_model=models.FormPublic) async def delete_form( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/main.py b/backend/src/main.py index 9de6475..7b91e07 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -13,9 +13,9 @@ 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, create_all_tables - app = FastAPI() + app.add_middleware( CORSMiddleware, allow_origins=[ @@ -27,7 +27,6 @@ app.add_middleware( expose_headers=['x-nbpage', 'Content-Disposition'] ) - app.include_router(template_router, prefix="/api") app.include_router(contracts_router, prefix="/api") app.include_router(forms_router, prefix="/api") diff --git a/backend/src/productors/productors.py b/backend/src/productors/productors.py index 3753720..f2058da 100644 --- a/backend/src/productors/productors.py +++ b/backend/src/productors/productors.py @@ -8,7 +8,7 @@ from src.auth.auth import get_current_user 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([]), @@ -17,7 +17,7 @@ def get_productors( ): return service.get_all(session, names, types) -@router.get('/{id}', response_model=models.ProductorPublic) +@router.get('{id}', response_model=models.ProductorPublic) def get_productor( id: int, user: models.User = Depends(get_current_user), @@ -28,7 +28,7 @@ def get_productor( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.post('/', response_model=models.ProductorPublic) +@router.post('', response_model=models.ProductorPublic) def create_productor( productor: models.ProductorCreate, user: models.User = Depends(get_current_user), @@ -36,7 +36,7 @@ def create_productor( ): return service.create_one(session, productor) -@router.put('/{id}', response_model=models.ProductorPublic) +@router.put('{id}', response_model=models.ProductorPublic) def update_productor( id: int, productor: models.ProductorUpdate, user: models.User = Depends(get_current_user), @@ -47,7 +47,7 @@ def update_productor( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('/{id}', response_model=models.ProductorPublic) +@router.delete('{id}', response_model=models.ProductorPublic) def delete_productor( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/products/products.py b/backend/src/products/products.py index 4de8f1c..e7c7e8c 100644 --- a/backend/src/products/products.py +++ b/backend/src/products/products.py @@ -7,7 +7,7 @@ import src.products.service as service from src.auth.auth import get_current_user 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), @@ -22,7 +22,7 @@ def get_products( types, ) -@router.get('/{id}', response_model=models.ProductPublic) +@router.get('{id}', response_model=models.ProductPublic) def get_product( id: int, user: models.User = Depends(get_current_user), @@ -33,7 +33,7 @@ def get_product( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.post('/', response_model=models.ProductPublic) +@router.post('', response_model=models.ProductPublic) def create_product( product: models.ProductCreate, user: models.User = Depends(get_current_user), @@ -41,7 +41,7 @@ def create_product( ): return service.create_one(session, product) -@router.put('/{id}', response_model=models.ProductPublic) +@router.put('{id}', response_model=models.ProductPublic) def update_product( id: int, product: models.ProductUpdate, user: models.User = Depends(get_current_user), @@ -52,7 +52,7 @@ def update_product( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('/{id}', response_model=models.ProductPublic) +@router.delete('{id}', response_model=models.ProductPublic) def delete_product( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/shipments/shipments.py b/backend/src/shipments/shipments.py index e3b5d69..ee47a34 100644 --- a/backend/src/shipments/shipments.py +++ b/backend/src/shipments/shipments.py @@ -8,7 +8,7 @@ from src.auth.auth import get_current_user 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), names: list[str] = Query([]), @@ -22,7 +22,7 @@ def get_shipments( forms, ) -@router.get('/{id}', response_model=models.ShipmentPublic) +@router.get('{id}', response_model=models.ShipmentPublic) def get_shipment( id: int, user: models.User = Depends(get_current_user), @@ -33,7 +33,7 @@ def get_shipment( raise HTTPException(status_code=404, detail=messages.notfound) 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), @@ -41,7 +41,7 @@ def create_shipment( ): return service.create_one(session, shipment) -@router.put('/{id}', response_model=models.ShipmentPublic) +@router.put('{id}', response_model=models.ShipmentPublic) def update_shipment( id: int, shipment: models.ShipmentUpdate, user: models.User = Depends(get_current_user), @@ -52,7 +52,7 @@ def update_shipment( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('/{id}', response_model=models.ShipmentPublic) +@router.delete('{id}', response_model=models.ShipmentPublic) def delete_shipment( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/templates/templates.py b/backend/src/templates/templates.py index c18baca..eb35873 100644 --- a/backend/src/templates/templates.py +++ b/backend/src/templates/templates.py @@ -8,14 +8,14 @@ from src.auth.auth import get_current_user 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) +@router.get('{id}', response_model=models.TemplatePublic) def get_template( id: int, user: models.User = Depends(get_current_user), @@ -26,7 +26,7 @@ def get_template( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.post('/', response_model=models.TemplatePublic) +@router.post('', response_model=models.TemplatePublic) def create_template( template: models.TemplateCreate, user: models.User = Depends(get_current_user), @@ -34,7 +34,7 @@ def create_template( ): return service.create_one(session, template) -@router.put('/{id}', response_model=models.TemplatePublic) +@router.put('{id}', response_model=models.TemplatePublic) def update_template( id: int, template: models.TemplateUpdate, user: models.User = Depends(get_current_user), @@ -45,7 +45,7 @@ def update_template( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('/{id}', response_model=models.TemplatePublic) +@router.delete('{id}', response_model=models.TemplatePublic) def delete_template( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/users/users.py b/backend/src/users/users.py index 0e3421e..79a66e2 100644 --- a/backend/src/users/users.py +++ b/backend/src/users/users.py @@ -8,7 +8,7 @@ from src.auth.auth import get_current_user 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,14 +21,14 @@ def get_users( emails, ) -@router.get('/roles', response_model=list[models.ContractType]) +@router.get('roles', response_model=list[models.ContractType]) def get_roles( user: models.User = Depends(get_current_user), session: Session = Depends(get_session) ): return service.get_roles(session) -@router.get('/{id}', response_model=models.UserPublic) +@router.get('{id}', response_model=models.UserPublic) def get_users( id: int, user: models.User = Depends(get_current_user), @@ -39,7 +39,7 @@ def get_users( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.post('/', response_model=models.UserPublic) +@router.post('', response_model=models.UserPublic) def create_user( user: models.UserCreate, logged_user: models.User = Depends(get_current_user), @@ -47,7 +47,7 @@ def create_user( ): return service.create_one(session, user) -@router.put('/{id}', response_model=models.UserPublic) +@router.put('{id}', response_model=models.UserPublic) def update_user( id: int, user: models.UserUpdate, @@ -59,7 +59,7 @@ def update_user( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('/{id}', response_model=models.UserPublic) +@router.delete('{id}', response_model=models.UserPublic) def delete_user( id: int, user: models.User = Depends(get_current_user), From 64060090386c8d8251d553794739a2a3da1c29ba Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 02:05:43 +0100 Subject: [PATCH 21/56] add checks --- frontend/src/pages/Productors/index.tsx | 2 +- frontend/src/pages/Users/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Productors/index.tsx b/frontend/src/pages/Productors/index.tsx index 19e8ef5..7a664d5 100644 --- a/frontend/src/pages/Productors/index.tsx +++ b/frontend/src/pages/Productors/index.tsx @@ -146,7 +146,7 @@ export default function Productors() { - {productors.map((productor) => ( + {productors?.map((productor) => ( ))} diff --git a/frontend/src/pages/Users/index.tsx b/frontend/src/pages/Users/index.tsx index 6bedaf8..b9c4d45 100644 --- a/frontend/src/pages/Users/index.tsx +++ b/frontend/src/pages/Users/index.tsx @@ -132,7 +132,7 @@ export default function Users() { - {users.map((user) => ( + {users?.map((user) => ( ))} From e37fae439f51d344dce8e71e6de5cf31a8f60f69 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 02:07:48 +0100 Subject: [PATCH 22/56] fix role route --- backend/src/users/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/users/users.py b/backend/src/users/users.py index 79a66e2..b599afe 100644 --- a/backend/src/users/users.py +++ b/backend/src/users/users.py @@ -21,7 +21,7 @@ def get_users( emails, ) -@router.get('roles', response_model=list[models.ContractType]) +@router.get('/roles', response_model=list[models.ContractType]) def get_roles( user: models.User = Depends(get_current_user), session: Session = Depends(get_session) From 7a94f1c96a90f062131b0f43610451f1a02bdd52 Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 10:33:00 +0100 Subject: [PATCH 23/56] fix app crash --- backend/pyproject.toml | 3 ++- backend/src/contracts/contracts.py | 10 +++++----- backend/src/forms/forms.py | 6 +++--- backend/src/productors/productors.py | 6 +++--- backend/src/products/products.py | 8 ++++---- backend/src/shipments/shipments.py | 6 +++--- backend/src/templates/templates.py | 6 +++--- backend/src/users/users.py | 6 +++--- docker-compose.dev.yaml | 2 +- frontend/src/components/Navbar/index.tsx | 6 +++--- frontend/src/components/Products/Modal/index.tsx | 2 +- 11 files changed, 31 insertions(+), 30 deletions(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1e833d1..7af776e 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -29,7 +29,8 @@ dependencies = [ "cryptography", "requests", "weasyprint", - "odfdo" + "odfdo", + "alembic" ] [project.urls] diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index e490a5f..5eab4b9 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -115,7 +115,7 @@ def get_contracts( ): return service.get_all(session, user, forms) -@router.get('{id}/file') +@router.get('/{id}/file') def get_contract_file( id: int, session: Session = Depends(get_session), @@ -135,7 +135,7 @@ def get_contract_file( } ) -@router.get('{form_id}/files') +@router.get('/{form_id}/files') def get_contract_files( form_id: int, session: Session = Depends(get_session), @@ -160,7 +160,7 @@ def get_contract_files( } ) -@router.get('{form_id}/recap') +@router.get('/{form_id}/recap') def get_contract_recap( form_id: int, session: Session = Depends(get_session), @@ -179,7 +179,7 @@ def get_contract_recap( } ) -@router.get('{id}', response_model=models.ContractPublic) +@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) @@ -188,7 +188,7 @@ def get_contract(id: int, session: Session = Depends(get_session), user: models. raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('{id}', response_model=models.ContractPublic) +@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) diff --git a/backend/src/forms/forms.py b/backend/src/forms/forms.py index d527491..a30eafc 100644 --- a/backend/src/forms/forms.py +++ b/backend/src/forms/forms.py @@ -17,7 +17,7 @@ async def get_forms( ): return service.get_all(session, seasons, productors, current_season) -@router.get('{id}', response_model=models.FormPublic) +@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: @@ -32,7 +32,7 @@ async def create_form( ): return service.create_one(session, form) -@router.put('{id}', response_model=models.FormPublic) +@router.put('/{id}', response_model=models.FormPublic) async def update_form( id: int, form: models.FormUpdate, user: models.User = Depends(get_current_user), @@ -43,7 +43,7 @@ async def update_form( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('{id}', response_model=models.FormPublic) +@router.delete('/{id}', response_model=models.FormPublic) async def delete_form( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/productors/productors.py b/backend/src/productors/productors.py index f2058da..cd24e23 100644 --- a/backend/src/productors/productors.py +++ b/backend/src/productors/productors.py @@ -17,7 +17,7 @@ def get_productors( ): return service.get_all(session, names, types) -@router.get('{id}', response_model=models.ProductorPublic) +@router.get('/{id}', response_model=models.ProductorPublic) def get_productor( id: int, user: models.User = Depends(get_current_user), @@ -36,7 +36,7 @@ def create_productor( ): return service.create_one(session, productor) -@router.put('{id}', response_model=models.ProductorPublic) +@router.put('/{id}', response_model=models.ProductorPublic) def update_productor( id: int, productor: models.ProductorUpdate, user: models.User = Depends(get_current_user), @@ -47,7 +47,7 @@ def update_productor( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('{id}', response_model=models.ProductorPublic) +@router.delete('/{id}', response_model=models.ProductorPublic) def delete_productor( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/products/products.py b/backend/src/products/products.py index e7c7e8c..7f212b2 100644 --- a/backend/src/products/products.py +++ b/backend/src/products/products.py @@ -6,7 +6,7 @@ from sqlmodel import Session import src.products.service as service from src.auth.auth import get_current_user router = APIRouter(prefix='/products') -#user=Depends(get_current_user) + @router.get('', response_model=list[models.ProductPublic], ) def get_products( user: models.User = Depends(get_current_user), @@ -22,7 +22,7 @@ def get_products( types, ) -@router.get('{id}', response_model=models.ProductPublic) +@router.get('/{id}', response_model=models.ProductPublic) def get_product( id: int, user: models.User = Depends(get_current_user), @@ -41,7 +41,7 @@ def create_product( ): return service.create_one(session, product) -@router.put('{id}', response_model=models.ProductPublic) +@router.put('/{id}', response_model=models.ProductPublic) def update_product( id: int, product: models.ProductUpdate, user: models.User = Depends(get_current_user), @@ -52,7 +52,7 @@ def update_product( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('{id}', response_model=models.ProductPublic) +@router.delete('/{id}', response_model=models.ProductPublic) def delete_product( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/shipments/shipments.py b/backend/src/shipments/shipments.py index ee47a34..91f798b 100644 --- a/backend/src/shipments/shipments.py +++ b/backend/src/shipments/shipments.py @@ -22,7 +22,7 @@ def get_shipments( forms, ) -@router.get('{id}', response_model=models.ShipmentPublic) +@router.get('/{id}', response_model=models.ShipmentPublic) def get_shipment( id: int, user: models.User = Depends(get_current_user), @@ -41,7 +41,7 @@ def create_shipment( ): return service.create_one(session, shipment) -@router.put('{id}', response_model=models.ShipmentPublic) +@router.put('/{id}', response_model=models.ShipmentPublic) def update_shipment( id: int, shipment: models.ShipmentUpdate, user: models.User = Depends(get_current_user), @@ -52,7 +52,7 @@ def update_shipment( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('{id}', response_model=models.ShipmentPublic) +@router.delete('/{id}', response_model=models.ShipmentPublic) def delete_shipment( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/templates/templates.py b/backend/src/templates/templates.py index eb35873..2f33222 100644 --- a/backend/src/templates/templates.py +++ b/backend/src/templates/templates.py @@ -15,7 +15,7 @@ def get_templates( ): return service.get_all(session) -@router.get('{id}', response_model=models.TemplatePublic) +@router.get('/{id}', response_model=models.TemplatePublic) def get_template( id: int, user: models.User = Depends(get_current_user), @@ -34,7 +34,7 @@ def create_template( ): return service.create_one(session, template) -@router.put('{id}', response_model=models.TemplatePublic) +@router.put('/{id}', response_model=models.TemplatePublic) def update_template( id: int, template: models.TemplateUpdate, user: models.User = Depends(get_current_user), @@ -45,7 +45,7 @@ def update_template( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('{id}', response_model=models.TemplatePublic) +@router.delete('/{id}', response_model=models.TemplatePublic) def delete_template( id: int, user: models.User = Depends(get_current_user), diff --git a/backend/src/users/users.py b/backend/src/users/users.py index b599afe..8c375bd 100644 --- a/backend/src/users/users.py +++ b/backend/src/users/users.py @@ -28,7 +28,7 @@ def get_roles( ): return service.get_roles(session) -@router.get('{id}', response_model=models.UserPublic) +@router.get('/{id}', response_model=models.UserPublic) def get_users( id: int, user: models.User = Depends(get_current_user), @@ -47,7 +47,7 @@ def create_user( ): return service.create_one(session, user) -@router.put('{id}', response_model=models.UserPublic) +@router.put('/{id}', response_model=models.UserPublic) def update_user( id: int, user: models.UserUpdate, @@ -59,7 +59,7 @@ def update_user( raise HTTPException(status_code=404, detail=messages.notfound) return result -@router.delete('{id}', response_model=models.UserPublic) +@router.delete('/{id}', response_model=models.UserPublic) def delete_user( id: int, user: models.User = Depends(get_current_user), diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index c07e388..b9d15dd 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -33,7 +33,7 @@ services: KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET} KEYCLOAK_REDIRECT_URI: ${KEYCLOAK_REDIRECT_URI} - DEBUG: ${DEBUG} + DEBUG: true MAX_AGE: ${MAX_AGE} ports: - "8000:8000" diff --git a/frontend/src/components/Navbar/index.tsx b/frontend/src/components/Navbar/index.tsx index 607938f..e40ad29 100644 --- a/frontend/src/components/Navbar/index.tsx +++ b/frontend/src/components/Navbar/index.tsx @@ -29,13 +29,13 @@ export function Navbar() { ) : null} {!user?.logged ? ( - {t("login with keycloak", { capfirst: true })} - + ) : ( From af4941c1b93a0dd9aee7207f1b362e43763b340e Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 11:17:20 +0100 Subject: [PATCH 24/56] fix contract generation --- backend/src/contracts/generate_contract.py | 29 +++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/backend/src/contracts/generate_contract.py b/backend/src/contracts/generate_contract.py index aa4478b..6adbd48 100644 --- a/backend/src/contracts/generate_contract.py +++ b/backend/src/contracts/generate_contract.py @@ -42,25 +42,24 @@ def generate_html_contract( 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, - } + # 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, + # } return HTML( string=output_text, base_url=template_dir, - **options ).write_pdf() from odfdo import Document, Table, Row, Cell From 34b2436ca036d2130c9e86e29d4f966af320c292 Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 12:05:41 +0100 Subject: [PATCH 25/56] fix contract submission --- backend/src/contracts/contracts.py | 5 ++--- frontend/src/pages/Contract/index.tsx | 9 ++++++--- frontend/src/services/api.ts | 21 ++++++++++++++------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index 5eab4b9..a7114c6 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -80,7 +80,6 @@ async def create_contract( 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 @@ -90,8 +89,8 @@ async def create_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}' diff --git a/frontend/src/pages/Contract/index.tsx b/frontend/src/pages/Contract/index.tsx index 8bec61e..9e59363 100644 --- a/frontend/src/pages/Contract/index.tsx +++ b/frontend/src/pages/Contract/index.tsx @@ -18,7 +18,7 @@ import { Title, } from "@mantine/core"; import { useForm } from "@mantine/form"; -import { IconMail, IconPhone, IconUser } from "@tabler/icons-react"; +import { IconDownload, IconMail, IconPhone, IconUser } from "@tabler/icons-react"; import { useCallback, useMemo, useRef } from "react"; import { useParams } from "react-router"; import { computePrices } from "./price"; @@ -140,6 +140,7 @@ export function Contract() { products: tranformProducts(withDefaultValues(inputForm.getValues().products)), }; await createContractMutation.mutateAsync(contract); + window.location.href = '/'; } else { const firstErrorField = Object.keys(errors.errors)[0]; const ref = inputRefs.current[firstErrorField]; @@ -316,8 +317,10 @@ export function Contract() { currency: "EUR", }).format(price)} - diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index cc29a8f..819eb6e 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -752,7 +752,6 @@ export function useGetAllContractFile() { disposition && disposition?.includes("filename=") ? disposition.split("filename=")[1].replace(/"/g, "") : `contract_${form_id}.zip`; - console.log(disposition); return { blob, filename }; }, onSuccess: ({ blob, filename }) => { @@ -770,20 +769,28 @@ export function useCreateContract() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (newContract: ContractCreate) => { - return fetch(`${Config.backend_uri}/contracts`, { + mutationFn: async (newContract: ContractCreate) => { + const res = await fetch(`${Config.backend_uri}/contracts`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(newContract), - }).then(async (res) => await res.blob()); + }).then((res) => res); + if (!res.ok) throw new Error(); + const blob = await res.blob(); + const disposition = res.headers.get("Content-Disposition"); + const filename = + disposition && disposition?.includes("filename=") + ? disposition.split("filename=")[1].replace(/"/g, "") + : `contract_${newContract.form_id}.pdf`; + return { blob, filename }; }, - onSuccess: async (pdfBlob) => { - const url = URL.createObjectURL(pdfBlob); + onSuccess: async ({ blob, filename }) => { + const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.download = `contract.pdf`; + link.download = filename; link.click(); URL.revokeObjectURL(url); await queryClient.invalidateQueries({ queryKey: ["contracts"] }); From 6dd5ade89017c83909dab4d92fbe54ca79335b3a Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 13:37:03 +0100 Subject: [PATCH 26/56] remove debug --- backend/src/auth/auth.py | 2 +- backend/src/contracts/contracts.py | 3 +-- backend/src/users/service.py | 6 ++---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/backend/src/auth/auth.py b/backend/src/auth/auth.py index d092f7b..2bf2614 100644 --- a/backend/src/auth/auth.py +++ b/backend/src/auth/auth.py @@ -109,7 +109,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) diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index a7114c6..1f53ae8 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -95,8 +95,7 @@ async def create_contract( pdf_file = io.BytesIO(pdf_bytes) contract_id = f'{new_contract.firstname}_{new_contract.lastname}_{new_contract.form.productor.type}_{new_contract.form.season}' service.add_contract_file(session, new_contract.id, pdf_bytes, price) - except Exception as e: - print(e) + except Exception: raise HTTPException(status_code=400, detail=messages.pdferror) return StreamingResponse( pdf_file, diff --git a/backend/src/users/service.py b/backend/src/users/service.py index cf72f66..1ceb8d5 100644 --- a/backend/src/users/service.py +++ b/backend/src/users/service.py @@ -69,13 +69,11 @@ def update_one(session: Session, id: int, user: models.UserCreate) -> models.Use 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) + 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) From ef5d9e845570dfdfafc3b4d9205296fd3baa457e Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 13:49:10 +0100 Subject: [PATCH 27/56] add default cheque quantity --- frontend/src/pages/Contract/index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Contract/index.tsx b/frontend/src/pages/Contract/index.tsx index 9e59363..db07b10 100644 --- a/frontend/src/pages/Contract/index.tsx +++ b/frontend/src/pages/Contract/index.tsx @@ -35,7 +35,7 @@ export function Contract() { email: "", phone: "", payment_method: "", - cheque_quantity: 0, + cheque_quantity: 1, cheques: [], products: {}, }, @@ -134,10 +134,12 @@ export function Contract() { return; } if (inputForm.isValid() && isShipmentsMinimumValue()) { + const formValues = inputForm.getValues(); const contract = { - ...inputForm.getValues(), + ...formValues, + cheque_quantity: formValues.payment_method === "cheque" ? formValues.cheque_quantity : 0, form_id: form.id, - products: tranformProducts(withDefaultValues(inputForm.getValues().products)), + products: tranformProducts(withDefaultValues(formValues.products)), }; await createContractMutation.mutateAsync(contract); window.location.href = '/'; From 12be0e5650de1cfd077aff62fc11cde3f0887500 Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 13:58:30 +0100 Subject: [PATCH 28/56] remove doublons --- backend/src/contracts/templates/layout.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/contracts/templates/layout.html b/backend/src/contracts/templates/layout.html index 32d017f..2585dfd 100644 --- a/backend/src/contracts/templates/layout.html +++ b/backend/src/contracts/templates/layout.html @@ -151,10 +151,6 @@ Saison du contrat {{contract_season}} - - Type de contrat - {{contract_type}} - Référent·e {{referer_name}} From b662a6a1f02c0a93adc02f365465a1f19bdbe97d Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 15:42:15 +0100 Subject: [PATCH 29/56] add base template for manual fill --- backend/src/contracts/contracts.py | 40 ++++++++++++++++++++ backend/src/contracts/generate_contract.py | 4 +- backend/src/contracts/templates/layout.html | 13 ++++--- frontend/locales/en.json | 2 + frontend/locales/fr.json | 2 + frontend/src/components/Forms/Card/index.tsx | 38 ++++++++++++++++++- frontend/src/services/api.ts | 28 ++++++++++++++ 7 files changed, 118 insertions(+), 9 deletions(-) diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index 1f53ae8..940fd5e 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -105,6 +105,46 @@ async def create_contract( } ) +@router.get('/{form_id}/base') +async def get_base_contract_template( + form_id: int, + session: Session = Depends(get_session), +): + form = form_service.get_one(session, form_id) + recurrents = [pr for pr in form.productor.products if pr.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.Contract( + firstname="", + form=form, + lastname="", + email="", + phone="", + payment_method="cheque" + ) + 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}_{empty_contract.form.season}' + except Exception: + raise HTTPException(status_code=400, detail=messages.pdferror) + 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([]), diff --git a/backend/src/contracts/generate_contract.py b/backend/src/contracts/generate_contract.py index 6adbd48..735b405 100644 --- a/backend/src/contracts/generate_contract.py +++ b/backend/src/contracts/generate_contract.py @@ -12,8 +12,8 @@ def generate_html_contract( cheques: list[dict], occasionals: list[dict], reccurents: list[dict], - recurrent_price: float, - total_price: float + 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) diff --git a/backend/src/contracts/templates/layout.html b/backend/src/contracts/templates/layout.html index 2585dfd..79cafff 100644 --- a/backend/src/contracts/templates/layout.html +++ b/backend/src/contracts/templates/layout.html @@ -281,7 +281,7 @@ {% endfor %} Total - {{recurrent_price}}€ + {{recurrent_price if recurrent_price else ""}}€ @@ -317,14 +317,15 @@ product.product.quantity_unit != None else ""}} - {{product.quantity}}{{"g" if product.product.unit == "1" else + {{product.product.quantity if product.product.quantity != None + else ""}}{{"g" if product.product.unit == "1" else "kg" if product.product.unit == "2" else "p" }} {% endfor%} Total - {{occasional.price}}€ + {{occasional.price if occasional.price else ""}}€ @@ -333,7 +334,7 @@ {% endif %}
Prix Total :
-
{{total_price}}€
+
{{total_price if total_price else ""}}€

Paiement par {{contract_payment_method}}

{% if contract_payment_method == "chèque" %} @@ -342,14 +343,14 @@ {% for cheque in cheques %} - Cheque n°{{cheque.name}} + Cheque n°{{cheque.name if cheque.name else ""}} {% endfor %} {% for cheque in cheques %} - {{cheque.value}}€ + {{cheque.value if cheque.value else ""}}€ {% endfor %} diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 99c9744..5e00fa2 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -75,6 +75,8 @@ "some contracts require a minimum value per shipment, ignore this field if it's not the case": "some contracts require a minimum value per shipment. Ignore this field if it does not apply to your contract.", "export contracts": "export contracts", "download recap": "download recap", + "fill contract online": "fill contract online", + "download base template to print": "download base template to print", "to export contracts submissions before sending to the productor go to the contracts section": "to export contracts submissions before sending to the productor go to the contracts section.", "in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract": "in this page you can view all contracts submissions, you can remove duplicates submission or download a specific contract", "you can download all contracts for your form using the export all": "you can download all contracts for your form using the export all", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index cc60ae3..d4ee378 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -89,6 +89,8 @@ "all contracts": "tous les contrats", "remove contract": "supprimer le contrat", "download contract": "télécharger le contrat", + "fill contract online": "remplir le contrat en ligne", + "download base template to print": "télécharger le contrat à remplir sur papier", "by selecting a form here you can download all contracts of your form": "en selectionnant un formulaire, vous téléchargez tous les contrats pour un formulaire donné.", "edit user": "modifier l'utilisateur·trice", "remove user": "supprimer l'utilisateur·trice", diff --git a/frontend/src/components/Forms/Card/index.tsx b/frontend/src/components/Forms/Card/index.tsx index 511c55c..cbeda7a 100644 --- a/frontend/src/components/Forms/Card/index.tsx +++ b/frontend/src/components/Forms/Card/index.tsx @@ -1,17 +1,53 @@ -import { Badge, Box, Group, Paper, Text, Title } from "@mantine/core"; +import { ActionIcon, Badge, Box, Group, Paper, Text, Title, Tooltip } from "@mantine/core"; import { Link } from "react-router"; import type { Form } from "@/services/resources/forms"; +import { IconDownload, IconExternalLink, IconLink } from "@tabler/icons-react"; +import { t } from "@/config/i18n"; +import { useGetContractFileTemplate } from "@/services/api"; export type FormCardProps = { form: Form; }; export function FormCard({ form }: FormCardProps) { + const contractBaseTemplate = useGetContractFileTemplate() return ( + + + { + await contractBaseTemplate.mutateAsync(form.id) + }} + > + + + + + + + + + + diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 819eb6e..8dea5f0 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -711,6 +711,34 @@ export function useGetContractFile() { }); } + +export function useGetContractFileTemplate() { + return useMutation({ + mutationFn: async (form_id: number) => { + const res = await fetchWithAuth(`${Config.backend_uri}/contracts/${form_id}/base`) + .then((res) => res); + + if (!res.ok) throw new Error(); + const blob = await res.blob(); + const disposition = res.headers.get("Content-Disposition"); + const filename = + disposition && disposition?.includes("filename=") + ? disposition.split("filename=")[1].replace(/"/g, "") + : `contract_${form_id}.pdf`; + return { blob, filename }; + }, + onSuccess: ({ blob, filename }) => { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + link.click(); + URL.revokeObjectURL(url); + }, + }); +} + + export function useGetRecap() { return useMutation({ mutationFn: async (form_id: number) => { From 2177a77fcf93d16cb271133fd7a61c1c0c1a244a Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 15:54:47 +0100 Subject: [PATCH 30/56] fix eslint --- frontend/src/components/Forms/Card/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Forms/Card/index.tsx b/frontend/src/components/Forms/Card/index.tsx index cbeda7a..9c2bc2f 100644 --- a/frontend/src/components/Forms/Card/index.tsx +++ b/frontend/src/components/Forms/Card/index.tsx @@ -1,7 +1,7 @@ import { ActionIcon, Badge, Box, Group, Paper, Text, Title, Tooltip } from "@mantine/core"; import { Link } from "react-router"; import type { Form } from "@/services/resources/forms"; -import { IconDownload, IconExternalLink, IconLink } from "@tabler/icons-react"; +import { IconDownload, IconExternalLink } from "@tabler/icons-react"; import { t } from "@/config/i18n"; import { useGetContractFileTemplate } from "@/services/api"; From f009674b13a621f6da1ba5594c67e551e3599707 Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 16:02:51 +0100 Subject: [PATCH 31/56] fix contract dto --- backend/src/contracts/contracts.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index 940fd5e..10ee6c6 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -117,13 +117,17 @@ async def get_base_contract_template( 'price': None, 'products': [{'product': pr, 'quantity': None} for pr in sh.products] } for sh in form.shipments] - empty_contract = models.Contract( + empty_contract = models.ContractPublic( firstname="", form=form, lastname="", email="", phone="", - payment_method="cheque" + 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: From 05480b44dfd2c1fe768021b2401de00ce8988bc2 Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 16:19:43 +0100 Subject: [PATCH 32/56] add debug --- backend/src/contracts/contracts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index 10ee6c6..a30ddc0 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -139,7 +139,8 @@ async def get_base_contract_template( ) pdf_file = io.BytesIO(pdf_bytes) contract_id = f'{empty_contract.form.productor.type}_{empty_contract.form.season}' - except Exception: + except Exception as e: + print(e) raise HTTPException(status_code=400, detail=messages.pdferror) return StreamingResponse( pdf_file, From 72f8005fbd206d30308f7460ec26f313053ff00d Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 16:26:19 +0100 Subject: [PATCH 33/56] fix recurent and layout none --- backend/src/contracts/contracts.py | 2 +- backend/src/contracts/templates/layout.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index a30ddc0..3322c84 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -111,7 +111,7 @@ async def get_base_contract_template( session: Session = Depends(get_session), ): form = form_service.get_one(session, form_id) - recurrents = [pr for pr in form.productor.products if pr.type == models.ProductType.RECCURENT] + recurrents = list(map(lambda x: {"product": x, "quantity": None}, filter(lambda product: product.type == models.ProductType.RECCURENT, form.productor.products))) occasionals = [{ 'shipment': sh, 'price': None, diff --git a/backend/src/contracts/templates/layout.html b/backend/src/contracts/templates/layout.html index 79cafff..f4a6116 100644 --- a/backend/src/contracts/templates/layout.html +++ b/backend/src/contracts/templates/layout.html @@ -274,7 +274,7 @@ else ""}} - {{rec.quantity}}{{"g" if rec.product.unit == "1" else "kg" if + {{rec.product.quantity if rec.product.quantity != None else ""}}{{"g" if rec.product.unit == "1" else "kg" if rec.product.unit == "2" else "p" }} From 85a70da07da2af0326f59c302be94778ef6ca68a Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Fri, 20 Feb 2026 17:36:32 +0100 Subject: [PATCH 34/56] add max payment method for cheque --- README.md | 19 +++-------- .../alembic/versions/7854064278ce_message.py | 33 +++++++++++++++++++ backend/src/models.py | 1 + backend/src/productors/service.py | 3 +- frontend/locales/en.json | 2 ++ frontend/locales/fr.json | 2 ++ .../PaymentMethods/Cheque/index.tsx | 19 +++++++---- .../src/components/Productors/Modal/index.tsx | 22 +++++++++---- frontend/src/pages/Contract/index.tsx | 5 +-- frontend/src/services/resources/productors.ts | 1 + 10 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 backend/alembic/versions/7854064278ce_message.py diff --git a/README.md b/README.md index e8e84f2..7d3387b 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ ## backend\src\contracts\contracts.py -- Send contract to referer - Extract recap -- Extract all contracts -- store total price + +## Payment method max cheque number + +## Link products to a form ## Wording @@ -28,14 +29,4 @@ ### Contact -## Migrations - -- use alembic for migration management - -## Filter forms in home view - -## Only show current season (if multiple form, only show the one with latest start date) - -## Update contract after register - -## Default filter +## Update contract after (without registration) diff --git a/backend/alembic/versions/7854064278ce_message.py b/backend/alembic/versions/7854064278ce_message.py new file mode 100644 index 0000000..9521bd5 --- /dev/null +++ b/backend/alembic/versions/7854064278ce_message.py @@ -0,0 +1,33 @@ +"""message + +Revision ID: 7854064278ce +Revises: c0b1073a8394 +Create Date: 2026-02-20 17:17:25.739406 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +import sqlmodel.sql.sqltypes + +# revision identifiers, used by Alembic. +revision: str = '7854064278ce' +down_revision: Union[str, Sequence[str], None] = 'c0b1073a8394' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('paymentmethod', sa.Column('max', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('paymentmethod', 'max') + # ### end Alembic commands ### diff --git a/backend/src/models.py b/backend/src/models.py index d5e8f6a..9ce18fd 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -36,6 +36,7 @@ class UserCreate(UserBase): class PaymentMethodBase(SQLModel): name: str details: str + max: int | None class PaymentMethod(PaymentMethodBase, table=True): id: int | None = Field(default=None, primary_key=True) diff --git a/backend/src/productors/service.py b/backend/src/productors/service.py index 247b13e..89a8a0a 100644 --- a/backend/src/productors/service.py +++ b/backend/src/productors/service.py @@ -47,7 +47,8 @@ def update_one(session: Session, id: int, productor: models.ProductorUpdate) -> models.PaymentMethod( name=pm["name"], details=pm["details"], - productor_id=id + productor_id=id, + max=pm["max"] ) ) del productor_updates["payment_methods"] diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 5e00fa2..22bd8f1 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -92,6 +92,8 @@ "templates": "templates", "users": "users", "forms": "contract forms", + "max cheque number": "max cheque number", + "can be empty default to 3": "can be empty default to 3", "form": "contract form", "select a form": "select a form", "download contracts": "download contracts", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index d4ee378..e0ae3f8 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -94,6 +94,8 @@ "by selecting a form here you can download all contracts of your form": "en selectionnant un formulaire, vous téléchargez tous les contrats pour un formulaire donné.", "edit user": "modifier l'utilisateur·trice", "remove user": "supprimer l'utilisateur·trice", + "max cheque number": "numbre maximum de cheques possible", + "can be empty default to 3": "optionnel, la valeur par défaut est à 3 cheques", "all forms": "tous les formulaires de contrat", "create new form": "créer un nouveau formulaire de contrat", "actions": "actions", diff --git a/frontend/src/components/PaymentMethods/Cheque/index.tsx b/frontend/src/components/PaymentMethods/Cheque/index.tsx index 0bbb8dc..64fde29 100644 --- a/frontend/src/components/PaymentMethods/Cheque/index.tsx +++ b/frontend/src/components/PaymentMethods/Cheque/index.tsx @@ -1,13 +1,14 @@ import { t } from "@/config/i18n"; import type { ContractInputs } from "@/services/resources/contracts"; -import { Group, NumberInput, Stack, Text, TextInput, Title } from "@mantine/core"; +import type { Productor } from "@/services/resources/productors"; +import { Group, NumberInput, Stack, TextInput, Title } from "@mantine/core"; import type { UseFormReturnType } from "@mantine/form"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; export type ContractChequeProps = { inputForm: UseFormReturnType; price: number; - chequeOrder: string; + productor: Productor; }; export type Cheque = { @@ -15,7 +16,7 @@ export type Cheque = { value: string; }; -export function ContractCheque({ inputForm, price, chequeOrder }: ContractChequeProps) { +export function ContractCheque({ inputForm, price, productor }: ContractChequeProps) { useEffect(() => { if (!inputForm.values.payment_method.includes("cheque")) { return; @@ -41,9 +42,13 @@ export function ContractCheque({ inputForm, price, chequeOrder }: ContractCheque // eslint-disable-next-line react-hooks/exhaustive-deps }, [inputForm.values.cheque_quantity, price, inputForm.values.cheques]); + const paymentMethod = useMemo(() => { + return productor?.payment_methods.find((el) => el.name === "cheque") + }, [productor]); + return ( - {`${t("order name")} : ${chequeOrder}`} + {`${t("order name")} : ${paymentMethod?.details}`} @@ -64,7 +69,7 @@ export function ContractCheque({ inputForm, price, chequeOrder }: ContractCheque {...inputForm.getInputProps(`cheques.${index}.name`)} error={ cheque.name == "" ? - {inputForm?.errors.cheques} : + inputForm?.errors.cheques : null } /> diff --git a/frontend/src/components/Productors/Modal/index.tsx b/frontend/src/components/Productors/Modal/index.tsx index a56ffc7..7d1e901 100644 --- a/frontend/src/components/Productors/Modal/index.tsx +++ b/frontend/src/components/Productors/Modal/index.tsx @@ -3,7 +3,9 @@ import { Group, Modal, MultiSelect, + NumberInput, Select, + Stack, TextInput, Title, type ModalBaseProps, @@ -107,6 +109,7 @@ export function ProductorModal({ existing ?? { name, details: "", + max: null, } ); }), @@ -115,12 +118,19 @@ export function ProductorModal({ /> {form.values.payment_methods.map((method, index) => method.name === "cheque" ? ( - + + + + ) : null, )} diff --git a/frontend/src/pages/Contract/index.tsx b/frontend/src/pages/Contract/index.tsx index db07b10..a175421 100644 --- a/frontend/src/pages/Contract/index.tsx +++ b/frontend/src/pages/Contract/index.tsx @@ -286,10 +286,7 @@ export function Contract() { /> {inputForm.values.payment_method === "cheque" ? ( el.name === "cheque") - ?.details || "" - } + productor={form?.productor} price={price} inputForm={inputForm} /> diff --git a/frontend/src/services/resources/productors.ts b/frontend/src/services/resources/productors.ts index 4cab000..9845d9c 100644 --- a/frontend/src/services/resources/productors.ts +++ b/frontend/src/services/resources/productors.ts @@ -9,6 +9,7 @@ export const PaymentMethods = [ export type PaymentMethod = { name: string; details: string; + max: number | null; }; export type Productor = { From 41552a889fb22bb11990c7181ef6ffd40d12039d Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 18:13:47 +0100 Subject: [PATCH 35/56] fix cheque validation --- frontend/src/components/Productors/Modal/index.tsx | 2 +- frontend/src/pages/Productors/index.tsx | 14 +++++++++++++- frontend/src/services/resources/productors.ts | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Productors/Modal/index.tsx b/frontend/src/components/Productors/Modal/index.tsx index 7d1e901..183a480 100644 --- a/frontend/src/components/Productors/Modal/index.tsx +++ b/frontend/src/components/Productors/Modal/index.tsx @@ -109,7 +109,7 @@ export function ProductorModal({ existing ?? { name, details: "", - max: null, + max: "", } ); }), diff --git a/frontend/src/pages/Productors/index.tsx b/frontend/src/pages/Productors/index.tsx index 7a664d5..f708959 100644 --- a/frontend/src/pages/Productors/index.tsx +++ b/frontend/src/pages/Productors/index.tsx @@ -58,6 +58,11 @@ export default function Productors() { async (productor: ProductorInputs) => { await createProductorMutation.mutateAsync({ ...productor, + payment_methods: productor.payment_methods.map((payment) =>( { + name: payment.name, + details: payment.details, + max: payment.max === "" ? null : payment.max + })) }); closeModal(); }, @@ -69,7 +74,14 @@ export default function Productors() { if (!id) return; await editProductorMutation.mutateAsync({ id: id, - productor: productor, + productor: { + ...productor, + payment_methods: productor.payment_methods.map((payment) =>( { + name: payment.name, + details: payment.details, + max: payment.max === "" ? null : payment.max + })) + }, }); closeModal(); }, diff --git a/frontend/src/services/resources/productors.ts b/frontend/src/services/resources/productors.ts index 9845d9c..9c06810 100644 --- a/frontend/src/services/resources/productors.ts +++ b/frontend/src/services/resources/productors.ts @@ -9,7 +9,7 @@ export const PaymentMethods = [ export type PaymentMethod = { name: string; details: string; - max: number | null; + max: number | string | null; }; export type Productor = { From 71a62e6d19236e027ec474331ba8c7e24a076537 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 18:16:10 +0100 Subject: [PATCH 36/56] fix type max --- frontend/src/components/PaymentMethods/Cheque/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/PaymentMethods/Cheque/index.tsx b/frontend/src/components/PaymentMethods/Cheque/index.tsx index 64fde29..ef8731e 100644 --- a/frontend/src/components/PaymentMethods/Cheque/index.tsx +++ b/frontend/src/components/PaymentMethods/Cheque/index.tsx @@ -57,7 +57,7 @@ export function ContractCheque({ inputForm, price, productor }: ContractChequePr { capfirst: true }, )} min={1} - max={paymentMethod?.max || 3} + max={paymentMethod?.max && paymentMethod?.max !== "" ? Number(paymentMethod?.max) : 3} {...inputForm.getInputProps(`cheque_quantity`)} /> From f4bb71a296b571994d7cf338f8e3e7996ee900d8 Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 18:51:11 +0100 Subject: [PATCH 37/56] fix layout --- backend/src/contracts/templates/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/contracts/templates/layout.html b/backend/src/contracts/templates/layout.html index f4a6116..37f09a7 100644 --- a/backend/src/contracts/templates/layout.html +++ b/backend/src/contracts/templates/layout.html @@ -274,7 +274,7 @@ else ""}} - {{rec.product.quantity if rec.product.quantity != None else ""}}{{"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" }} From da22f241980828a8aa6bf4f863aed641ad6bd26b Mon Sep 17 00:00:00 2001 From: JulienAldon Date: Fri, 20 Feb 2026 18:51:45 +0100 Subject: [PATCH 38/56] fix layout --- backend/src/contracts/templates/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/contracts/templates/layout.html b/backend/src/contracts/templates/layout.html index 37f09a7..7870c68 100644 --- a/backend/src/contracts/templates/layout.html +++ b/backend/src/contracts/templates/layout.html @@ -317,7 +317,7 @@ product.product.quantity_unit != None else ""}} - {{product.product.quantity if product.product.quantity != None + {{product.quantity if product.quantity != None else ""}}{{"g" if product.product.unit == "1" else "kg" if product.product.unit == "2" else "p" }} From 124b0700da8181ba60d7d2991d6115fde5fc5f7d Mon Sep 17 00:00:00 2001 From: Julien Aldon Date: Mon, 23 Feb 2026 15:38:29 +0100 Subject: [PATCH 39/56] add visible field to form --- README.md | 2 ++ .../alembic/versions/e777ed5729ce_message.py | 33 +++++++++++++++++++ backend/src/forms/forms.py | 12 ++++++- backend/src/forms/service.py | 10 ++++++ backend/src/models.py | 2 ++ backend/src/productors/productors.py | 2 +- backend/src/productors/service.py | 7 ++-- backend/src/products/products.py | 3 +- backend/src/products/service.py | 6 +++- backend/src/shipments/service.py | 7 +++- backend/src/shipments/shipments.py | 2 ++ frontend/locales/en.json | 3 ++ frontend/locales/fr.json | 5 ++- frontend/src/components/Forms/Modal/index.tsx | 7 ++++ frontend/src/components/Forms/Row/index.tsx | 8 ++++- .../src/components/Shipments/Modal/index.tsx | 4 +-- frontend/src/pages/Forms/index.tsx | 7 ++-- frontend/src/services/api.ts | 10 +++++- frontend/src/services/resources/forms.ts | 4 +++ 19 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 backend/alembic/versions/e777ed5729ce_message.py diff --git a/README.md b/README.md index 7d3387b..c3aed71 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ - check products - Publish +## Only show productors / products / forms for referent of type + ## Footer ### Legal diff --git a/backend/alembic/versions/e777ed5729ce_message.py b/backend/alembic/versions/e777ed5729ce_message.py new file mode 100644 index 0000000..d2fa32c --- /dev/null +++ b/backend/alembic/versions/e777ed5729ce_message.py @@ -0,0 +1,33 @@ +"""message + +Revision ID: e777ed5729ce +Revises: 7854064278ce +Create Date: 2026-02-23 13:53:09.999893 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +import sqlmodel.sql.sqltypes + +# revision identifiers, used by Alembic. +revision: str = 'e777ed5729ce' +down_revision: Union[str, Sequence[str], None] = '7854064278ce' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('form', sa.Column('visible', sa.Boolean(), nullable=False, default=False, server_default="False")) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('form', 'visible') + # ### end Alembic commands ### diff --git a/backend/src/forms/forms.py b/backend/src/forms/forms.py index a30eafc..2254259 100644 --- a/backend/src/forms/forms.py +++ b/backend/src/forms/forms.py @@ -13,10 +13,20 @@ 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('/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) diff --git a/backend/src/forms/service.py b/backend/src/forms/service.py index 5156776..6a49f64 100644 --- a/backend/src/forms/service.py +++ b/backend/src/forms/service.py @@ -7,12 +7,20 @@ def get_all( 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)) + if not user: + statement = statement.where(models.Form.visible == True) if current_season: subquery = ( select( @@ -29,6 +37,8 @@ def get_all( (models.Productor.type == subquery.c.type) & (models.Form.start == subquery.c.max_start) ) + if not user: + statement = statement.where(models.Form.visible == True) return session.exec(statement.order_by(models.Form.name)).all() return session.exec(statement.order_by(models.Form.name)).all() diff --git a/backend/src/models.py b/backend/src/models.py index 9ce18fd..fa25ee4 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -136,6 +136,7 @@ class FormBase(SQLModel): start: datetime.date end: datetime.date minimum_shipment_value: float | None + visible: bool class FormPublic(FormBase): id: int @@ -167,6 +168,7 @@ class FormUpdate(SQLModel): start: datetime.date | None end: datetime.date | None minimum_shipment_value: float | None + visible: bool | None class FormCreate(FormBase): pass diff --git a/backend/src/productors/productors.py b/backend/src/productors/productors.py index cd24e23..4e89a81 100644 --- a/backend/src/productors/productors.py +++ b/backend/src/productors/productors.py @@ -15,7 +15,7 @@ def get_productors( 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) def get_productor( diff --git a/backend/src/productors/service.py b/backend/src/productors/service.py index 89a8a0a..dd7a822 100644 --- a/backend/src/productors/service.py +++ b/backend/src/productors/service.py @@ -2,11 +2,14 @@ from sqlmodel import Session, select import src.models as models def get_all( - session: Session, + 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: diff --git a/backend/src/products/products.py b/backend/src/products/products.py index 7f212b2..b7dced1 100644 --- a/backend/src/products/products.py +++ b/backend/src/products/products.py @@ -16,7 +16,8 @@ def get_products( productors: list[str] = Query([]), ): return service.get_all( - session, + session, + user, names, productors, types, diff --git a/backend/src/products/service.py b/backend/src/products/service.py index ae91f28..b898cda 100644 --- a/backend/src/products/service.py +++ b/backend/src/products/service.py @@ -3,11 +3,15 @@ import src.models as models def get_all( 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: diff --git a/backend/src/shipments/service.py b/backend/src/shipments/service.py index e5a8a1f..5158fd4 100644 --- a/backend/src/shipments/service.py +++ b/backend/src/shipments/service.py @@ -3,11 +3,16 @@ import src.models as models def get_all( session: Session, + user: models.User, names: list[str], dates: list[str], forms: list[int] ) -> list[models.ShipmentPublic]: - statement = select(models.Shipment) + 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 len(names) > 0: statement = statement.where(models.Shipment.name.in_(names)) if len(dates) > 0: diff --git a/backend/src/shipments/shipments.py b/backend/src/shipments/shipments.py index 91f798b..3a4c85d 100644 --- a/backend/src/shipments/shipments.py +++ b/backend/src/shipments/shipments.py @@ -11,12 +11,14 @@ router = APIRouter(prefix='/shipments') @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, diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 22bd8f1..02fe189 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -82,7 +82,10 @@ "you can download all contracts for your form using the export all": "you can download all contracts for your form using the export all", "in the same corner you can download a recap by clicking on the button": "in the same corner you can download a recap by clicking on the", "once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page": "once all contracts downloaded, you can delete the form (to avoid new submissions) and hide it from the home page", + "by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form": "by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form", "contracts": "contracts", + "hidden": "hidden", + "visible": "visible", "minimum price for this shipment should be at least": "minimum price for this shipment should be at least", "there is": "there is", "for this contract": "for this contract.", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index e0ae3f8..72bd885 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -73,7 +73,10 @@ "shipment products is necessary only for occasional products (if all products are recurrent leave empty)": "il est nécessaire de configurer les produits pour la livraison uniquement si il y a des produits occasionnels (laisser vide si tous les produits sont récurents).", "recurrent product is for all shipments, occasional product is for a specific shipment (see shipment form)": "les produits récurrents sont pour toutes les livraisons, les produits occasionnels sont pour une livraison particulière (voir formulaire de création de livraison).", "some contracts require a minimum value per shipment, ignore this field if it's not the case": "certains contrats nécessitent une valeur minimum par livraison. Ce champ peut être ignoré s’il ne s’applique pas à votre contrat.", + "by checking this option the form will be accessible publicly on the home page, only check it if everything is fine with your form": "en cochant cette option le formulaire sera accessible publiquement sur la page d'accueil, cochez cette option uniquement si tout est prêt avec votre formulaire.", "contracts": "contrats", + "hidden": "caché", + "visible": "visible", "minimum price for this shipment should be at least": "le prix minimum d'une livraison doit être au moins de", "there is": "il y a", "for this contract": "pour ce contrat.", @@ -166,7 +169,7 @@ "with cheque and transfer": "avec chèques et virements configuré pour le producteur", "mililiter": "mililitres (ml)", "this field is optionnal a product can have a quantity if configured inside the product it will be shown inside the form": "ce champ est optionnel dans la configuration d'un produit, il représente la quantité d'un produit (poids d'une tranche de foie, poids d'un panier, taille d'un bocal...). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens.", - "this field is also optionnal if a product have a quantity you can select the correct unit (metric system). It will be shown next to product quantity inside the form": "ce champs est optionnel dans la configuation d'un produit, il représente l'unité de mesure associé à la quantité d'un produit (g, kg, ml, L). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens à coté de la quantité du produit.", + "this field is also optionnal if a product have a quantity you can select the correct unit (metric system). It will be shown next to product quantity inside the form": "ce champs est optionnel dans la configuation d'un produit, il représente l'unité de mesure associée à la quantité d'un produit (g, kg, ml, L). Si ce champs est renseigné il sera affiché dans le formulaire à destination des amapiens à coté de la quantité du produit.", "with 150 set as quantity and g as quantity unit in product": "avec \"150\" en quantité de produit et \"grammes\" selectionné dans l'unité de quantité du produit", "all shipments should be recreated for each form creation": "les livraisons étant liées à un formulaire elles doivent être recréés pour chaque nouveau formulaire.", "a productor can be edited if its informations change, it should not be recreated for each contracts": "un(e) producteur·trice peut être édité si ses informations changent, il/elle ne doit pas être recréé pour chaque nouveau contrat.", diff --git a/frontend/src/components/Forms/Modal/index.tsx b/frontend/src/components/Forms/Modal/index.tsx index 36754dd..869fa49 100644 --- a/frontend/src/components/Forms/Modal/index.tsx +++ b/frontend/src/components/Forms/Modal/index.tsx @@ -1,5 +1,6 @@ import { Button, + Checkbox, Group, Modal, NumberInput, @@ -33,6 +34,7 @@ export default function FormModal({ opened, onClose, currentForm, handleSubmit } productor_id: currentForm?.productor?.id.toString() ?? "", referer_id: currentForm?.referer?.id.toString() ?? "", minimum_shipment_value: currentForm?.minimum_shipment_value ?? null, + visible: currentForm?.visible ?? false }, validate: { name: (value) => @@ -136,6 +138,11 @@ export default function FormModal({ opened, onClose, currentForm, handleSubmit } radius="sm" {...form.getInputProps("minimum_shipment_value")} /> +