feat(sage): add invoice creation and update endpoints
This commit is contained in:
parent
5a5b6307b9
commit
ec5a0f0089
2 changed files with 597 additions and 0 deletions
64
main.py
64
main.py
|
|
@ -172,6 +172,19 @@ class AvoirUpdateGatewayRequest(BaseModel):
|
||||||
numero: str
|
numero: str
|
||||||
avoir_data: Dict
|
avoir_data: Dict
|
||||||
|
|
||||||
|
class FactureCreateGatewayRequest(BaseModel):
|
||||||
|
"""Création d'une facture côté gateway"""
|
||||||
|
client_id: str
|
||||||
|
date_facture: Optional[date] = None
|
||||||
|
lignes: List[Dict]
|
||||||
|
reference: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FactureUpdateGatewayRequest(BaseModel):
|
||||||
|
"""Modèle pour modification facture côté gateway"""
|
||||||
|
numero: str
|
||||||
|
facture_data: Dict
|
||||||
|
|
||||||
# =====================================================
|
# =====================================================
|
||||||
# SÉCURITÉ
|
# SÉCURITÉ
|
||||||
# =====================================================
|
# =====================================================
|
||||||
|
|
@ -2957,6 +2970,57 @@ def modifier_avoir_endpoint(req: AvoirUpdateGatewayRequest):
|
||||||
logger.error(f"Erreur technique modification avoir: {e}")
|
logger.error(f"Erreur technique modification avoir: {e}")
|
||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.post("/sage/factures/create", dependencies=[Depends(verify_token)])
|
||||||
|
def creer_facture_endpoint(req: FactureCreateGatewayRequest):
|
||||||
|
"""
|
||||||
|
➕ Création d'une facture dans Sage
|
||||||
|
|
||||||
|
⚠️ NOTE: Les factures peuvent avoir des champs obligatoires supplémentaires
|
||||||
|
selon la configuration Sage (DO_CodeJournal, DO_Souche, etc.)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Vérifier que le client existe
|
||||||
|
client = sage.lire_client(req.client_id)
|
||||||
|
if not client:
|
||||||
|
raise HTTPException(404, f"Client {req.client_id} introuvable")
|
||||||
|
|
||||||
|
# Préparer les données pour le connecteur
|
||||||
|
facture_data = {
|
||||||
|
"client": {"code": req.client_id, "intitule": ""},
|
||||||
|
"date_facture": req.date_facture or date.today(),
|
||||||
|
"reference": req.reference,
|
||||||
|
"lignes": req.lignes,
|
||||||
|
}
|
||||||
|
|
||||||
|
resultat = sage.creer_facture_enrichi(facture_data)
|
||||||
|
return {"success": True, "data": resultat}
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"Erreur métier création facture: {e}")
|
||||||
|
raise HTTPException(400, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur technique création facture: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/sage/factures/update", dependencies=[Depends(verify_token)])
|
||||||
|
def modifier_facture_endpoint(req: FactureUpdateGatewayRequest):
|
||||||
|
"""
|
||||||
|
✏️ Modification d'une facture dans Sage
|
||||||
|
|
||||||
|
⚠️ ATTENTION: Les factures comptabilisées peuvent être verrouillées
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resultat = sage.modifier_facture(req.numero, req.facture_data)
|
||||||
|
return {"success": True, "data": resultat}
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"Erreur métier modification facture: {e}")
|
||||||
|
raise HTTPException(404, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur technique modification facture: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
# =====================================================
|
# =====================================================
|
||||||
# LANCEMENT
|
# LANCEMENT
|
||||||
|
|
|
||||||
|
|
@ -5386,6 +5386,539 @@ class SageConnector:
|
||||||
logger.error(f"❌ Erreur technique: {e}", exc_info=True)
|
logger.error(f"❌ Erreur technique: {e}", exc_info=True)
|
||||||
raise RuntimeError(f"Erreur Sage: {str(e)}")
|
raise RuntimeError(f"Erreur Sage: {str(e)}")
|
||||||
|
|
||||||
|
def creer_facture_enrichi(self, facture_data: dict) -> Dict:
|
||||||
|
"""
|
||||||
|
➕ Création d'une facture (type 60 = Facture)
|
||||||
|
|
||||||
|
⚠️ ATTENTION: Les factures ont souvent des champs obligatoires supplémentaires
|
||||||
|
selon la configuration Sage (DO_CodeJournal, DO_Souche, DO_Regime, etc.)
|
||||||
|
|
||||||
|
✅ Gestion identique aux autres documents + champs spécifiques factures
|
||||||
|
"""
|
||||||
|
if not self.cial:
|
||||||
|
raise RuntimeError("Connexion Sage non établie")
|
||||||
|
|
||||||
|
logger.info(f"🚀 Début création facture pour client {facture_data['client']['code']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self._com_context(), self._lock_com:
|
||||||
|
transaction_active = False
|
||||||
|
try:
|
||||||
|
self.cial.CptaApplication.BeginTrans()
|
||||||
|
transaction_active = True
|
||||||
|
logger.debug("✅ Transaction Sage démarrée")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Création document FACTURE (type 60)
|
||||||
|
process = self.cial.CreateProcess_Document(settings.SAGE_TYPE_FACTURE)
|
||||||
|
doc = process.Document
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = win32com.client.CastTo(doc, "IBODocumentVente3")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info("📄 Document facture créé")
|
||||||
|
|
||||||
|
# Date
|
||||||
|
import pywintypes
|
||||||
|
|
||||||
|
if isinstance(facture_data["date_facture"], str):
|
||||||
|
date_obj = datetime.fromisoformat(facture_data["date_facture"])
|
||||||
|
elif isinstance(facture_data["date_facture"], date):
|
||||||
|
date_obj = datetime.combine(facture_data["date_facture"], datetime.min.time())
|
||||||
|
else:
|
||||||
|
date_obj = datetime.now()
|
||||||
|
|
||||||
|
doc.DO_Date = pywintypes.Time(date_obj)
|
||||||
|
logger.info(f"📅 Date définie: {date_obj.date()}")
|
||||||
|
|
||||||
|
# Client (CRITIQUE)
|
||||||
|
factory_client = self.cial.CptaApplication.FactoryClient
|
||||||
|
persist_client = factory_client.ReadNumero(facture_data["client"]["code"])
|
||||||
|
|
||||||
|
if not persist_client:
|
||||||
|
raise ValueError(f"Client {facture_data['client']['code']} introuvable")
|
||||||
|
|
||||||
|
client_obj = self._cast_client(persist_client)
|
||||||
|
if not client_obj:
|
||||||
|
raise ValueError(f"Impossible de charger le client")
|
||||||
|
|
||||||
|
doc.SetDefaultClient(client_obj)
|
||||||
|
doc.Write()
|
||||||
|
logger.info(f"👤 Client {facture_data['client']['code']} associé")
|
||||||
|
|
||||||
|
# Référence externe (optionnelle)
|
||||||
|
if facture_data.get("reference"):
|
||||||
|
try:
|
||||||
|
doc.DO_Ref = facture_data["reference"]
|
||||||
|
logger.info(f"📖 Référence: {facture_data['reference']}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# CHAMPS SPÉCIFIQUES FACTURES
|
||||||
|
# ============================================
|
||||||
|
logger.info("⚙️ Configuration champs spécifiques factures...")
|
||||||
|
|
||||||
|
# Code journal (si disponible)
|
||||||
|
try:
|
||||||
|
if hasattr(doc, "DO_CodeJournal"):
|
||||||
|
# Essayer de récupérer le code journal par défaut
|
||||||
|
try:
|
||||||
|
param_societe = self.cial.CptaApplication.ParametreSociete
|
||||||
|
journal_defaut = getattr(param_societe, "P_CodeJournalVte", "VTE")
|
||||||
|
doc.DO_CodeJournal = journal_defaut
|
||||||
|
logger.info(f" ✅ Code journal: {journal_defaut}")
|
||||||
|
except:
|
||||||
|
doc.DO_CodeJournal = "VTE"
|
||||||
|
logger.info(" ✅ Code journal: VTE (défaut)")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f" ⚠️ Code journal: {e}")
|
||||||
|
|
||||||
|
# Souche (si disponible)
|
||||||
|
try:
|
||||||
|
if hasattr(doc, "DO_Souche"):
|
||||||
|
doc.DO_Souche = 0
|
||||||
|
logger.debug(" ✅ Souche: 0 (défaut)")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Régime (si disponible)
|
||||||
|
try:
|
||||||
|
if hasattr(doc, "DO_Regime"):
|
||||||
|
doc.DO_Regime = 0
|
||||||
|
logger.debug(" ✅ Régime: 0 (défaut)")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Lignes
|
||||||
|
try:
|
||||||
|
factory_lignes = doc.FactoryDocumentLigne
|
||||||
|
except:
|
||||||
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
||||||
|
|
||||||
|
factory_article = self.cial.FactoryArticle
|
||||||
|
|
||||||
|
logger.info(f"📦 Ajout de {len(facture_data['lignes'])} lignes...")
|
||||||
|
|
||||||
|
for idx, ligne_data in enumerate(facture_data["lignes"], 1):
|
||||||
|
logger.info(f"--- Ligne {idx}: {ligne_data['article_code']} ---")
|
||||||
|
|
||||||
|
# Charger l'article RÉEL depuis Sage
|
||||||
|
persist_article = factory_article.ReadReference(ligne_data["article_code"])
|
||||||
|
|
||||||
|
if not persist_article:
|
||||||
|
raise ValueError(f"❌ Article {ligne_data['article_code']} introuvable dans Sage")
|
||||||
|
|
||||||
|
article_obj = win32com.client.CastTo(persist_article, "IBOArticle3")
|
||||||
|
article_obj.Read()
|
||||||
|
|
||||||
|
# Récupérer le prix de vente RÉEL
|
||||||
|
prix_sage = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
||||||
|
designation_sage = getattr(article_obj, "AR_Design", "")
|
||||||
|
logger.info(f"💰 Prix Sage: {prix_sage}€")
|
||||||
|
|
||||||
|
if prix_sage == 0:
|
||||||
|
logger.warning(f"⚠️ Article {ligne_data['article_code']} a un prix = 0€ (toléré)")
|
||||||
|
|
||||||
|
# Créer la ligne
|
||||||
|
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"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite)
|
||||||
|
logger.info(f"✅ Article associé via SetDefaultArticleReference")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ SetDefaultArticleReference échoué: {e}, tentative avec objet")
|
||||||
|
try:
|
||||||
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
||||||
|
logger.info(f"✅ Article associé via SetDefaultArticle")
|
||||||
|
except Exception as e2:
|
||||||
|
logger.error(f"❌ Toutes les méthodes ont échoué")
|
||||||
|
ligne_obj.DL_Design = designation_sage or ligne_data.get("designation", "")
|
||||||
|
ligne_obj.DL_Qte = quantite
|
||||||
|
logger.warning("⚠️ Configuration manuelle appliquée")
|
||||||
|
|
||||||
|
# Vérifier le prix automatique
|
||||||
|
prix_auto = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
||||||
|
logger.info(f"💰 Prix auto chargé: {prix_auto}€")
|
||||||
|
|
||||||
|
# Ajuster le prix si nécessaire
|
||||||
|
prix_a_utiliser = ligne_data.get("prix_unitaire_ht")
|
||||||
|
|
||||||
|
if prix_a_utiliser is not None and prix_a_utiliser > 0:
|
||||||
|
ligne_obj.DL_PrixUnitaire = float(prix_a_utiliser)
|
||||||
|
logger.info(f"💰 Prix personnalisé: {prix_a_utiliser}€")
|
||||||
|
elif prix_auto == 0 and prix_sage > 0:
|
||||||
|
ligne_obj.DL_PrixUnitaire = float(prix_sage)
|
||||||
|
logger.info(f"💰 Prix Sage forcé: {prix_sage}€")
|
||||||
|
elif prix_auto > 0:
|
||||||
|
logger.info(f"💰 Prix auto conservé: {prix_auto}€")
|
||||||
|
|
||||||
|
prix_final = float(getattr(ligne_obj, "DL_PrixUnitaire", 0.0))
|
||||||
|
montant_ligne = quantite * prix_final
|
||||||
|
logger.info(f"✅ {quantite} x {prix_final}€ = {montant_ligne}€")
|
||||||
|
|
||||||
|
# Remise
|
||||||
|
remise = ligne_data.get("remise_pourcentage", 0)
|
||||||
|
if remise > 0:
|
||||||
|
try:
|
||||||
|
ligne_obj.DL_Remise01REM_Valeur = float(remise)
|
||||||
|
ligne_obj.DL_Remise01REM_Type = 0
|
||||||
|
montant_apres_remise = montant_ligne * (1 - remise / 100)
|
||||||
|
logger.info(f"🎁 Remise {remise}% → {montant_apres_remise}€")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ Remise non appliquée: {e}")
|
||||||
|
|
||||||
|
# Écrire la ligne
|
||||||
|
ligne_obj.Write()
|
||||||
|
logger.info(f"✅ Ligne {idx} écrite")
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# VALIDATION FINALE
|
||||||
|
# ============================================
|
||||||
|
logger.info("💾 Validation facture...")
|
||||||
|
|
||||||
|
# Réassocier le client avant validation (critique pour factures)
|
||||||
|
try:
|
||||||
|
doc.SetClient(client_obj)
|
||||||
|
logger.debug(" ✅ Client réassocié avant validation")
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
doc.SetDefaultClient(client_obj)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
doc.Write()
|
||||||
|
|
||||||
|
logger.info("🔄 Process()...")
|
||||||
|
process.Process()
|
||||||
|
|
||||||
|
if transaction_active:
|
||||||
|
self.cial.CptaApplication.CommitTrans()
|
||||||
|
logger.info("✅ Transaction committée")
|
||||||
|
|
||||||
|
# Récupération numéro
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
numero_facture = None
|
||||||
|
try:
|
||||||
|
doc_result = process.DocumentResult
|
||||||
|
if doc_result:
|
||||||
|
doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3")
|
||||||
|
doc_result.Read()
|
||||||
|
numero_facture = getattr(doc_result, "DO_Piece", "")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not numero_facture:
|
||||||
|
numero_facture = getattr(doc, "DO_Piece", "")
|
||||||
|
|
||||||
|
if not numero_facture:
|
||||||
|
raise RuntimeError("Numéro facture vide après création")
|
||||||
|
|
||||||
|
logger.info(f"📄 Numéro facture: {numero_facture}")
|
||||||
|
|
||||||
|
# Relecture
|
||||||
|
factory_doc = self.cial.FactoryDocumentVente
|
||||||
|
persist_reread = factory_doc.ReadPiece(settings.SAGE_TYPE_FACTURE, numero_facture)
|
||||||
|
|
||||||
|
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))
|
||||||
|
else:
|
||||||
|
total_ht = 0.0
|
||||||
|
total_ttc = 0.0
|
||||||
|
|
||||||
|
logger.info(f"✅✅✅ FACTURE CRÉÉE: {numero_facture} - {total_ttc}€ TTC ✅✅✅")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"numero_facture": numero_facture,
|
||||||
|
"total_ht": total_ht,
|
||||||
|
"total_ttc": total_ttc,
|
||||||
|
"nb_lignes": len(facture_data["lignes"]),
|
||||||
|
"client_code": facture_data["client"]["code"],
|
||||||
|
"date_facture": str(date_obj.date()),
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if transaction_active:
|
||||||
|
try:
|
||||||
|
self.cial.CptaApplication.RollbackTrans()
|
||||||
|
logger.error("❌ Transaction annulée (rollback)")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Erreur création facture: {e}", exc_info=True)
|
||||||
|
raise RuntimeError(f"Échec création facture: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def modifier_facture(self, numero: str, facture_data: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
✏️ Modification d'une facture existante
|
||||||
|
|
||||||
|
⚠️ ATTENTION: Les factures comptabilisées peuvent être verrouillées par Sage
|
||||||
|
|
||||||
|
🔧 STRATÉGIE REMPLACEMENT LIGNES:
|
||||||
|
- Si nouvelles lignes fournies → Supprime TOUTES les anciennes puis ajoute les nouvelles
|
||||||
|
- Utilise .Remove() pour la suppression
|
||||||
|
"""
|
||||||
|
if not self.cial:
|
||||||
|
raise RuntimeError("Connexion Sage non établie")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self._com_context(), self._lock_com:
|
||||||
|
logger.info(f"🔬 === MODIFICATION FACTURE {numero} ===")
|
||||||
|
|
||||||
|
# ÉTAPE 1 : CHARGER LE DOCUMENT
|
||||||
|
logger.info("📂 Chargement document...")
|
||||||
|
|
||||||
|
factory = self.cial.FactoryDocumentVente
|
||||||
|
persist = None
|
||||||
|
|
||||||
|
# Chercher le document
|
||||||
|
for type_test in [60, 5, settings.SAGE_TYPE_FACTURE]:
|
||||||
|
try:
|
||||||
|
persist_test = factory.ReadPiece(type_test, numero)
|
||||||
|
if persist_test:
|
||||||
|
persist = persist_test
|
||||||
|
logger.info(f" ✅ Document trouvé (type={type_test})")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not persist:
|
||||||
|
raise ValueError(f"❌ Facture {numero} INTROUVABLE")
|
||||||
|
|
||||||
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
||||||
|
doc.Read()
|
||||||
|
|
||||||
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
||||||
|
|
||||||
|
logger.info(f" 📊 Statut={statut_actuel}")
|
||||||
|
|
||||||
|
# Vérifier qu'elle n'est pas transformée ou annulée
|
||||||
|
if statut_actuel == 5:
|
||||||
|
raise ValueError(f"La facture {numero} a déjà été transformée")
|
||||||
|
|
||||||
|
if statut_actuel == 6:
|
||||||
|
raise ValueError(f"La facture {numero} est annulée")
|
||||||
|
|
||||||
|
# Vérifier client initial
|
||||||
|
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 initial: {e}")
|
||||||
|
|
||||||
|
if not client_code_initial:
|
||||||
|
raise ValueError("❌ Client introuvable dans le document")
|
||||||
|
|
||||||
|
# Compter les lignes initiales
|
||||||
|
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:
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info(f" 📦 Lignes initiales: {nb_lignes_initial}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" ⚠️ Erreur comptage lignes: {e}")
|
||||||
|
|
||||||
|
# ÉTAPE 2 : DÉTERMINER LES MODIFICATIONS
|
||||||
|
champs_modifies = []
|
||||||
|
|
||||||
|
modif_date = "date_facture" in facture_data
|
||||||
|
modif_statut = "statut" in facture_data
|
||||||
|
modif_ref = "reference" in facture_data
|
||||||
|
modif_lignes = "lignes" in facture_data and facture_data["lignes"] is not None
|
||||||
|
|
||||||
|
logger.info(f"📋 Modifications demandées:")
|
||||||
|
logger.info(f" Date: {modif_date}")
|
||||||
|
logger.info(f" Statut: {modif_statut}")
|
||||||
|
logger.info(f" Référence: {modif_ref}")
|
||||||
|
logger.info(f" Lignes: {modif_lignes}")
|
||||||
|
|
||||||
|
# ÉTAPE 3 : TEST WRITE() BASIQUE
|
||||||
|
logger.info("🧪 Test Write() basique (sans modification)...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc.Write()
|
||||||
|
logger.info(" ✅ Write() basique OK")
|
||||||
|
doc.Read()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" ❌ Write() basique ÉCHOUE: {e}")
|
||||||
|
logger.error(f" ❌ ABANDON: Le document est VERROUILLÉ")
|
||||||
|
raise ValueError(f"Document verrouillé, impossible de modifier: {e}")
|
||||||
|
|
||||||
|
# ÉTAPE 4 : MODIFICATIONS SIMPLES
|
||||||
|
if not modif_lignes and (modif_date or modif_statut or modif_ref):
|
||||||
|
logger.info("🎯 Modifications simples (sans lignes)...")
|
||||||
|
|
||||||
|
if modif_date:
|
||||||
|
import pywintypes
|
||||||
|
date_str = facture_data["date_facture"]
|
||||||
|
|
||||||
|
if isinstance(date_str, str):
|
||||||
|
date_obj = datetime.fromisoformat(date_str)
|
||||||
|
elif isinstance(date_str, date):
|
||||||
|
date_obj = datetime.combine(date_str, datetime.min.time())
|
||||||
|
else:
|
||||||
|
date_obj = date_str
|
||||||
|
|
||||||
|
doc.DO_Date = pywintypes.Time(date_obj)
|
||||||
|
champs_modifies.append("date")
|
||||||
|
logger.info(f" ✅ Date définie: {date_obj.date()}")
|
||||||
|
|
||||||
|
if modif_statut:
|
||||||
|
nouveau_statut = facture_data["statut"]
|
||||||
|
doc.DO_Statut = nouveau_statut
|
||||||
|
champs_modifies.append("statut")
|
||||||
|
logger.info(f" ✅ Statut défini: {nouveau_statut}")
|
||||||
|
|
||||||
|
if modif_ref:
|
||||||
|
try:
|
||||||
|
doc.DO_Ref = facture_data["reference"]
|
||||||
|
champs_modifies.append("reference")
|
||||||
|
logger.info(f" ✅ Référence définie")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" ⚠️ Référence non définie: {e}")
|
||||||
|
|
||||||
|
doc.Write()
|
||||||
|
logger.info(" ✅ Write() réussi")
|
||||||
|
|
||||||
|
# ÉTAPE 5 : REMPLACEMENT COMPLET LIGNES
|
||||||
|
elif modif_lignes:
|
||||||
|
logger.info("🎯 REMPLACEMENT COMPLET DES LIGNES...")
|
||||||
|
|
||||||
|
nouvelles_lignes = facture_data["lignes"]
|
||||||
|
nb_nouvelles = len(nouvelles_lignes)
|
||||||
|
|
||||||
|
logger.info(f" 📊 {nb_lignes_initial} lignes existantes → {nb_nouvelles} nouvelles")
|
||||||
|
|
||||||
|
try:
|
||||||
|
factory_lignes = doc.FactoryDocumentLigne
|
||||||
|
except:
|
||||||
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
||||||
|
|
||||||
|
factory_article = self.cial.FactoryArticle
|
||||||
|
|
||||||
|
# SUPPRESSION TOUTES LES LIGNES
|
||||||
|
if nb_lignes_initial > 0:
|
||||||
|
logger.info(f" 🗑️ Suppression de {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()
|
||||||
|
logger.debug(f" ✅ Ligne {idx} supprimée")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" ⚠️ Erreur suppression ligne {idx}: {e}")
|
||||||
|
|
||||||
|
logger.info(" ✅ Toutes les lignes supprimées")
|
||||||
|
|
||||||
|
# AJOUT NOUVELLES LIGNES
|
||||||
|
logger.info(f" ➕ Ajout de {nb_nouvelles} nouvelles lignes...")
|
||||||
|
|
||||||
|
for idx, ligne_data in enumerate(nouvelles_lignes, 1):
|
||||||
|
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()
|
||||||
|
|
||||||
|
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"])
|
||||||
|
|
||||||
|
try:
|
||||||
|
ligne_obj.SetDefaultArticleReference(ligne_data["article_code"], quantite)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
ligne_obj.SetDefaultArticle(article_obj, quantite)
|
||||||
|
except:
|
||||||
|
ligne_obj.DL_Design = ligne_data.get("designation", "")
|
||||||
|
ligne_obj.DL_Qte = quantite
|
||||||
|
|
||||||
|
if ligne_data.get("prix_unitaire_ht"):
|
||||||
|
ligne_obj.DL_PrixUnitaire = float(ligne_data["prix_unitaire_ht"])
|
||||||
|
|
||||||
|
if ligne_data.get("remise_pourcentage", 0) > 0:
|
||||||
|
try:
|
||||||
|
ligne_obj.DL_Remise01REM_Valeur = float(ligne_data["remise_pourcentage"])
|
||||||
|
ligne_obj.DL_Remise01REM_Type = 0
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ligne_obj.Write()
|
||||||
|
logger.debug(f" ✅ Ligne {idx} ajoutée")
|
||||||
|
|
||||||
|
logger.info(f" ✅ {nb_nouvelles} nouvelles lignes ajoutées")
|
||||||
|
|
||||||
|
doc.Write()
|
||||||
|
champs_modifies.append("lignes")
|
||||||
|
|
||||||
|
# ÉTAPE 6 : RELECTURE ET RETOUR
|
||||||
|
import time
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
doc.Read()
|
||||||
|
|
||||||
|
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
|
||||||
|
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
|
||||||
|
|
||||||
|
logger.info(f"✅✅✅ FACTURE MODIFIÉE: {numero} ✅✅✅")
|
||||||
|
logger.info(f" 💰 Totaux: {total_ht}€ HT / {total_ttc}€ TTC")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"numero": numero,
|
||||||
|
"total_ht": total_ht,
|
||||||
|
"total_ttc": total_ttc,
|
||||||
|
"champs_modifies": champs_modifies,
|
||||||
|
"statut": getattr(doc, "DO_Statut", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue