Unified creating method for documents
This commit is contained in:
parent
6a346876aa
commit
96021205a4
5 changed files with 499 additions and 1338 deletions
1354
sage_connector.py
1354
sage_connector.py
File diff suppressed because it is too large
Load diff
79
schemas/documents/doc_config.py
Normal file
79
schemas/documents/doc_config.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from typing import Optional
|
||||
from enum import Enum
|
||||
from config import settings
|
||||
|
||||
|
||||
class TypeDocumentVente(Enum):
|
||||
"""Types de documents de vente supportés"""
|
||||
|
||||
DEVIS = 0
|
||||
COMMANDE = 1
|
||||
LIVRAISON = 3
|
||||
FACTURE = 6
|
||||
AVOIR = 5
|
||||
|
||||
|
||||
class ConfigDocument:
|
||||
"""Configuration spécifique pour chaque type de document"""
|
||||
|
||||
def __init__(self, type_doc: TypeDocumentVente):
|
||||
self.type_doc = type_doc
|
||||
self.type_sage = self._get_type_sage()
|
||||
self.champ_date_principale = self._get_champ_date()
|
||||
self.champ_numero = self._get_champ_numero()
|
||||
self.nom_document = self._get_nom_document()
|
||||
self.champ_date_secondaire = self._get_champ_date_secondaire()
|
||||
|
||||
def _get_type_sage(self) -> int:
|
||||
mapping = {
|
||||
TypeDocumentVente.DEVIS: settings.SAGE_TYPE_DEVIS,
|
||||
TypeDocumentVente.COMMANDE: settings.SAGE_TYPE_BON_COMMANDE,
|
||||
TypeDocumentVente.LIVRAISON: settings.SAGE_TYPE_BON_LIVRAISON,
|
||||
TypeDocumentVente.FACTURE: settings.SAGE_TYPE_FACTURE,
|
||||
TypeDocumentVente.AVOIR: settings.SAGE_TYPE_BON_AVOIR,
|
||||
}
|
||||
return mapping[self.type_doc]
|
||||
|
||||
def _get_champ_date(self) -> str:
|
||||
"""Retourne le nom du champ principal dans les données"""
|
||||
mapping = {
|
||||
TypeDocumentVente.DEVIS: "date_devis",
|
||||
TypeDocumentVente.COMMANDE: "date_commande",
|
||||
TypeDocumentVente.LIVRAISON: "date_livraison",
|
||||
TypeDocumentVente.FACTURE: "date_facture",
|
||||
TypeDocumentVente.AVOIR: "date_avoir",
|
||||
}
|
||||
return mapping[self.type_doc]
|
||||
|
||||
def _get_champ_date_secondaire(self) -> Optional[str]:
|
||||
"""Retourne le nom du champ secondaire (date de livraison, etc.)"""
|
||||
mapping = {
|
||||
TypeDocumentVente.DEVIS: "date_livraison",
|
||||
TypeDocumentVente.COMMANDE: "date_livraison",
|
||||
TypeDocumentVente.LIVRAISON: "date_livraison_prevue",
|
||||
TypeDocumentVente.FACTURE: "date_livraison",
|
||||
TypeDocumentVente.AVOIR: "date_livraison",
|
||||
}
|
||||
return mapping.get(self.type_doc)
|
||||
|
||||
def _get_champ_numero(self) -> str:
|
||||
"""Retourne le nom du champ pour le numéro de document dans le résultat"""
|
||||
mapping = {
|
||||
TypeDocumentVente.DEVIS: "numero_devis",
|
||||
TypeDocumentVente.COMMANDE: "numero_commande",
|
||||
TypeDocumentVente.LIVRAISON: "numero_livraison",
|
||||
TypeDocumentVente.FACTURE: "numero_facture",
|
||||
TypeDocumentVente.AVOIR: "numero_avoir",
|
||||
}
|
||||
return mapping[self.type_doc]
|
||||
|
||||
def _get_nom_document(self) -> str:
|
||||
"""Retourne le nom du document pour les logs"""
|
||||
mapping = {
|
||||
TypeDocumentVente.DEVIS: "devis",
|
||||
TypeDocumentVente.COMMANDE: "commande",
|
||||
TypeDocumentVente.LIVRAISON: "livraison",
|
||||
TypeDocumentVente.FACTURE: "facture",
|
||||
TypeDocumentVente.AVOIR: "avoir",
|
||||
}
|
||||
return mapping[self.type_doc]
|
||||
0
utils/functions/data/__init__.py
Normal file
0
utils/functions/data/__init__.py
Normal file
392
utils/functions/data/create_doc.py
Normal file
392
utils/functions/data/create_doc.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
from typing import Dict
|
||||
import win32com.client
|
||||
import pywintypes
|
||||
import time
|
||||
from schemas.documents.doc_config import TypeDocumentVente, ConfigDocument
|
||||
import logging
|
||||
from utils.functions.functions import normaliser_date
|
||||
from utils.tiers.clients.clients_data import _cast_client
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _creer_document_vente_unifie(
|
||||
self, doc_data: dict, type_document: TypeDocumentVente
|
||||
) -> Dict:
|
||||
if not self.cial:
|
||||
raise RuntimeError("Connexion Sage non établie")
|
||||
|
||||
config = ConfigDocument(type_document)
|
||||
logger.info(
|
||||
f"📝 Début création {config.nom_document} pour client {doc_data['client']['code']}"
|
||||
)
|
||||
|
||||
try:
|
||||
with self._com_context(), self._lock_com:
|
||||
transaction_active = False
|
||||
|
||||
try:
|
||||
# Démarrage transaction
|
||||
try:
|
||||
self.cial.CptaApplication.BeginTrans()
|
||||
transaction_active = True
|
||||
logger.debug("✓ Transaction Sage démarrée")
|
||||
except Exception as e:
|
||||
logger.warning(f"BeginTrans échoué (non critique): {e}")
|
||||
|
||||
# Création du document
|
||||
process = self.cial.CreateProcess_Document(config.type_sage)
|
||||
doc = process.Document
|
||||
|
||||
try:
|
||||
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(f"✓ Document {config.nom_document} créé")
|
||||
|
||||
# ===== DATES =====
|
||||
doc.DO_Date = pywintypes.Time(
|
||||
normaliser_date(doc_data.get(config.champ_date_principale))
|
||||
)
|
||||
|
||||
# Date secondaire (livraison, etc.)
|
||||
if config.champ_date_secondaire and doc_data.get(
|
||||
config.champ_date_secondaire
|
||||
):
|
||||
doc.DO_DateLivr = pywintypes.Time(
|
||||
normaliser_date(doc_data[config.champ_date_secondaire])
|
||||
)
|
||||
logger.info(
|
||||
f"✓ {config.champ_date_secondaire}: {doc_data[config.champ_date_secondaire]}"
|
||||
)
|
||||
|
||||
# ===== CLIENT =====
|
||||
factory_client = self.cial.CptaApplication.FactoryClient
|
||||
persist_client = factory_client.ReadNumero(doc_data["client"]["code"])
|
||||
|
||||
if not persist_client:
|
||||
raise ValueError(f"Client {doc_data['client']['code']} introuvable")
|
||||
|
||||
client_obj = _cast_client(persist_client)
|
||||
if not client_obj:
|
||||
raise ValueError("Impossible de charger le client")
|
||||
|
||||
doc.SetDefaultClient(client_obj)
|
||||
doc.Write()
|
||||
logger.info(f"✓ Client {doc_data['client']['code']} associé")
|
||||
|
||||
# ===== RÉFÉRENCE =====
|
||||
if doc_data.get("reference"):
|
||||
try:
|
||||
doc.DO_Ref = doc_data["reference"]
|
||||
logger.info(f"✓ Référence: {doc_data['reference']}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Référence non définie: {e}")
|
||||
|
||||
# ===== CONFIGURATION SPÉCIFIQUE FACTURE =====
|
||||
if type_document == TypeDocumentVente.FACTURE:
|
||||
_configurer_facture(self, doc)
|
||||
|
||||
# ===== FACTORY LIGNES =====
|
||||
try:
|
||||
factory_lignes = doc.FactoryDocumentLigne
|
||||
except Exception:
|
||||
factory_lignes = doc.FactoryDocumentVenteLigne
|
||||
|
||||
factory_article = self.cial.FactoryArticle
|
||||
|
||||
logger.info(f"📦 Ajout de {len(doc_data['lignes'])} lignes...")
|
||||
|
||||
# ===== TRAITEMENT DES LIGNES =====
|
||||
for idx, ligne_data in enumerate(doc_data["lignes"], 1):
|
||||
_ajouter_ligne_document(
|
||||
cial=self.cial,
|
||||
factory_lignes=factory_lignes,
|
||||
factory_article=factory_article,
|
||||
ligne_data=ligne_data,
|
||||
idx=idx,
|
||||
doc=doc,
|
||||
)
|
||||
|
||||
# ===== VALIDATION =====
|
||||
logger.info("💾 Validation du document...")
|
||||
|
||||
# Pour les factures, réassocier le client avant validation
|
||||
if type_document == TypeDocumentVente.FACTURE:
|
||||
try:
|
||||
doc.SetClient(client_obj)
|
||||
logger.debug(" ↳ Client réassocié avant validation")
|
||||
except Exception:
|
||||
try:
|
||||
doc.SetDefaultClient(client_obj)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
doc.Write()
|
||||
|
||||
# Process() sauf pour devis en brouillon
|
||||
if type_document != TypeDocumentVente.DEVIS:
|
||||
process.Process()
|
||||
logger.info("✓ Process() appelé")
|
||||
else:
|
||||
try:
|
||||
process.Process()
|
||||
logger.info("✓ Process() appelé (devis)")
|
||||
except Exception:
|
||||
logger.debug(" ↳ Process() ignoré pour devis brouillon")
|
||||
|
||||
# Commit transaction
|
||||
if transaction_active:
|
||||
try:
|
||||
self.cial.CptaApplication.CommitTrans()
|
||||
logger.info("✓ Transaction committée")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# ===== RÉCUPÉRATION DU NUMÉRO =====
|
||||
numero_document = _recuperer_numero_document(process, doc)
|
||||
|
||||
if not numero_document:
|
||||
raise RuntimeError(
|
||||
f"Numéro {config.nom_document} vide après création"
|
||||
)
|
||||
|
||||
logger.info(f"📄 Numéro: {numero_document}")
|
||||
|
||||
# ===== RELECTURE POUR TOTAUX =====
|
||||
doc_final_data = _relire_document_final(
|
||||
self,
|
||||
config=config,
|
||||
numero_document=numero_document,
|
||||
doc_data=doc_data,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"✅ {config.nom_document.upper()} CRÉÉ: "
|
||||
f"{numero_document} - {doc_final_data['total_ttc']}€ TTC"
|
||||
)
|
||||
|
||||
return doc_final_data
|
||||
|
||||
except Exception:
|
||||
if transaction_active:
|
||||
try:
|
||||
self.cial.CptaApplication.RollbackTrans()
|
||||
logger.error(" Transaction annulée (rollback)")
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f" ERREUR CRÉATION {config.nom_document.upper()}: {e}", exc_info=True
|
||||
)
|
||||
raise RuntimeError(f"Échec création {config.nom_document}: {str(e)}")
|
||||
|
||||
|
||||
def _appliquer_remise_ligne(ligne_obj, remise_pourcent: float) -> bool:
|
||||
"""Applique la remise via FromString - SOLUTION FINALE"""
|
||||
try:
|
||||
import pythoncom
|
||||
|
||||
dispatch = ligne_obj._oleobj_
|
||||
|
||||
# 1. Récupérer l'objet Remise
|
||||
dispid = dispatch.GetIDsOfNames(0, "Remise")
|
||||
remise_obj = dispatch.Invoke(dispid, 0, pythoncom.DISPATCH_PROPERTYGET, 1)
|
||||
remise_wrapper = win32com.client.Dispatch(remise_obj)
|
||||
|
||||
# 2. Définir la remise via FromString
|
||||
remise_wrapper.FromString(f"{remise_pourcent}%")
|
||||
|
||||
# 3. Calcul (optionnel mais recommandé)
|
||||
try:
|
||||
remise_wrapper.Calcul()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 4. Write la ligne
|
||||
ligne_obj.Write()
|
||||
|
||||
logger.info(f" ✅ Remise {remise_pourcent}% appliquée")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f" ❌ Erreur remise: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _ajouter_ligne_document(
|
||||
cial, factory_lignes, factory_article, ligne_data: dict, idx: int, doc
|
||||
) -> None:
|
||||
"""VERSION FINALE AVEC REMISES FONCTIONNELLES"""
|
||||
logger.info(f" ├─ Ligne {idx}: {ligne_data['article_code']}")
|
||||
|
||||
# ===== CRÉATION LIGNE =====
|
||||
persist_article = factory_article.ReadReference(ligne_data["article_code"])
|
||||
if not persist_article:
|
||||
raise ValueError(f"Article {ligne_data['article_code']} introuvable")
|
||||
|
||||
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
|
||||
article_obj.Read()
|
||||
|
||||
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
||||
designation_sage = getattr(article_obj, "AR_Design", "")
|
||||
|
||||
ligne_persist = factory_lignes.Create()
|
||||
try:
|
||||
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentLigne3")
|
||||
except:
|
||||
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
|
||||
|
||||
quantite = float(ligne_data["quantite"])
|
||||
|
||||
# ===== ASSOCIATION ARTICLE =====
|
||||
try:
|
||||
ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite)
|
||||
except:
|
||||
try:
|
||||
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
||||
except:
|
||||
ligne_obj.DL_Design = designation_sage
|
||||
ligne_obj.DL_Qte = quantite
|
||||
|
||||
# ===== PRIX =====
|
||||
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
||||
prix_perso = ligne_data.get("prix_unitaire_ht")
|
||||
|
||||
if prix_perso and prix_perso > 0:
|
||||
ligne_obj.DL_PrixUnitaire = float(prix_perso)
|
||||
elif prix_auto == 0 and prix_sage > 0:
|
||||
ligne_obj.DL_PrixUnitaire = float(prix_sage)
|
||||
|
||||
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0))
|
||||
logger.info(f" 💰 Prix: {prix_final}€")
|
||||
|
||||
# ===== WRITE INITIAL =====
|
||||
ligne_obj.Write()
|
||||
|
||||
# ===== APPLICATION REMISE (TOUTES LES LIGNES!) =====
|
||||
remise = ligne_data.get("remise_pourcentage", 0)
|
||||
if remise and remise > 0:
|
||||
logger.info(f" 🎯 Application remise {remise}%...")
|
||||
_appliquer_remise_ligne(ligne_obj, remise)
|
||||
|
||||
logger.info(f" ✓ Ligne {idx} terminée")
|
||||
|
||||
|
||||
def _configurer_facture(self, doc) -> None:
|
||||
"""Configuration spécifique pour les factures"""
|
||||
logger.info(" 🔧 Configuration spécifique facture...")
|
||||
|
||||
try:
|
||||
if hasattr(doc, "DO_CodeJournal"):
|
||||
try:
|
||||
param_societe = self.cial.CptaApplication.ParametreSociete
|
||||
journal_defaut = getattr(param_societe, "P_CodeJournalVte", "VTE")
|
||||
doc.DO_CodeJournal = journal_defaut
|
||||
logger.debug(f" ✓ Code journal: {journal_defaut}")
|
||||
except Exception:
|
||||
doc.DO_CodeJournal = "VTE"
|
||||
logger.debug(" ✓ Code journal: VTE (défaut)")
|
||||
except Exception as e:
|
||||
logger.debug(f" Code journal: {e}")
|
||||
|
||||
try:
|
||||
if hasattr(doc, "DO_Souche"):
|
||||
doc.DO_Souche = 0
|
||||
logger.debug(" ✓ Souche: 0")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if hasattr(doc, "DO_Regime"):
|
||||
doc.DO_Regime = 0
|
||||
logger.debug(" ✓ Régime: 0")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _recuperer_numero_document(process, doc) -> str:
|
||||
"""Récupère le numéro du document créé"""
|
||||
numero = None
|
||||
|
||||
try:
|
||||
doc_result = process.DocumentResult
|
||||
if doc_result:
|
||||
doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3")
|
||||
doc_result.Read()
|
||||
numero = getattr(doc_result, "DO_Piece", "")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not numero:
|
||||
numero = getattr(doc, "DO_Piece", "")
|
||||
|
||||
return numero
|
||||
|
||||
|
||||
def _relire_document_final(
|
||||
self, config: ConfigDocument, numero_document: str, doc_data: dict
|
||||
) -> Dict:
|
||||
"""
|
||||
Relit le document pour obtenir les totaux calculés par Sage
|
||||
"""
|
||||
factory_doc = self.cial.FactoryDocumentVente
|
||||
persist_reread = factory_doc.ReadPiece(config.type_sage, numero_document)
|
||||
|
||||
if persist_reread:
|
||||
doc_final = win32com.client.CastTo(persist_reread, "IBODocumentVente3")
|
||||
doc_final.Read()
|
||||
|
||||
total_ht = float(getattr(doc_final, "DO_TotalHT", 0.0))
|
||||
total_ttc = float(getattr(doc_final, "DO_TotalTTC", 0.0))
|
||||
reference_finale = getattr(doc_final, "DO_Ref", "")
|
||||
|
||||
# Date secondaire
|
||||
date_secondaire_value = None
|
||||
if config.champ_date_secondaire:
|
||||
try:
|
||||
date_livr = getattr(doc_final, "DO_DateLivr", None)
|
||||
if date_livr:
|
||||
date_secondaire_value = date_livr.strftime("%Y-%m-%d")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
# Valeurs par défaut si relecture échoue
|
||||
total_ht = 0.0
|
||||
total_ttc = 0.0
|
||||
reference_finale = doc_data.get("reference", "")
|
||||
date_secondaire_value = doc_data.get(config.champ_date_secondaire)
|
||||
|
||||
# Construction du résultat
|
||||
resultat = {
|
||||
config.champ_numero: numero_document,
|
||||
"total_ht": total_ht,
|
||||
"total_ttc": total_ttc,
|
||||
"nb_lignes": len(doc_data["lignes"]),
|
||||
"client_code": doc_data["client"]["code"],
|
||||
config.champ_date_principale: str(
|
||||
normaliser_date(doc_data.get(config.champ_date_principale))
|
||||
),
|
||||
"reference": reference_finale,
|
||||
}
|
||||
|
||||
# Ajout date secondaire si applicable
|
||||
if config.champ_date_secondaire:
|
||||
resultat[config.champ_date_secondaire] = date_secondaire_value
|
||||
|
||||
return resultat
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_creer_document_vente_unifie",
|
||||
"_ajouter_ligne_document",
|
||||
"_configurer_facture",
|
||||
"_recuperer_numero_document",
|
||||
"_relire_document_final",
|
||||
]
|
||||
Loading…
Reference in a new issue