diff --git a/README.md b/README.md index e334eb7..3273912 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,16 @@ ## Wording -- planned -> occasionnal (planifié -> occasionnel) - all translations -## Tutorial for referers +## Draft / Publish form +- By default form is in draft mode +- Validate a form (button) + - check if productor + - check if shipments + - check products +- Publish -### Glossary - -Occasional -Recurrent ## Footer @@ -32,8 +33,8 @@ Recurrent - use alembic for migration management -## Update contract after register - -## Filter contracts in home view +## Filter forms in home view ## Only show current season (if multiple form, only show the one with latest start date) + +## Update contract after register diff --git a/backend/src/contracts/contracts.py b/backend/src/contracts/contracts.py index 70d6926..bab0fd4 100644 --- a/backend/src/contracts/contracts.py +++ b/backend/src/contracts/contracts.py @@ -81,6 +81,7 @@ async def create_contract( total_price = '{:10.2f}'.format(recurrent_price + compute_occasional_prices(occasionals)) cheques = list(map(lambda x: {"name": x.name, "value": x.value}, new_contract.cheques)) # TODO: send contract to referer + try: pdf_bytes = generate_html_contract( new_contract, @@ -92,6 +93,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, id, pdf_bytes) except: raise HTTPException(status_code=400, detail=PDFerrorOccured) return StreamingResponse( @@ -109,6 +111,23 @@ def get_contracts( ): return service.get_all(session, forms) +@router.get('/{id}/file') +def get_contract_file( + id: int, + session: Session = Depends(get_session) +): + contract = service.get_one(session, id) + print(contract.file) + if contract is None: + raise HTTPException(status_code=404, detail=messages.notfound) + return StreamingResponse( + contract.file, + media_type='application/pdf', + headers={ + 'Content-Disposition': f'attachement; filename=contract_{contract.id}.pdf' + } + ) + @router.get('/{id}', response_model=models.ContractPublic) def get_contract(id: int, session: Session = Depends(get_session)): result = service.get_one(session, id) diff --git a/backend/src/contracts/service.py b/backend/src/contracts/service.py index 51a07dc..57032d8 100644 --- a/backend/src/contracts/service.py +++ b/backend/src/contracts/service.py @@ -39,6 +39,17 @@ def create_one(session: Session, contract: models.ContractCreate) -> models.Cont session.refresh(new_contract) return new_contract +def add_contract_file(session: Session, id: int, file: bytes): + statement = select(models.Contract).where(models.Contract.id == id) + result = session.exec(statement) + contract = result.first() + + contract.file = file + session.add(contract) + session.commit() + session.refresh(contract) + return contract + def update_one(session: Session, id: int, contract: models.ContractUpdate) -> models.ContractPublic: statement = select(models.Contract).where(models.Contract.id == id) result = session.exec(statement) diff --git a/backend/src/models.py b/backend/src/models.py index 99a0951..aec38a3 100644 --- a/backend/src/models.py +++ b/backend/src/models.py @@ -1,4 +1,4 @@ -from sqlmodel import Field, SQLModel, Relationship +from sqlmodel import Field, SQLModel, Relationship, Column, LargeBinary from enum import StrEnum from typing import Optional import datetime @@ -208,6 +208,7 @@ class Contract(ContractBase, table=True): back_populates="contract", cascade_delete=True ) + file: bytes = Field(sa_column=Column(LargeBinary)) class ContractCreate(ContractBase): products: list["ContractProductCreate"] = [] @@ -215,12 +216,13 @@ class ContractCreate(ContractBase): form_id: int class ContractUpdate(SQLModel): - pass + file: bytes class ContractPublic(ContractBase): id: int products: list["ContractProduct"] = [] form: Form + # file: bytes class ContractProductBase(SQLModel): product_id: int = Field( diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 2d20030..a523b8d 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -72,6 +72,7 @@ "shipment products is necessary only for occasional products (if all products are recurrent leave empty)": "shipment products configuration is only necessary for occasional products (leave empty if all products are recurrent).", "recurrent product is for all shipments, occasional product is for a specific shipment (see shipment form)": "recurrent products are for all shipments, occasional products are for a specific shipment (see shipment form).", "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.", + "contracts": "contracts", "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.", @@ -166,6 +167,7 @@ "of the productor": "of the producer", "of the shipment": "of the shipment", "of the contract": "of the contract", + "login with keycloak": "login with keycloak", "there is no contract for now": "There is no contract at the moment.", "for transfer method contact your referer or productor": "for bank transfer, contact your referent or producer.", "cheque quantity": "number of cheques", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index 2ca3c0d..27718e8 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -72,6 +72,7 @@ "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.", + "contracts": "contrats", "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,6 +167,7 @@ "of the productor": "du producteur·trice", "of the shipment": "de la livraison", "of the contract": "du contrat", + "login with keycloak": "se connecter avec keycloak", "there is no contract for now": "Il n'y a pas de contrats pour le moment.", "for transfer method contact your referer or productor": "pour mettre en place le virement automatique, contactez votre référent ou le producteur.", "cheque quantity": "quantité de chèques (pour le paiement en plusieurs fois)", diff --git a/frontend/src/components/Contracts/Row/index.tsx b/frontend/src/components/Contracts/Row/index.tsx index 2021823..27db1e7 100644 --- a/frontend/src/components/Contracts/Row/index.tsx +++ b/frontend/src/components/Contracts/Row/index.tsx @@ -1,8 +1,9 @@ import { ActionIcon, Table, Tooltip } from "@mantine/core"; import { type Contract } from "@/services/resources/contracts"; -import { IconX } from "@tabler/icons-react"; +import { IconDownload, IconX } from "@tabler/icons-react"; import { t } from "@/config/i18n"; -import { useDeleteContract } from "@/services/api"; +import { useDeleteContract, useGetContractFile } from "@/services/api"; +import { useCallback } from "react"; export type ContractRowProps = { contract: Contract; @@ -12,7 +13,19 @@ export default function ContractRow({ contract }: ContractRowProps) { // const [searchParams] = useSearchParams(); const deleteMutation = useDeleteContract(); // const navigate = useNavigate(); + const {refetch, isFetching} = useGetContractFile(contract.id) + const handleDownload = useCallback(async () => { + const { data } = await refetch(); + if (!data) + return; + const url = URL.createObjectURL(data.blob); + const link = document.createElement("a"); + link.href = url; + link.download = data.filename; + link.click(); + URL.revokeObjectURL(url); + }, [useGetContractFile]) return (