Adding facture validation
This commit is contained in:
parent
ff8c35fcfa
commit
e281751c5e
5 changed files with 347 additions and 50 deletions
102
main.py
102
main.py
|
|
@ -1443,15 +1443,12 @@ def regler_facture_endpoint(
|
|||
numero_facture: str,
|
||||
req: ReglementFactureRequest,
|
||||
):
|
||||
"""
|
||||
Règle une facture (totalement ou partiellement)
|
||||
|
||||
- **numero_facture**: Numéro de la facture (ex: FA00081)
|
||||
- **montant**: Montant du règlement
|
||||
- **mode_reglement**: 1=Virement, 2=Chèque, 3=Traite, 4=CB, 5=LCR, 6=Prélèvement, 7=Espèces
|
||||
"""
|
||||
try:
|
||||
date_reg = datetime.combine(req.date_reglement, datetime.min.time()) if req.date_reglement else None
|
||||
date_reg = (
|
||||
datetime.combine(req.date_reglement, datetime.min.time())
|
||||
if req.date_reglement
|
||||
else None
|
||||
)
|
||||
|
||||
result = sage.regler_facture(
|
||||
numero_facture=numero_facture,
|
||||
|
|
@ -1461,7 +1458,11 @@ def regler_facture_endpoint(
|
|||
reference=req.reference or "",
|
||||
libelle=req.libelle or "",
|
||||
)
|
||||
return {"success": True, "message": "Règlement effectué avec succès", "data": result}
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Règlement effectué avec succès",
|
||||
"data": result,
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
|
|
@ -1472,12 +1473,6 @@ def regler_facture_endpoint(
|
|||
|
||||
@app.post("/sage/reglements/multiple", dependencies=[Depends(verify_token)])
|
||||
def regler_factures_client_endpoint(req: ReglementMultipleRequest):
|
||||
"""
|
||||
Règle plusieurs factures d'un client
|
||||
|
||||
Si numeros_factures est fourni, règle ces factures dans l'ordre.
|
||||
Sinon, règle les factures les plus anciennes en priorité.
|
||||
"""
|
||||
try:
|
||||
resultat = sage.regler_factures_client(
|
||||
client_code=req.client_code,
|
||||
|
|
@ -1504,9 +1499,6 @@ def regler_factures_client_endpoint(req: ReglementMultipleRequest):
|
|||
"/sage/factures/{numero_facture}/reglements", dependencies=[Depends(verify_token)]
|
||||
)
|
||||
def get_reglements_facture_endpoint(numero_facture: str):
|
||||
"""
|
||||
Récupère tous les règlements d'une facture
|
||||
"""
|
||||
try:
|
||||
resultat = sage.lire_reglements_facture(numero_facture)
|
||||
return {"success": True, "data": resultat}
|
||||
|
|
@ -1526,9 +1518,6 @@ def get_reglements_client_endpoint(
|
|||
date_fin: Optional[datetime] = Query(None, description="Date fin (filtrage)"),
|
||||
inclure_soldees: bool = Query(True, description="Inclure les factures soldées"),
|
||||
):
|
||||
"""
|
||||
Récupère tous les règlements d'un client avec leurs factures
|
||||
"""
|
||||
try:
|
||||
resultat = sage.lire_reglements_client(
|
||||
client_code=client_code,
|
||||
|
|
@ -1546,11 +1535,56 @@ def get_reglements_client_endpoint(
|
|||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post(
|
||||
"/sage/factures/{numero_facture}/valider", dependencies=[Depends(verify_token)]
|
||||
)
|
||||
def valider_facture_endpoint(numero_facture: str):
|
||||
try:
|
||||
resultat = sage.valider_facture(numero_facture)
|
||||
return {"success": True, "data": resultat}
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Erreur métier validation {numero_facture}: {e}")
|
||||
raise HTTPException(400, str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur technique validation {numero_facture}: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post(
|
||||
"/sage/factures/{numero_facture}/devalider", dependencies=[Depends(verify_token)]
|
||||
)
|
||||
def devalider_facture_endpoint(numero_facture: str):
|
||||
try:
|
||||
resultat = sage.devalider_facture(numero_facture)
|
||||
return {"success": True, "data": resultat}
|
||||
|
||||
except ValueError as e:
|
||||
logger.warning(f"Erreur métier dévalidation {numero_facture}: {e}")
|
||||
raise HTTPException(400, str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur technique dévalidation {numero_facture}: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.get(
|
||||
"/sage/factures/{numero_facture}/statut-validation",
|
||||
dependencies=[Depends(verify_token)],
|
||||
)
|
||||
def get_statut_validation_endpoint(numero_facture: str):
|
||||
try:
|
||||
resultat = sage.get_statut_validation(numero_facture)
|
||||
return {"success": True, "data": resultat}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(404, str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lecture statut {numero_facture}: {e}")
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.get("/sage/reglements/modes", dependencies=[Depends(verify_token)])
|
||||
def get_modes_reglement():
|
||||
"""
|
||||
Retourne la liste des modes de règlement disponibles
|
||||
"""
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
|
|
@ -1569,16 +1603,6 @@ def get_modes_reglement():
|
|||
|
||||
@app.get("/sage/journaux")
|
||||
def get_tous_journaux():
|
||||
"""
|
||||
Liste TOUS les journaux (pour diagnostic)
|
||||
|
||||
Types:
|
||||
- 0 = Achats
|
||||
- 1 = Ventes
|
||||
- 2 = Trésorerie (Banque/Caisse) ← Pour les règlements
|
||||
- 3 = Général (OD)
|
||||
- 4 = Situation
|
||||
"""
|
||||
try:
|
||||
journaux = sage.lire_tous_journaux()
|
||||
|
||||
|
|
@ -1606,9 +1630,6 @@ def get_tous_journaux():
|
|||
|
||||
@app.get("/sage/journaux/banque")
|
||||
def get_journaux_banque():
|
||||
"""
|
||||
Liste les journaux de trésorerie (type 2) pour les règlements
|
||||
"""
|
||||
try:
|
||||
journaux = sage.lire_journaux_banque()
|
||||
|
||||
|
|
@ -1631,11 +1652,6 @@ def get_journaux_banque():
|
|||
|
||||
@app.get("/sage/reglements/introspection")
|
||||
def introspecter_reglements():
|
||||
"""
|
||||
Introspection des objets COM de règlement (diagnostic)
|
||||
|
||||
Utile pour comprendre les attributs disponibles sur les objets de règlement.
|
||||
"""
|
||||
try:
|
||||
data = sage.introspecter_reglement()
|
||||
return {"success": True, "data": data}
|
||||
|
|
@ -1691,7 +1707,7 @@ def introspection_com():
|
|||
try:
|
||||
val = getattr(param, logo_attr, None)
|
||||
resultats["logo_tests"][logo_attr] = str(type(val))
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -103,6 +103,12 @@ from utils.documents.settle import (
|
|||
introspecter_reglement as _intro,
|
||||
)
|
||||
|
||||
from utils.documents.validations import (
|
||||
valider_facture as _valider,
|
||||
devalider_facture as _devalider,
|
||||
get_statut_validation as _get_statut,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -3732,7 +3738,7 @@ class SageConnector:
|
|||
client.ReadLock()
|
||||
locked = True
|
||||
lock_method_used = "ReadLock"
|
||||
logger.info(f" Verrouillage via ReadLock() [OK]")
|
||||
logger.info(" Verrouillage via ReadLock() [OK]")
|
||||
break
|
||||
|
||||
# Approche 2: Lock
|
||||
|
|
@ -3740,7 +3746,7 @@ class SageConnector:
|
|||
client.Lock()
|
||||
locked = True
|
||||
lock_method_used = "Lock"
|
||||
logger.info(f" Verrouillage via Lock() [OK]")
|
||||
logger.info(" Verrouillage via Lock() [OK]")
|
||||
break
|
||||
|
||||
# Approche 3: LockRecord
|
||||
|
|
@ -3748,7 +3754,7 @@ class SageConnector:
|
|||
client.LockRecord()
|
||||
locked = True
|
||||
lock_method_used = "LockRecord"
|
||||
logger.info(f" Verrouillage via LockRecord() [OK]")
|
||||
logger.info(" Verrouillage via LockRecord() [OK]")
|
||||
break
|
||||
|
||||
# Approche 4: Read avec paramètre mode écriture
|
||||
|
|
@ -3756,12 +3762,12 @@ class SageConnector:
|
|||
try:
|
||||
client.Read(1) # 1 = mode écriture
|
||||
lock_method_used = "Read(1)"
|
||||
logger.info(f" Verrouillage via Read(1) [OK]")
|
||||
logger.info(" Verrouillage via Read(1) [OK]")
|
||||
except TypeError:
|
||||
client.Read()
|
||||
lock_method_used = "Read()"
|
||||
logger.info(
|
||||
f" Read() simple (pas de verrouillage explicite)"
|
||||
" Read() simple (pas de verrouillage explicite)"
|
||||
)
|
||||
break
|
||||
|
||||
|
|
@ -8079,3 +8085,12 @@ class SageConnector:
|
|||
|
||||
def introspecter_reglement(self):
|
||||
return _intro(self)
|
||||
|
||||
def valider_facture(self, numero_facture: str):
|
||||
return _valider(self, numero_facture)
|
||||
|
||||
def devalider_facture(self, numero_facture: str):
|
||||
return _devalider(self, numero_facture)
|
||||
|
||||
def get_statut_validation(self, numero_facture: str):
|
||||
return _get_statut(self, numero_facture)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List
|
||||
import win32com.client
|
||||
import pywintypes
|
||||
from datetime import datetime
|
||||
|
|
|
|||
267
utils/documents/validations.py
Normal file
267
utils/documents/validations.py
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
from typing import Dict
|
||||
import win32com.client
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def valider_facture(self, numero_facture: str) -> Dict:
|
||||
"""
|
||||
Valide une facture (pose le cadenas)
|
||||
|
||||
Une facture validée ne peut plus être modifiée dans Sage.
|
||||
DO_Valide passe de 0 à 1.
|
||||
"""
|
||||
if not self.cial:
|
||||
raise RuntimeError("Connexion Sage non établie")
|
||||
|
||||
logger.info(f"🔒 Validation facture {numero_facture}")
|
||||
|
||||
try:
|
||||
with self._com_context(), self._lock_com:
|
||||
factory = self.cial.FactoryDocumentVente
|
||||
|
||||
if not factory.ExistPiece(60, numero_facture):
|
||||
raise ValueError(f"Facture {numero_facture} introuvable")
|
||||
|
||||
persist = factory.ReadPiece(60, numero_facture)
|
||||
if not persist:
|
||||
raise ValueError(f"Impossible de lire la facture {numero_facture}")
|
||||
|
||||
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
||||
doc.Read()
|
||||
|
||||
valide_actuel = getattr(doc, "DO_Valide", 0)
|
||||
statut = getattr(doc, "DO_Statut", 0)
|
||||
|
||||
if statut == 6:
|
||||
raise ValueError(
|
||||
f"Facture {numero_facture} annulée, validation impossible"
|
||||
)
|
||||
|
||||
if valide_actuel == 1:
|
||||
logger.info(f"Facture {numero_facture} déjà validée")
|
||||
return _build_response(doc, numero_facture, deja_valide=True)
|
||||
|
||||
doc.DO_Valide = 1
|
||||
doc.Write()
|
||||
doc.Read()
|
||||
|
||||
nouveau_valide = getattr(doc, "DO_Valide", 0)
|
||||
|
||||
if nouveau_valide != 1:
|
||||
raise RuntimeError("Échec validation: DO_Valide non modifié")
|
||||
|
||||
logger.info(f"✅ Facture {numero_facture} validée")
|
||||
|
||||
return _build_response(doc, numero_facture, deja_valide=False)
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Erreur validation {numero_facture}: {e}", exc_info=True)
|
||||
raise RuntimeError(f"Échec validation: {str(e)}")
|
||||
|
||||
|
||||
def devalider_facture(self, numero_facture: str) -> Dict:
|
||||
"""
|
||||
Dévalide une facture (retire le cadenas)
|
||||
|
||||
Attention: Une facture avec règlements ne peut pas être dévalidée.
|
||||
DO_Valide passe de 1 à 0.
|
||||
"""
|
||||
if not self.cial:
|
||||
raise RuntimeError("Connexion Sage non établie")
|
||||
|
||||
logger.info(f"🔓 Dévalidation facture {numero_facture}")
|
||||
|
||||
try:
|
||||
with self._com_context(), self._lock_com:
|
||||
montant_regle = _get_montant_regle(self, numero_facture)
|
||||
if montant_regle > 0.01:
|
||||
raise ValueError(
|
||||
f"Facture {numero_facture} partiellement réglée ({montant_regle}€), "
|
||||
"dévalidation impossible"
|
||||
)
|
||||
|
||||
factory = self.cial.FactoryDocumentVente
|
||||
|
||||
if not factory.ExistPiece(60, numero_facture):
|
||||
raise ValueError(f"Facture {numero_facture} introuvable")
|
||||
|
||||
persist = factory.ReadPiece(60, numero_facture)
|
||||
if not persist:
|
||||
raise ValueError(f"Impossible de lire la facture {numero_facture}")
|
||||
|
||||
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
||||
doc.Read()
|
||||
|
||||
valide_actuel = getattr(doc, "DO_Valide", 0)
|
||||
statut = getattr(doc, "DO_Statut", 0)
|
||||
|
||||
if statut == 6:
|
||||
raise ValueError(f"Facture {numero_facture} annulée")
|
||||
|
||||
if statut == 5:
|
||||
raise ValueError(
|
||||
f"Facture {numero_facture} transformée, dévalidation impossible"
|
||||
)
|
||||
|
||||
if valide_actuel == 0:
|
||||
logger.info(f"Facture {numero_facture} déjà non validée")
|
||||
return _build_response(
|
||||
doc, numero_facture, deja_valide=False, action="devalidation"
|
||||
)
|
||||
|
||||
doc.DO_Valide = 0
|
||||
doc.Write()
|
||||
doc.Read()
|
||||
|
||||
nouveau_valide = getattr(doc, "DO_Valide", 0)
|
||||
|
||||
if nouveau_valide != 0:
|
||||
raise RuntimeError("Échec dévalidation: DO_Valide non modifié")
|
||||
|
||||
logger.info(f"✅ Facture {numero_facture} dévalidée")
|
||||
|
||||
return _build_response(
|
||||
doc, numero_facture, deja_valide=False, action="devalidation"
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Erreur dévalidation {numero_facture}: {e}", exc_info=True)
|
||||
raise RuntimeError(f"Échec dévalidation: {str(e)}")
|
||||
|
||||
|
||||
def get_statut_validation(self, numero_facture: str) -> Dict:
|
||||
"""
|
||||
Retourne le statut de validation d'une facture
|
||||
"""
|
||||
if not self.cial:
|
||||
raise RuntimeError("Connexion Sage non établie")
|
||||
|
||||
with self._get_sql_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
DO_Valide, DO_Statut, DO_TotalTTC, DO_MontantRegle,
|
||||
CT_NumPayeur, DO_Date, DO_Ref
|
||||
FROM F_DOCENTETE
|
||||
WHERE DO_Piece = ? AND DO_Type = 6
|
||||
""",
|
||||
(numero_facture,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
raise ValueError(f"Facture {numero_facture} introuvable")
|
||||
|
||||
valide = int(row[0]) if row[0] is not None else 0
|
||||
statut = int(row[1]) if row[1] is not None else 0
|
||||
total_ttc = float(row[2]) if row[2] else 0.0
|
||||
montant_regle = float(row[3]) if row[3] else 0.0
|
||||
client_code = row[4].strip() if row[4] else ""
|
||||
date_facture = row[5]
|
||||
reference = row[6].strip() if row[6] else ""
|
||||
|
||||
solde = max(0.0, total_ttc - montant_regle)
|
||||
|
||||
return {
|
||||
"numero_facture": numero_facture,
|
||||
"est_validee": valide == 1,
|
||||
"statut": statut,
|
||||
"statut_libelle": _get_statut_libelle(statut),
|
||||
"peut_etre_modifiee": valide == 0 and statut not in (5, 6),
|
||||
"peut_etre_devalidee": valide == 1
|
||||
and montant_regle < 0.01
|
||||
and statut not in (5, 6),
|
||||
"total_ttc": total_ttc,
|
||||
"montant_regle": montant_regle,
|
||||
"solde_restant": solde,
|
||||
"client_code": client_code,
|
||||
"date_facture": date_facture.strftime("%Y-%m-%d") if date_facture else None,
|
||||
"reference": reference,
|
||||
}
|
||||
|
||||
|
||||
def _get_montant_regle(self, numero_facture: str) -> float:
|
||||
"""Récupère le montant déjà réglé d'une facture"""
|
||||
with self._get_sql_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT ISNULL(SUM(DR_MontantRegle), 0)
|
||||
FROM F_REGLECH
|
||||
WHERE DO_Piece = ? AND DO_Type = 6
|
||||
""",
|
||||
(numero_facture,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return float(row[0]) if row else 0.0
|
||||
|
||||
|
||||
def _build_response(
|
||||
doc, numero_facture: str, deja_valide: bool, action: str = "validation"
|
||||
) -> Dict:
|
||||
"""Construit la réponse de validation/dévalidation"""
|
||||
valide = getattr(doc, "DO_Valide", 0)
|
||||
statut = getattr(doc, "DO_Statut", 0)
|
||||
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
|
||||
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
|
||||
|
||||
client_code = ""
|
||||
try:
|
||||
client_obj = getattr(doc, "Client", None)
|
||||
if client_obj:
|
||||
client_obj.Read()
|
||||
client_code = getattr(client_obj, "CT_Num", "").strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if action == "validation":
|
||||
message = (
|
||||
"Facture déjà validée" if deja_valide else "Facture validée avec succès"
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
"Facture déjà non validée"
|
||||
if deja_valide
|
||||
else "Facture dévalidée avec succès"
|
||||
)
|
||||
|
||||
return {
|
||||
"numero_facture": numero_facture,
|
||||
"est_validee": valide == 1,
|
||||
"statut": statut,
|
||||
"statut_libelle": _get_statut_libelle(statut),
|
||||
"total_ht": total_ht,
|
||||
"total_ttc": total_ttc,
|
||||
"client_code": client_code,
|
||||
"message": message,
|
||||
"action_effectuee": not deja_valide,
|
||||
}
|
||||
|
||||
|
||||
def _get_statut_libelle(statut: int) -> str:
|
||||
"""Retourne le libellé d'un statut de document"""
|
||||
statuts = {
|
||||
0: "Brouillon",
|
||||
1: "Confirmé",
|
||||
2: "En cours",
|
||||
3: "Imprimé",
|
||||
4: "Suspendu",
|
||||
5: "Transformé",
|
||||
6: "Annulé",
|
||||
}
|
||||
return statuts.get(statut, f"Statut {statut}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"valider_facture",
|
||||
"devalider_facture",
|
||||
"get_statut_validation",
|
||||
]
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
from typing import Optional
|
||||
from pathlib import Path
|
||||
import base64
|
||||
import logging
|
||||
|
|
|
|||
Loading…
Reference in a new issue