diff --git a/main.py b/main.py index b80ee91..4ab50bd 100644 --- a/main.py +++ b/main.py @@ -1457,6 +1457,11 @@ def regler_facture_endpoint( date_reglement=date_reg, reference=req.reference or "", libelle=req.libelle or "", + code_journal=req.code_journal, # None = auto + devise_code=req.devise_code, + cours_devise=req.cours_devise, + tva_encaissement=req.tva_encaissement, + compte_general=req.compte_general, ) return { "success": True, @@ -1474,15 +1479,23 @@ def regler_facture_endpoint( @app.post("/sage/reglements/multiple", dependencies=[Depends(verify_token)]) def regler_factures_client_endpoint(req: ReglementMultipleRequest): try: + date_reg = ( + datetime.combine(req.date_reglement, datetime.min.time()) + if req.date_reglement + else None + ) resultat = sage.regler_factures_client( client_code=req.client_code, montant_total=req.montant_total, mode_reglement=req.mode_reglement, - date_reglement=req.date_reglement, + date_reglement=date_reg, reference=req.reference or "", libelle=req.libelle or "", - code_journal=req.code_journal, + code_journal=req.code_journal, # None = auto numeros_factures=req.numeros_factures, + devise_code=req.devise_code, + cours_devise=req.cours_devise, + tva_encaissement=req.tva_encaissement, ) return {"success": True, "data": resultat} @@ -1591,21 +1604,75 @@ def get_statut_validation_endpoint(numero_facture: str): @app.get("/sage/reglements/modes", dependencies=[Depends(verify_token)]) -def get_modes_reglement(): - return { - "success": True, - "data": { - "modes": [ - {"code": 1, "libelle": "Virement"}, - {"code": 2, "libelle": "Chèque"}, - {"code": 3, "libelle": "Traite"}, - {"code": 4, "libelle": "Carte bancaire"}, - {"code": 5, "libelle": "LCR"}, - {"code": 6, "libelle": "Prélèvement"}, - {"code": 7, "libelle": "Espèces"}, - ] - }, - } +def get_modes_reglement_endpoint(): + """Récupère les modes de règlement depuis Sage""" + try: + modes = sage.lire_modes_reglement() + return {"success": True, "data": {"modes": modes}} + except Exception as e: + logger.error(f"Erreur lecture modes règlement: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/sage/devises", dependencies=[Depends(verify_token)]) +def get_devises_endpoint(): + """Récupère les devises disponibles depuis Sage""" + try: + devises = sage.lire_devises() + return {"success": True, "data": {"devises": devises}} + except Exception as e: + logger.error(f"Erreur lecture devises: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/sage/journaux/tresorerie", dependencies=[Depends(verify_token)]) +def get_journaux_tresorerie_endpoint(): + """Récupère les journaux de trésorerie (banque + caisse)""" + try: + journaux = sage.lire_journaux_tresorerie() + return {"success": True, "data": {"journaux": journaux}} + except Exception as e: + logger.error(f"Erreur lecture journaux trésorerie: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/sage/comptes-generaux", dependencies=[Depends(verify_token)]) +def get_comptes_generaux_endpoint( + prefixe: Optional[str] = Query(None, description="Filtre par préfixe (ex: 41, 51)"), + type_compte: Optional[str] = Query( + None, + description="Type: client, fournisseur, banque, caisse, tva, produit, charge", + ), +): + """Récupère les comptes généraux""" + try: + comptes = sage.lire_comptes_generaux(prefixe=prefixe, type_compte=type_compte) + return {"success": True, "data": {"comptes": comptes, "total": len(comptes)}} + except Exception as e: + logger.error(f"Erreur lecture comptes généraux: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/sage/tva/taux", dependencies=[Depends(verify_token)]) +def get_tva_taux_endpoint(): + """Récupère les taux de TVA""" + try: + taux = sage.lire_tva_taux() + return {"success": True, "data": {"taux": taux}} + except Exception as e: + logger.error(f"Erreur lecture taux TVA: {e}") + raise HTTPException(500, str(e)) + + +@app.get("/sage/parametres/encaissement", dependencies=[Depends(verify_token)]) +def get_parametres_encaissement_endpoint(): + """Récupère les paramètres TVA sur encaissement""" + try: + params = sage.lire_parametres_encaissement() + return {"success": True, "data": params} + except Exception as e: + logger.error(f"Erreur lecture paramètres encaissement: {e}") + raise HTTPException(500, str(e)) @app.get("/sage/journaux") diff --git a/sage_connector.py b/sage_connector.py index 840517f..387f50c 100644 --- a/sage_connector.py +++ b/sage_connector.py @@ -94,6 +94,13 @@ from utils.functions.society.societe_data import ( ) from utils.documents.settle import ( + lire_modes_reglement, + lire_devises, + lire_journaux_tresorerie, + lire_comptes_generaux, + lire_tva_taux, + lire_parametres_encaissement, + _get_modes_reglement_standards, regler_facture as _regler_facture, regler_factures_client as _regler_factures_client, lire_reglements_client as _lire_reglements_client, @@ -8017,33 +8024,47 @@ class SageConnector: def regler_facture( self, - numero_facture, - montant, - mode_reglement=2, - date_reglement=None, - reference="", - libelle="", + numero_facture: str, + montant: float, + mode_reglement: int = 0, + date_reglement: datetime = None, + reference: str = "", + libelle: str = "", + code_journal: str = None, + devise_code: int = 0, + cours_devise: float = 1.0, + tva_encaissement: bool = False, + compte_general: str = None, ): + """Règle une facture""" return _regler_facture( self, - numero_facture, - montant, - mode_reglement, - date_reglement, - reference, - libelle, + numero_facture=numero_facture, + montant=montant, + mode_reglement=mode_reglement, + date_reglement=date_reglement, + reference=reference, + libelle=libelle, + code_journal=code_journal, + devise_code=devise_code, + cours_devise=cours_devise, + tva_encaissement=tva_encaissement, + compte_general=compte_general, ) def regler_factures_client( self, client_code: str, montant_total: float, - mode_reglement: int = 2, + mode_reglement: int = 0, date_reglement: datetime = None, reference: str = "", libelle: str = "", - code_journal: str = "BEU", + code_journal: str = None, numeros_factures: List[str] = None, + devise_code: int = 0, + cours_devise: float = 1.0, + tva_encaissement: bool = False, ): """Règle plusieurs factures d'un client""" return _regler_factures_client( @@ -8056,6 +8077,9 @@ class SageConnector: libelle=libelle, code_journal=code_journal, numeros_factures=numeros_factures, + devise_code=devise_code, + cours_devise=cours_devise, + tva_encaissement=tva_encaissement, ) def lire_reglements_facture(self, numero_facture: str): @@ -8102,3 +8126,21 @@ class SageConnector: def introspecter_document_complet(self, numero_facture: str) -> dict: return _introspect_doc(self, numero_facture) + + def lire_modes_reglement(self) -> dict: + return lire_modes_reglement(self) + + def lire_devises(self) -> dict: + return lire_devises(self) + + def lire_journaux_tresorerie(self) -> dict: + return lire_journaux_tresorerie(self) + + def lire_comptes_generaux(self, prefixe, type_compte) -> dict: + return lire_comptes_generaux(self, prefixe, type_compte) + + def lire_tva_taux(self) -> dict: + return lire_tva_taux(self) + + def lire_parametres_encaissement(self) -> dict: + return lire_parametres_encaissement(self) diff --git a/schemas/documents/reglements.py b/schemas/documents/reglements.py index 47d7a7e..ae8faa4 100644 --- a/schemas/documents/reglements.py +++ b/schemas/documents/reglements.py @@ -69,6 +69,10 @@ class ReglementFactureRequest(BaseModel): code_journal: str = Field( default="BEU", max_length=6, description="Code journal comptable" ) + devise_code: Optional[int] = Field(0) + cours_devise: Optional[float] = Field(1.0) + tva_encaissement: Optional[bool] = Field(False) + compte_general: Optional[str] = Field(None) @field_validator("montant") def validate_montant(cls, v): @@ -103,6 +107,9 @@ class ReglementMultipleRequest(BaseModel): default=None, description="Liste des factures à régler (sinon: plus anciennes d'abord)", ) + devise_code: Optional[int] = Field(0) + cours_devise: Optional[float] = Field(1.0) + tva_encaissement: Optional[bool] = Field(False) @field_validator("client_code", mode="before") def strip_client_code(cls, v): diff --git a/utils/documents/settle.py b/utils/documents/settle.py index 242ca89..537f3bd 100644 --- a/utils/documents/settle.py +++ b/utils/documents/settle.py @@ -12,26 +12,114 @@ logger = logging.getLogger(__name__) def _get_journal_auto(self, mode_reglement: int) -> str: with self._get_sql_connection() as conn: cursor = conn.cursor() - if mode_reglement == ModeReglement.ESPECES: - cursor.execute( - "SELECT TOP 1 JO_Num FROM F_JOURNAUX WHERE JO_Type = 2 AND JO_Reglement = 1 AND CG_Num LIKE '53%' ORDER BY JO_Num" - ) + + # Mode Espèces = 2 (selon l'image Sage fournie) + if mode_reglement == 2: # Espèces + cursor.execute(""" + SELECT TOP 1 JO_Num + FROM F_JOURNAUX + WHERE JO_Type = 2 + AND JO_Reglement = 1 + AND CG_Num LIKE '53%' + AND (JO_Sommeil = 0 OR JO_Sommeil IS NULL) + ORDER BY JO_Num + """) else: - cursor.execute( - "SELECT TOP 1 JO_Num FROM F_JOURNAUX WHERE JO_Type = 2 AND JO_Reglement = 1 AND CG_Num LIKE '51%' ORDER BY JO_Num" - ) + # Autres modes → Banque + cursor.execute(""" + SELECT TOP 1 JO_Num + FROM F_JOURNAUX + WHERE JO_Type = 2 + AND JO_Reglement = 1 + AND CG_Num LIKE '51%' + AND (JO_Sommeil = 0 OR JO_Sommeil IS NULL) + ORDER BY JO_Num + """) + row = cursor.fetchone() if row: return row[0].strip() - cursor.execute( - "SELECT TOP 1 JO_Num FROM F_JOURNAUX WHERE JO_Type = 2 AND JO_Reglement = 1 ORDER BY JO_Num" - ) + + # Fallback: premier journal de trésorerie disponible + cursor.execute(""" + SELECT TOP 1 JO_Num + FROM F_JOURNAUX + WHERE JO_Type = 2 + AND JO_Reglement = 1 + AND (JO_Sommeil = 0 OR JO_Sommeil IS NULL) + ORDER BY JO_Num + """) row = cursor.fetchone() if row: return row[0].strip() + raise ValueError("Aucun journal de trésorerie configuré") +def _valider_coherence_journal_mode(self, code_journal: str, mode_reglement: int): + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT CG_Num + FROM F_JOURNAUX + WHERE JO_Num = ? + """, + (code_journal,), + ) + + row = cursor.fetchone() + if not row: + raise ValueError(f"Journal {code_journal} introuvable") + + compte_general = (row[0] or "").strip() + + # Mode Espèces (2) doit utiliser un journal caisse (53x) + if mode_reglement == 2: + if not compte_general.startswith("53"): + logger.warning( + f"Mode Espèces avec journal non-caisse ({code_journal}, compte {compte_general})" + ) + else: + # Autres modes doivent utiliser un journal banque (51x) + if compte_general.startswith("53"): + logger.warning( + f"Mode non-espèces avec journal caisse ({code_journal}, compte {compte_general})" + ) + + +def _get_mode_reglement_libelle(self, mode_reglement: int) -> str: + """Récupère le libellé d'un mode de règlement""" + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT R_Intitule + FROM P_REGLEMENT + WHERE cbIndice = ? + """, + (mode_reglement,), + ) + + row = cursor.fetchone() + if row: + return (row[0] or "").strip() + + # Fallback sur les libellés standards + libelles = { + 0: "Chèque", + 1: "Virement", + 2: "Espèces", + 3: "LCR Acceptée", + 4: "LCR non acceptée", + 5: "BOR", + 6: "Prélèvement", + 7: "Carte bancaire", + 8: "Bon d'achat", + } + return libelles.get(mode_reglement, f"Mode {mode_reglement}") + + def lire_journaux_banque(self) -> List[Dict]: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -83,10 +171,15 @@ def regler_facture( self, numero_facture: str, montant: float, - mode_reglement: int = ModeReglement.CHEQUE, + mode_reglement: int = 0, # 0 = Chèque par défaut date_reglement: datetime = None, reference: str = "", libelle: str = "", + code_journal: str = None, # Si None, déduit automatiquement + devise_code: int = 0, + cours_devise: float = 1.0, + tva_encaissement: bool = False, + compte_general: str = None, ) -> Dict: if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -94,9 +187,17 @@ def regler_facture( raise ValueError("Le montant du règlement doit être positif") date_reglement = date_reglement or datetime.now() - code_journal = _get_journal_auto(self, mode_reglement) + + # Déduction automatique du journal si non fourni + if not code_journal: + code_journal = _get_journal_auto(self, mode_reglement) + else: + # Valider la cohérence journal/mode + _valider_coherence_journal_mode(self, code_journal, mode_reglement) + logger.info( - f"Règlement facture {numero_facture}: {montant}€ (mode: {mode_reglement}, journal: {code_journal})" + f"Règlement facture {numero_facture}: {montant}€ " + f"(mode: {mode_reglement}, journal: {code_journal}, devise: {devise_code})" ) try: @@ -122,6 +223,7 @@ def regler_facture( f"Montant ({montant}€) supérieur au solde ({solde_actuel:.2f}€)" ) + # Récupérer le client client_code = "" try: client_obj = getattr(doc, "Client", None) @@ -131,22 +233,28 @@ def regler_facture( except Exception: pass + # Récupérer l'échéance echeance = _get_premiere_echeance(doc) if not echeance: raise ValueError(f"Facture {numero_facture} sans échéance") + # Exécuter le règlement numero_reglement = _executer_reglement_com( self, - doc, - echeance, - montant, - mode_reglement, - date_reglement, - reference, - libelle, - code_journal, - client_code, - numero_facture, + doc=doc, + echeance=echeance, + montant=montant, + mode_reglement=mode_reglement, + date_reglement=date_reglement, + reference=reference, + libelle=libelle, + code_journal=code_journal, + client_code=client_code, + numero_facture=numero_facture, + devise_code=devise_code, + cours_devise=cours_devise, + tva_encaissement=tva_encaissement, + compte_general=compte_general, ) time.sleep(0.3) @@ -161,16 +269,22 @@ def regler_facture( nouveau_solde = total_ttc - nouveau_montant_regle logger.info(f"Règlement effectué - Solde restant: {nouveau_solde:.2f}€") + # Récupérer le libellé du mode règlement + mode_libelle = _get_mode_reglement_libelle(self, mode_reglement) + return { "numero_facture": numero_facture, "numero_reglement": numero_reglement, "montant_regle": montant, "date_reglement": date_reglement.strftime("%Y-%m-%d"), "mode_reglement": mode_reglement, - "mode_reglement_libelle": ModeReglement.get_libelle(mode_reglement), + "mode_reglement_libelle": mode_libelle, "reference": reference, "libelle": libelle, "code_journal": code_journal, + "devise_code": devise_code, + "cours_devise": cours_devise, + "tva_encaissement": tva_encaissement, "total_facture": total_ttc, "solde_restant": nouveau_solde, "facture_soldee": nouveau_solde < 0.01, @@ -217,6 +331,10 @@ def _executer_reglement_com( code_journal, client_code, numero_facture, + devise_code=0, + cours_devise=1.0, + tva_encaissement=False, + compte_general=None, ): erreurs = [] @@ -266,7 +384,6 @@ def _executer_reglement_com( # 5. Mode de règlement via l'objet Reglement try: - # Lire le mode de règlement depuis la base mode_factory = getattr( self.cial.CptaApplication, "FactoryModeReglement", None ) @@ -278,15 +395,59 @@ def _executer_reglement_com( except Exception as e: logger.debug(f" Mode règlement via factory: {e}") + # 6. Devise + if devise_code != 0: + try: + reg.RG_Devise = devise_code + logger.info(f" RG_Devise: {devise_code}") + except Exception as e: + logger.debug(f" RG_Devise: {e}") + + try: + reg.RG_Cours = cours_devise + logger.info(f" RG_Cours: {cours_devise}") + except Exception as e: + logger.debug(f" RG_Cours: {e}") + + # Montant en devise + try: + montant_devise = montant * cours_devise + reg.RG_MontantDev = montant_devise + logger.info(f" RG_MontantDev: {montant_devise}") + except Exception as e: + logger.debug(f" RG_MontantDev: {e}") + + # 7. TVA sur encaissement + if tva_encaissement: + try: + reg.RG_Encaissement = 1 + logger.info(" RG_Encaissement: 1 (TVA sur encaissement)") + except Exception as e: + logger.debug(f" RG_Encaissement: {e}") + + # 8. Compte général spécifique + if compte_general: + try: + cg_factory = self.cial.CptaApplication.FactoryCompteG + cg_persist = cg_factory.ReadNumero(compte_general) + if cg_persist: + reg.CompteG = cg_persist + logger.info(f" CompteG défini: {compte_general}") + except Exception as e: + logger.debug(f" CompteG: {e}") + + # 9. Référence et libellé if reference: try: reg.RG_Reference = reference + logger.info(f" RG_Reference: {reference}") except Exception: pass if libelle: try: reg.RG_Libelle = libelle + logger.info(f" RG_Libelle: {libelle}") except Exception: pass @@ -300,12 +461,12 @@ def _executer_reglement_com( except Exception: pass - # 6. ÉCRIRE le règlement + # 10. ÉCRIRE le règlement reg.Write() numero = getattr(reg, "RG_Piece", None) logger.info(f" Règlement écrit avec numéro: {numero}") - # 7. Créer le lien règlement-échéance via la factory DU RÈGLEMENT + # 11. Créer le lien règlement-échéance via la factory DU RÈGLEMENT try: logger.info(" Création du lien règlement-échéance...") factory_reg_ech = getattr(reg, "FactoryDocumentReglementEcheance", None) @@ -325,7 +486,7 @@ def _executer_reglement_com( except Exception: continue - # Définir l'échéance - le Reglement est déjà lié via la factory + # Définir l'échéance try: reg_ech.Echeance = echeance logger.info(" Echeance définie") @@ -379,7 +540,6 @@ def _executer_reglement_com( logger.info(" Process() réussi!") return str(numero) if numero else None else: - # Vérifier les erreurs try: errors = process.Errors if errors: @@ -400,18 +560,15 @@ def _executer_reglement_com( try: logger.info("Tentative via modification directe de l'échéance...") - # L'échéance a un attribut Reglement qui est le mode de règlement mode_obj = getattr(echeance, "Reglement", None) if mode_obj: attrs = [a for a in dir(mode_obj) if not a.startswith("_")] logger.info(f" Attributs Reglement échéance: {attrs[:15]}...") - # Vérifier si l'échéance a FactoryDocumentReglementEcheance factory_reg_ech = getattr(echeance, "FactoryDocumentReglementEcheance", None) if factory_reg_ech: logger.info(" FactoryDocumentReglementEcheance trouvée sur échéance") - # Créer le lien depuis l'échéance reg_ech = factory_reg_ech.Create() for iface in [ @@ -425,15 +582,12 @@ def _executer_reglement_com( except Exception: continue - # Ici, l'échéance devrait déjà être liée - # Il faut définir le règlement try: - # Créer un nouveau règlement pour ce lien factory_reg = self.cial.FactoryDocumentReglement new_reg = factory_reg.Create() new_reg = win32com.client.CastTo(new_reg, "IBODocumentReglement") - # Configurer minimalement + # Configurer journal_factory = self.cial.CptaApplication.FactoryJournal journal_persist = journal_factory.ReadNumero(code_journal) if journal_persist: @@ -449,14 +603,37 @@ def _executer_reglement_com( new_reg.RG_Montant = montant new_reg.RG_Impute = 1 - # Écrire le règlement + # Devise si non EUR + if devise_code != 0: + try: + new_reg.RG_Devise = devise_code + new_reg.RG_Cours = cours_devise + new_reg.RG_MontantDev = montant * cours_devise + except Exception: + pass + + # TVA encaissement + if tva_encaissement: + try: + new_reg.RG_Encaissement = 1 + except Exception: + pass + + # Compte général + if compte_general: + try: + cg_factory = self.cial.CptaApplication.FactoryCompteG + cg_persist = cg_factory.ReadNumero(compte_general) + if cg_persist: + new_reg.CompteG = cg_persist + except Exception: + pass + new_reg.Write() logger.info( f" Nouveau règlement créé: {getattr(new_reg, 'RG_Piece', None)}" ) - # Assigner au lien - ici on doit peut-être utiliser un autre attribut - # Puisque reg_ech.Reglement n'est pas settable, essayons via SetDefault try: reg_ech.SetDefault() except Exception: @@ -770,6 +947,315 @@ def lire_reglements_client( } +def lire_modes_reglement(self) -> List[Dict]: + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + cbIndice, + R_Intitule, + R_Code, + R_ModePaieDebit, + R_ModePaieCredit + FROM P_REGLEMENT + WHERE R_Intitule IS NOT NULL AND LTRIM(RTRIM(R_Intitule)) <> '' + ORDER BY cbIndice + """) + + modes = [] + for row in cursor.fetchall(): + intitule = (row[1] or "").strip() + if intitule: # Ne garder que ceux avec un intitulé + modes.append( + { + "code": row[0], # cbIndice + "intitule": intitule, + "code_sage": (row[2] or "").strip(), + "mode_paie_debit": row[3] or 0, + "mode_paie_credit": row[4] or 0, + } + ) + + return modes + + +def _get_modes_reglement_standards() -> List[Dict]: + """Modes de règlement standards Sage si P_REGLEMENT non configuré""" + return [ + {"code": 0, "intitule": "Chèque", "type": "banque"}, + {"code": 1, "intitule": "Virement", "type": "banque"}, + {"code": 2, "intitule": "Espèces", "type": "caisse"}, + {"code": 3, "intitule": "LCR Acceptée", "type": "banque"}, + {"code": 4, "intitule": "LCR non acceptée", "type": "banque"}, + {"code": 5, "intitule": "BOR", "type": "banque"}, + {"code": 6, "intitule": "Prélèvement", "type": "banque"}, + {"code": 7, "intitule": "Carte bancaire", "type": "banque"}, + {"code": 8, "intitule": "Bon d'achat", "type": "autre"}, + ] + + +def lire_devises(self) -> List[Dict]: + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + with self._get_sql_connection() as conn: + cursor = conn.cursor() + + # Vérifier d'abord si F_DEVISE existe + cursor.execute(""" + SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_NAME = 'F_DEVISE' + """) + has_f_devise = cursor.fetchone()[0] > 0 + + if has_f_devise: + cursor.execute(""" + SELECT + D_Code, + D_Intitule, + D_Sigle, + D_CoursActuel + FROM F_DEVISE + ORDER BY D_Code + """) + devises = [] + for row in cursor.fetchall(): + devises.append( + { + "code": row[0], + "intitule": (row[1] or "").strip(), + "sigle": (row[2] or "").strip(), + "cours_actuel": float(row[3] or 1.0), + } + ) + if devises: + return devises + + # Fallback: Lire depuis P_DOSSIER + cursor.execute(""" + SELECT N_DeviseCompte, N_DeviseEquival + FROM P_DOSSIER + """) + row = cursor.fetchone() + + # Devise par défaut basée sur la config dossier + devise_principale = row[0] if row else 0 + + # Retourner les devises standards + devises_standards = [ + { + "code": 0, + "intitule": "Euro", + "sigle": "EUR", + "cours_actuel": 1.0, + "est_principale": devise_principale == 0, + }, + { + "code": 1, + "intitule": "Dollar US", + "sigle": "USD", + "cours_actuel": 1.0, + "est_principale": devise_principale == 1, + }, + { + "code": 2, + "intitule": "Livre Sterling", + "sigle": "GBP", + "cours_actuel": 1.0, + "est_principale": devise_principale == 2, + }, + { + "code": 3, + "intitule": "Franc Suisse", + "sigle": "CHF", + "cours_actuel": 1.0, + "est_principale": devise_principale == 3, + }, + ] + + return devises_standards + + +def lire_journaux_tresorerie(self) -> List[Dict]: + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + JO_Num, + JO_Intitule, + CG_Num, + JO_Type, + JO_Reglement, + JO_Sommeil + FROM F_JOURNAUX + WHERE JO_Type = 2 + AND JO_Reglement = 1 + AND (JO_Sommeil = 0 OR JO_Sommeil IS NULL) + ORDER BY JO_Num + """) + + journaux = [] + for row in cursor.fetchall(): + compte_general = (row[2] or "").strip() + # Déterminer le type basé sur le compte général + if compte_general.startswith("53"): + type_journal = "caisse" + elif compte_general.startswith("51"): + type_journal = "banque" + else: + type_journal = "tresorerie" + + journaux.append( + { + "code": row[0].strip(), + "intitule": (row[1] or "").strip(), + "compte_general": compte_general, + "type": type_journal, + } + ) + + return journaux + + +def lire_comptes_generaux( + self, prefixe: str = None, type_compte: str = None +) -> List[Dict]: + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + # Mapping type -> préfixes de comptes + prefixes_map = { + "client": ["411"], + "fournisseur": ["401"], + "banque": ["51"], + "caisse": ["53"], + "tva_collectee": ["4457"], + "tva_deductible": ["4456"], + "tva": ["445"], + "produit": ["7"], + "charge": ["6"], + } + + with self._get_sql_connection() as conn: + cursor = conn.cursor() + + query = """ + SELECT + CG_Num, + CG_Intitule, + CG_Type, + CG_Raccourci, + CG_Sommeil + FROM F_COMPTEG + WHERE (CG_Sommeil = 0 OR CG_Sommeil IS NULL) + """ + params = [] + + # Appliquer les filtres + if type_compte and type_compte in prefixes_map: + prefixes = prefixes_map[type_compte] + conditions = " OR ".join(["CG_Num LIKE ?" for _ in prefixes]) + query += f" AND ({conditions})" + params.extend([f"{p}%" for p in prefixes]) + elif prefixe: + query += " AND CG_Num LIKE ?" + params.append(f"{prefixe}%") + + query += " ORDER BY CG_Num" + + cursor.execute(query, params) + + comptes = [] + for row in cursor.fetchall(): + comptes.append( + { + "numero": row[0].strip(), + "intitule": (row[1] or "").strip(), + "type": row[2] or 0, + "raccourci": (row[3] or "").strip(), + } + ) + + return comptes + + +def lire_tva_taux(self) -> List[Dict]: + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + TA_No, + TA_Code, + TA_Intitule, + TA_Taux, + TA_TTaux, + TA_Type, + TA_Sens, + CG_Num, + TA_Assujet + FROM F_TAXE + ORDER BY TA_No + """) + + taux = [] + for row in cursor.fetchall(): + taux.append( + { + "numero": row[0], + "code": (row[1] or "").strip(), + "intitule": (row[2] or "").strip(), + "taux": float(row[3] or 0), + "type_taux": row[4] or 0, # 0=Pourcentage, 1=Montant + "type": row[5] or 0, # 0=Collectée, 1=Déductible, etc. + "sens": row[6] or 0, # 0=Vente, 1=Achat + "compte_general": (row[7] or "").strip(), + "assujetti": row[8] or 0, + } + ) + + return taux + + +def lire_parametres_encaissement(self) -> Dict: + if not self.cial: + raise RuntimeError("Connexion Sage non établie") + + with self._get_sql_connection() as conn: + cursor = conn.cursor() + cursor.execute(""" + SELECT + D_TVAEncReg, + D_TVAEncAffect, + N_DeviseCompte + FROM P_DOSSIER + """) + + row = cursor.fetchone() + if row: + return { + "tva_encaissement_regime": row[0] or 0, + "tva_encaissement_affectation": row[1] or 0, + "tva_encaissement_actif": (row[0] or 0) > 0, + "devise_compte": row[2] or 0, + } + + return { + "tva_encaissement_regime": 0, + "tva_encaissement_affectation": 0, + "tva_encaissement_actif": False, + "devise_compte": 0, + } + + __all__ = [ "ModeReglement", "lire_journaux_banque", @@ -779,4 +1265,11 @@ __all__ = [ "regler_factures_client", "lire_reglements_facture", "lire_reglements_client", + "lire_modes_reglement", + "lire_devises", + "lire_journaux_tresorerie", + "lire_comptes_generaux", + "lire_tva_taux", + "lire_parametres_encaissement", + "_get_modes_reglement_standards", ]