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

652 lines
23 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(
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:
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}")
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éé")
date_principale = normaliser_date(
doc_data.get(config.champ_date_principale)
)
doc.DO_Date = pywintypes.Time(date_principale)
try:
doc.DO_Heure = pywintypes.Time(date_principale)
logger.debug(
f"DO_Heure défini: {date_principale.strftime('%H:%M:%S')}"
)
except Exception as e:
logger.debug(f"DO_Heure non défini: {e}")
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]}"
)
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é")
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}")
if type_document == TypeDocumentVente.FACTURE:
_configurer_facture(self, doc)
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...")
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,
)
logger.info(" Validation du document...")
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()
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")
if transaction_active:
try:
self.cial.CptaApplication.CommitTrans()
logger.info(" Transaction committée")
except Exception:
pass
time.sleep(2)
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}")
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_
dispid = dispatch.GetIDsOfNames(0, "Remise")
remise_obj = dispatch.Invoke(dispid, 0, pythoncom.DISPATCH_PROPERTYGET, 1)
remise_wrapper = win32com.client.Dispatch(remise_obj)
remise_wrapper.FromString(f"{remise_pourcent}%")
try:
remise_wrapper.Calcul()
except Exception:
pass
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']}")
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 Exception:
ligne_obj = win32com.client.CastTo(ligne_persist, "IBODocumentVenteLigne3")
quantite = float(ligne_data["quantite"])
try:
ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite)
except Exception:
try:
ligne_obj.SetDefaultArticle(article_obj, quantite)
except Exception:
ligne_obj.DL_Design = designation_sage
ligne_obj.DL_Qte = quantite
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}")
ligne_obj.Write()
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,
client_code_fallback: str = None,
) -> 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)
client_code = None
date_secondaire_value = None
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", "")
try:
client_obj = getattr(doc_final, "Client", None)
if client_obj:
client_obj.Read()
client_code = getattr(client_obj, "CT_Num", "").strip()
except Exception:
pass
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:
total_ht = 0.0
total_ttc = 0.0
reference_finale = doc_data.get("reference", "")
date_secondaire_value = doc_data.get(config.champ_date_secondaire)
if not client_code:
client_code = client_code_fallback or doc_data.get("client", {}).get("code", "")
resultat = {
config.champ_numero: numero_document,
"total_ht": total_ht,
"total_ttc": total_ttc,
"nb_lignes": len(doc_data.get("lignes", [])),
"client_code": client_code,
config.champ_date_principale: str(
normaliser_date(doc_data.get(config.champ_date_principale))
)
if doc_data.get(config.champ_date_principale)
else None,
"reference": reference_finale,
}
if config.champ_date_secondaire:
resultat[config.champ_date_secondaire] = date_secondaire_value
return resultat
def modifier_document_vente(
self, numero: str, doc_data: dict, type_document: TypeDocumentVente
) -> Dict:
if not self.cial:
raise RuntimeError("Connexion Sage non établie")
config = ConfigDocument(type_document)
logger.info(f" === MODIFICATION {config.nom_document.upper()} {numero} ===")
try:
with self._com_context(), self._lock_com:
logger.info(" Chargement document...")
factory = self.cial.FactoryDocumentVente
persist = None
for type_test in [config.type_sage, 50]:
try:
persist_test = factory.ReadPiece(type_test, numero)
if persist_test:
persist = persist_test
logger.info(f" Document trouvé (type={type_test})")
break
except Exception:
continue
if not persist:
raise ValueError(
f"{config.nom_document.capitalize()} {numero} introuvable"
)
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
statut_actuel = getattr(doc, "DO_Statut", 0)
if statut_actuel == 5:
raise ValueError(f"Le {config.nom_document} a déjà été transformé")
if statut_actuel == 6:
raise ValueError(f"Le {config.nom_document} est annulé")
client_code_initial = ""
try:
client_obj = getattr(doc, "Client", None)
if client_obj:
client_obj.Read()
client_code_initial = getattr(client_obj, "CT_Num", "").strip()
logger.info(f" Client initial: {client_code_initial}")
except Exception as e:
logger.error(f" Erreur lecture client: {e}")
if not client_code_initial:
raise ValueError("Client introuvable dans le document")
nb_lignes_initial = 0
try:
factory_lignes = getattr(doc, "FactoryDocumentLigne", None) or getattr(
doc, "FactoryDocumentVenteLigne", None
)
index = 1
while index <= 100:
try:
ligne_p = factory_lignes.List(index)
if ligne_p is None:
break
nb_lignes_initial += 1
index += 1
except Exception:
break
logger.info(f" Lignes existantes: {nb_lignes_initial}")
except Exception as e:
logger.warning(f" Erreur comptage lignes: {e}")
champs_modifies = []
modif_date = config.champ_date_principale in doc_data
modif_date_sec = (
config.champ_date_secondaire
and config.champ_date_secondaire in doc_data
)
modif_statut = "statut" in doc_data
modif_ref = "reference" in doc_data
modif_lignes = "lignes" in doc_data and doc_data["lignes"] is not None
logger.info(" Modifications demandées:")
logger.info(f" {config.champ_date_principale}: {modif_date}")
if config.champ_date_secondaire:
logger.info(f" {config.champ_date_secondaire}: {modif_date_sec}")
logger.info(f" Statut: {modif_statut}")
logger.info(f" Référence: {modif_ref}")
logger.info(f" Lignes: {modif_lignes}")
doc_data_temp = doc_data.copy()
reference_a_modifier = None
statut_a_modifier = None
if modif_lignes:
if modif_ref:
reference_a_modifier = doc_data_temp.pop("reference")
modif_ref = False
if modif_statut:
statut_a_modifier = doc_data_temp.pop("statut")
modif_statut = False
logger.info(" Test Write() basique...")
try:
doc.Write()
doc.Read()
logger.info(" Write() basique OK")
except Exception as e:
logger.error(f" Document verrouillé: {e}")
raise ValueError(f"Document verrouillé: {e}")
if not modif_lignes and (
modif_date or modif_date_sec or modif_statut or modif_ref
):
logger.info(" Modifications simples...")
if modif_date:
date_principale = normaliser_date(
doc_data_temp.get(config.champ_date_principale)
)
doc.DO_Date = pywintypes.Time(date_principale)
try:
doc.DO_Heure = pywintypes.Time(date_principale)
except Exception:
pass
champs_modifies.append(config.champ_date_principale)
if modif_date_sec:
doc.DO_DateLivr = pywintypes.Time(
normaliser_date(doc_data_temp[config.champ_date_secondaire])
)
champs_modifies.append(config.champ_date_secondaire)
if modif_statut:
doc.DO_Statut = doc_data_temp["statut"]
champs_modifies.append("statut")
if modif_ref:
doc.DO_Ref = doc_data_temp["reference"]
champs_modifies.append("reference")
if type_document == TypeDocumentVente.FACTURE:
_configurer_facture(self, doc)
doc.Write()
logger.info(" Modifications appliquées")
elif modif_lignes:
logger.info(" REMPLACEMENT COMPLET DES LIGNES...")
if modif_date:
doc.DO_Date = pywintypes.Time(
normaliser_date(doc_data_temp.get(config.champ_date_principale))
)
champs_modifies.append(config.champ_date_principale)
if modif_date_sec:
doc.DO_DateLivr = pywintypes.Time(
normaliser_date(doc_data_temp[config.champ_date_secondaire])
)
champs_modifies.append(config.champ_date_secondaire)
if type_document == TypeDocumentVente.FACTURE:
_configurer_facture(self, doc)
nouvelles_lignes = doc_data["lignes"]
nb_nouvelles = len(nouvelles_lignes)
logger.info(f" {nb_lignes_initial}{nb_nouvelles} lignes")
try:
factory_lignes = doc.FactoryDocumentLigne
except Exception:
factory_lignes = doc.FactoryDocumentVenteLigne
factory_article = self.cial.FactoryArticle
if nb_lignes_initial > 0:
logger.info(f" Suppression {nb_lignes_initial} lignes...")
for idx in range(nb_lignes_initial, 0, -1):
try:
ligne_p = factory_lignes.List(idx)
if ligne_p:
ligne = win32com.client.CastTo(
ligne_p, "IBODocumentLigne3"
)
ligne.Read()
ligne.Remove()
except Exception as e:
logger.warning(f" Ligne {idx}: {e}")
logger.info(" Lignes supprimées")
logger.info(f" Ajout {nb_nouvelles} lignes...")
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
_ajouter_ligne_document(
cial=self.cial,
factory_lignes=factory_lignes,
factory_article=factory_article,
ligne_data=ligne_data,
idx=idx,
doc=doc,
)
logger.info(" Nouvelles lignes ajoutées avec remises")
doc.Write()
time.sleep(0.5)
doc.Read()
champs_modifies.append("lignes")
if reference_a_modifier is not None:
try:
logger.info(
f" Modification référence: '{reference_a_modifier}'"
)
doc.DO_Ref = (
str(reference_a_modifier) if reference_a_modifier else ""
)
doc.Write()
time.sleep(0.5)
doc.Read()
champs_modifies.append("reference")
except Exception as e:
logger.warning(f" Référence: {e}")
if statut_a_modifier is not None:
try:
logger.info(f" Modification statut: {statut_a_modifier}")
doc.DO_Statut = int(statut_a_modifier)
doc.Write()
time.sleep(0.5)
doc.Read()
champs_modifies.append("statut")
except Exception as e:
logger.warning(f" Statut: {e}")
resultat = _relire_document_final(
self, config, numero, doc_data, client_code_fallback=client_code_initial
)
resultat["champs_modifies"] = champs_modifies
logger.info(f" {config.nom_document.upper()} {numero} MODIFIÉ")
logger.info(
f" Totaux: {resultat['total_ht']}€ HT / {resultat['total_ttc']}€ TTC"
)
logger.info(f" Champs modifiés: {champs_modifies}")
return resultat
except ValueError as e:
logger.error(f" ERREUR MÉTIER: {e}")
raise
except Exception as e:
logger.error(f" ERREUR TECHNIQUE: {e}", exc_info=True)
raise RuntimeError(f"Erreur Sage: {str(e)}")
__all__ = [
"creer_document_vente",
"_ajouter_ligne_document",
"_configurer_facture",
"_recuperer_numero_document",
"_relire_document_final",
"modifier_document_vente",
]