diff --git a/main.py b/main.py index d4463fa..a3c941d 100644 --- a/main.py +++ b/main.py @@ -1542,13 +1542,17 @@ 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)) + raise HTTPException(status_code=400, detail=str(e)) + except RuntimeError as e: + logger.error(f"Erreur runtime validation {numero_facture}: {e}") + raise HTTPException(status_code=500, detail=str(e)) except Exception as e: - logger.error(f"Erreur technique validation {numero_facture}: {e}") - raise HTTPException(500, str(e)) + logger.error( + f"Erreur technique validation {numero_facture}: {e}", exc_info=True + ) + raise HTTPException(status_code=500, detail=str(e)) @app.post( @@ -1558,13 +1562,17 @@ 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)) + raise HTTPException(status_code=400, detail=str(e)) + except RuntimeError as e: + logger.error(f"Erreur runtime dévalidation {numero_facture}: {e}") + raise HTTPException(status_code=500, detail=str(e)) except Exception as e: - logger.error(f"Erreur technique dévalidation {numero_facture}: {e}") - raise HTTPException(500, str(e)) + logger.error( + f"Erreur technique dévalidation {numero_facture}: {e}", exc_info=True + ) + raise HTTPException(status_code=500, detail=str(e)) @app.get( @@ -1575,12 +1583,11 @@ 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)) + raise HTTPException(status_code=404, detail=str(e)) except Exception as e: - logger.error(f"Erreur lecture statut {numero_facture}: {e}") - raise HTTPException(500, str(e)) + logger.error(f"Erreur lecture statut {numero_facture}: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) @app.get("/sage/reglements/modes", dependencies=[Depends(verify_token)]) diff --git a/sage_connector.py b/sage_connector.py index 5ba597d..93bcb9a 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -8086,11 +8086,11 @@ class SageConnector: def introspecter_reglement(self): return _intro(self) - def valider_facture(self, numero_facture: str): + def valider_facture(self, numero_facture: str) -> dict: return _valider(self, numero_facture) - def devalider_facture(self, numero_facture: str): + def devalider_facture(self, numero_facture: str) -> dict: return _devalider(self, numero_facture) - def get_statut_validation(self, numero_facture: str): + def get_statut_validation(self, numero_facture: str) -> dict: return _get_statut(self, numero_facture) diff --git a/utils/documents/validations.py b/utils/documents/validations.py index 01e933f..e17d06b 100644 --- a/utils/documents/validations.py +++ b/utils/documents/validations.py @@ -5,151 +5,22 @@ import logging logger = logging.getLogger(__name__) -def valider_facture(self, numero_facture: str) -> Dict: +# ============================================================ +# FONCTIONS SQL (LECTURE) +# ============================================================ + + +def get_statut_validation(connector, 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. + Retourne le statut de validation d'une facture (SQL) """ - 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: + with connector._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 + DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC, + DO_MontantRegle, CT_NumPayeur, DO_Date, DO_Ref FROM F_DOCENTETE WHERE DO_Piece = ? AND DO_Type = 6 """, @@ -162,11 +33,12 @@ def get_statut_validation(self, numero_facture: str) -> Dict: 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 "" + total_ht = float(row[2]) if row[2] else 0.0 + total_ttc = float(row[3]) if row[3] else 0.0 + montant_regle = float(row[4]) if row[4] else 0.0 + client_code = row[5].strip() if row[5] else "" + date_facture = row[6] + reference = row[7].strip() if row[7] else "" solde = max(0.0, total_ttc - montant_regle) @@ -179,6 +51,7 @@ def get_statut_validation(self, numero_facture: str) -> Dict: "peut_etre_devalidee": valide == 1 and montant_regle < 0.01 and statut not in (5, 6), + "total_ht": total_ht, "total_ttc": total_ttc, "montant_regle": montant_regle, "solde_restant": solde, @@ -188,39 +61,171 @@ def get_statut_validation(self, numero_facture: str) -> Dict: } -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: +def _get_facture_info_sql(connector, numero_facture: str) -> Dict: + """ + Récupère les infos d'une facture via SQL (pour vérifications préalables) + """ + with connector._get_sql_connection() as conn: cursor = conn.cursor() cursor.execute( """ - SELECT ISNULL(SUM(DR_MontantRegle), 0) - FROM F_REGLECH + SELECT + DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC, + ISNULL((SELECT SUM(DR_MontantRegle) FROM F_REGLECH + WHERE DO_Piece = d.DO_Piece AND DO_Type = 6), 0) as MontantRegle, + CT_NumPayeur + FROM F_DOCENTETE d WHERE DO_Piece = ? AND DO_Type = 6 """, (numero_facture,), ) row = cursor.fetchone() - return float(row[0]) if row else 0.0 + + if not row: + return None + + return { + "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_ht": float(row[2]) if row[2] else 0.0, + "total_ttc": float(row[3]) if row[3] else 0.0, + "montant_regle": float(row[4]) if row[4] else 0.0, + "client_code": row[5].strip() if row[5] else "", + } -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)) +def valider_facture(connector, numero_facture: str) -> Dict: + if not connector.cial: + raise RuntimeError("Connexion Sage non établie") - client_code = "" + logger.info(f"🔒 Validation facture {numero_facture}") + + # 1. Vérifications préalables via SQL + info = _get_facture_info_sql(connector, numero_facture) + if not info: + raise ValueError(f"Facture {numero_facture} introuvable") + + if info["statut"] == 6: + raise ValueError(f"Facture {numero_facture} annulée, validation impossible") + + if info["valide"] == 1: + logger.info(f"Facture {numero_facture} déjà validée") + return _build_response_sql( + connector, numero_facture, deja_valide=True, action="validation" + ) + + # 2. Modification via COM try: - client_obj = getattr(doc, "Client", None) - if client_obj: - client_obj.Read() - client_code = getattr(client_obj, "CT_Num", "").strip() - except Exception: - pass + with connector._com_context(), connector._lock_com: + factory = connector.cial.FactoryDocumentVente + 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() + doc.DO_Valide = 1 + doc.Write() + + logger.info(f"✅ Facture {numero_facture} validée") + + except ValueError: + raise + except Exception as e: + logger.error(f"❌ Erreur COM validation {numero_facture}: {e}", exc_info=True) + raise RuntimeError(f"Échec validation: {str(e)}") + + # 3. Vérification et réponse via SQL + info_apres = _get_facture_info_sql(connector, numero_facture) + if info_apres and info_apres["valide"] != 1: + raise RuntimeError("Échec validation: DO_Valide non modifié") + + return _build_response_sql( + connector, numero_facture, deja_valide=False, action="validation" + ) + + +def devalider_facture(connector, numero_facture: str) -> Dict: + """ + Dévalide une facture (retire le cadenas) + + - Vérifications: SQL + - Modification DO_Valide: COM + - Réponse: SQL + """ + if not connector.cial: + raise RuntimeError("Connexion Sage non établie") + + logger.info(f"🔓 Dévalidation facture {numero_facture}") + + # 1. Vérifications préalables via SQL + info = _get_facture_info_sql(connector, numero_facture) + if not info: + raise ValueError(f"Facture {numero_facture} introuvable") + + if info["statut"] == 6: + raise ValueError(f"Facture {numero_facture} annulée") + + if info["statut"] == 5: + raise ValueError( + f"Facture {numero_facture} transformée, dévalidation impossible" + ) + + if info["montant_regle"] > 0.01: + raise ValueError( + f"Facture {numero_facture} partiellement réglée ({info['montant_regle']:.2f}€), " + "dévalidation impossible" + ) + + if info["valide"] == 0: + logger.info(f"Facture {numero_facture} déjà non validée") + return _build_response_sql( + connector, numero_facture, deja_valide=True, action="devalidation" + ) + + # 2. Modification via COM + try: + with connector._com_context(), connector._lock_com: + factory = connector.cial.FactoryDocumentVente + 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() + doc.DO_Valide = 0 + doc.Write() + + logger.info(f"✅ Facture {numero_facture} dévalidée") + + except ValueError: + raise + except Exception as e: + logger.error(f"❌ Erreur COM dévalidation {numero_facture}: {e}", exc_info=True) + raise RuntimeError(f"Échec dévalidation: {str(e)}") + + # 3. Vérification et réponse via SQL + info_apres = _get_facture_info_sql(connector, numero_facture) + if info_apres and info_apres["valide"] != 0: + raise RuntimeError("Échec dévalidation: DO_Valide non modifié") + + return _build_response_sql( + connector, numero_facture, deja_valide=False, action="devalidation" + ) + + +# ============================================================ +# FONCTIONS UTILITAIRES +# ============================================================ + + +def _build_response_sql( + connector, numero_facture: str, deja_valide: bool, action: str +) -> Dict: + """Construit la réponse via SQL (pas COM)""" + info = _get_facture_info_sql(connector, numero_facture) if action == "validation": message = ( @@ -235,12 +240,12 @@ def _build_response( 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, + "est_validee": info["valide"] == 1 if info else False, + "statut": info["statut"] if info else 0, + "statut_libelle": _get_statut_libelle(info["statut"]) if info else "Inconnu", + "total_ht": info["total_ht"] if info else 0.0, + "total_ttc": info["total_ttc"] if info else 0.0, + "client_code": info["client_code"] if info else "", "message": message, "action_effectuee": not deja_valide, }