feat(factures): add create and update invoice endpoints

This commit is contained in:
Fanilo-Nantenaina 2025-12-08 09:43:34 +03:00
parent c15ae79c6a
commit 57d1f313f4
2 changed files with 280 additions and 0 deletions

238
api.py
View file

@ -462,7 +462,66 @@ class AvoirUpdateRequest(BaseModel):
"statut": 2
}
}
class LigneFacture(BaseModel):
"""Ligne de facture"""
article_code: str
quantite: float
prix_unitaire_ht: Optional[float] = None
remise_pourcentage: Optional[float] = 0.0
@field_validator("article_code", mode="before")
def strip_insecables(cls, v):
return v.replace("\xa0", "").strip()
class FactureCreateRequest(BaseModel):
"""Création d'une facture"""
client_id: str
date_facture: Optional[date] = None
lignes: List[LigneFacture]
reference: Optional[str] = None
class Config:
json_schema_extra = {
"example": {
"client_id": "CLI000001",
"date_facture": "2024-01-15",
"reference": "FA-EXT-001",
"lignes": [
{
"article_code": "ART001",
"quantite": 10.0,
"prix_unitaire_ht": 50.0,
"remise_pourcentage": 5.0
}
]
}
}
class FactureUpdateRequest(BaseModel):
"""Modification d'une facture existante"""
date_facture: Optional[date] = None
lignes: Optional[List[LigneFacture]] = None
statut: Optional[int] = Field(None, ge=0, le=6)
reference: Optional[str] = None
class Config:
json_schema_extra = {
"example": {
"lignes": [
{
"article_code": "ART001",
"quantite": 15.0,
"prix_unitaire_ht": 45.0
}
],
"statut": 2
}
}
# =====================================================
# SERVICES EXTERNES (Universign)
# =====================================================
@ -1920,6 +1979,185 @@ class RelanceFactureRequest(BaseModel):
doc_id: str
message_personnalise: Optional[str] = None
@app.post("/factures", status_code=201, tags=["US-A7"])
async def creer_facture(
facture: FactureCreateRequest,
session: AsyncSession = Depends(get_session)
):
"""
Création d'une facture
**Workflow typique:**
1. Commande Livraison Facture (transformations successives)
2. OU création directe d'une facture (cette route)
**Champs obligatoires:**
- `client_id`: Code du client
- `lignes`: Liste des lignes (min 1)
**Champs optionnels:**
- `date_facture`: Date de la facture (par défaut: aujourd'hui)
- `reference`: Référence externe (ex: numéro de commande client)
**Notes importantes:**
- Les factures peuvent avoir des champs obligatoires supplémentaires selon la configuration Sage
- Le statut initial est généralement 2 (Accepté/Validé)
- Les factures sont soumises aux règles de numérotation strictes
Args:
facture: Données de la facture à créer
Returns:
Facture créée avec son numéro et ses totaux
"""
try:
# Vérifier que le client existe
client = sage_client.lire_client(facture.client_id)
if not client:
raise HTTPException(404, f"Client {facture.client_id} introuvable")
# Préparer les données pour la gateway
facture_data = {
"client_id": facture.client_id,
"date_facture": (
facture.date_facture.isoformat()
if facture.date_facture
else None
),
"reference": facture.reference,
"lignes": [
{
"article_code": l.article_code,
"quantite": l.quantite,
"prix_unitaire_ht": l.prix_unitaire_ht,
"remise_pourcentage": l.remise_pourcentage,
}
for l in facture.lignes
],
}
# Appel à la gateway Windows
resultat = sage_client.creer_facture(facture_data)
logger.info(f"✅ Facture créée: {resultat.get('numero_facture')}")
return {
"success": True,
"message": "Facture créée avec succès",
"data": {
"numero_facture": resultat["numero_facture"],
"client_id": facture.client_id,
"date_facture": resultat["date_facture"],
"total_ht": resultat["total_ht"],
"total_ttc": resultat["total_ttc"],
"nb_lignes": resultat["nb_lignes"],
"reference": facture.reference
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur création facture: {e}")
raise HTTPException(500, str(e))
@app.put("/factures/{id}", tags=["US-A7"])
async def modifier_facture(
id: str,
facture_update: FactureUpdateRequest,
session: AsyncSession = Depends(get_session)
):
"""
Modification d'une facture existante
**Champs modifiables:**
- `date_facture`: Nouvelle date
- `lignes`: Nouvelles lignes (remplace toutes les lignes existantes)
- `statut`: Nouveau statut
- `reference`: Référence externe
**Restrictions IMPORTANTES:**
- Une facture transformée (statut=5) ne peut plus être modifiée
- Une facture annulée (statut=6) ne peut plus être modifiée
- **ATTENTION**: Les factures comptabilisées peuvent être verrouillées par Sage
- Certaines factures peuvent être en lecture seule selon les droits utilisateur
**Note importante:**
Si `lignes` est fourni, TOUTES les lignes existantes seront remplacées
Args:
id: Numéro de la facture à modifier
facture_update: Champs à mettre à jour
Returns:
Facture modifiée avec ses nouvelles valeurs
"""
try:
# Vérifier que la facture existe
facture_existante = sage_client.lire_document(
id,
settings.SAGE_TYPE_FACTURE
)
if not facture_existante:
raise HTTPException(404, f"Facture {id} introuvable")
# Vérifier le statut
statut_actuel = facture_existante.get("statut", 0)
if statut_actuel == 5:
raise HTTPException(
400,
f"La facture {id} a déjà été transformée et ne peut plus être modifiée"
)
if statut_actuel == 6:
raise HTTPException(
400,
f"La facture {id} est annulée et ne peut plus être modifiée"
)
# Construire les données de mise à jour
update_data = {}
if facture_update.date_facture:
update_data["date_facture"] = facture_update.date_facture.isoformat()
if facture_update.lignes is not None:
update_data["lignes"] = [
{
"article_code": l.article_code,
"quantite": l.quantite,
"prix_unitaire_ht": l.prix_unitaire_ht,
"remise_pourcentage": l.remise_pourcentage,
}
for l in facture_update.lignes
]
if facture_update.statut is not None:
update_data["statut"] = facture_update.statut
if facture_update.reference is not None:
update_data["reference"] = facture_update.reference
# Appel à la gateway Windows
resultat = sage_client.modifier_facture(id, update_data)
logger.info(f"✅ Facture {id} modifiée avec succès")
return {
"success": True,
"message": f"Facture {id} modifiée avec succès",
"facture": resultat
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Erreur modification facture {id}: {e}")
raise HTTPException(500, str(e))
# Templates email (si pas déjà définis)
templates_email_db = {

View file

@ -501,6 +501,48 @@ class SageGatewayClient:
"numero": numero,
"avoir_data": avoir_data
}).get("data", {})
def creer_facture(self, facture_data: Dict) -> Dict:
"""
Création d'une facture
Args:
facture_data: Dictionnaire contenant:
- client_id (str): Code du client
- date_facture (str, optional): Date au format ISO
- reference (str, optional): Référence externe
- lignes (List[Dict]): Liste des lignes avec:
- article_code (str)
- quantite (float)
- prix_unitaire_ht (float, optional)
- remise_pourcentage (float, optional)
Returns:
Facture créée avec son numéro et ses totaux
"""
return self._post("/sage/factures/create", facture_data).get("data", {})
def modifier_facture(self, numero: str, facture_data: Dict) -> Dict:
"""
Modification d'une facture existante
Args:
numero: Numéro de la facture à modifier
facture_data: Dictionnaire contenant les champs à modifier:
- date_facture (str, optional): Nouvelle date
- lignes (List, optional): Nouvelles lignes
- statut (int, optional): Nouveau statut
- reference (str, optional): Nouvelle référence
Returns:
Facture modifiée avec totaux recalculés
"""
return self._post("/sage/factures/update", {
"numero": numero,
"facture_data": facture_data
}).get("data", {})