Sage100-ws/utils/documents/validations.py
2026-01-15 09:00:58 +01:00

267 lines
8.3 KiB
Python

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",
]