Sage100-ws/utils/functions/data/create_doc.py

392 lines
13 KiB
Python

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