Sage100-ws/utils/documents/validations.py

623 lines
21 KiB
Python

from typing import Dict
import win32com.client
import logging
logger = logging.getLogger(__name__)
def get_statut_validation(connector, numero_facture: str) -> Dict:
"""Retourne le statut de validation d'une facture (SQL)"""
with connector._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC,
ISNULL(DO_MontantRegle, 0), CT_NumPayeur, DO_Date, DO_Ref
FROM F_DOCENTETE
WHERE DO_Piece = ? AND DO_Type = 6
""",
(numero_facture,),
)
row = cursor.fetchone()
if not row:
raise ValueError(f"Facture {numero_facture} introuvable")
valide = int(row[0]) if row[0] is not None else 0
statut = int(row[1]) if row[1] is not None else 0
total_ht = float(row[2]) if row[2] else 0.0
total_ttc = float(row[3]) if row[3] else 0.0
montant_regle = float(row[4]) if row[4] else 0.0
client_code = row[5].strip() if row[5] else ""
date_facture = row[6]
reference = row[7].strip() if row[7] else ""
solde = max(0.0, total_ttc - montant_regle)
return {
"numero_facture": numero_facture,
"est_validee": valide == 1,
"statut": statut,
"statut_libelle": _get_statut_libelle(statut),
"peut_etre_modifiee": valide == 0 and statut not in (5, 6),
"peut_etre_devalidee": valide == 1
and montant_regle < 0.01
and statut not in (5, 6),
"total_ht": total_ht,
"total_ttc": total_ttc,
"montant_regle": montant_regle,
"solde_restant": solde,
"client_code": client_code,
"date_facture": date_facture.strftime("%Y-%m-%d") if date_facture else None,
"reference": reference,
}
def _get_facture_info_sql(connector, numero_facture: str) -> Dict:
"""Récupère les infos d'une facture via SQL"""
with connector._get_sql_connection() as conn:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
DO_Valide, DO_Statut, DO_TotalHT, DO_TotalTTC,
ISNULL(DO_MontantRegle, 0), CT_NumPayeur
FROM F_DOCENTETE
WHERE DO_Piece = ? AND DO_Type = 6
""",
(numero_facture,),
)
row = cursor.fetchone()
if not row:
return None
return {
"valide": int(row[0]) if row[0] is not None else 0,
"statut": int(row[1]) if row[1] is not None else 0,
"total_ht": float(row[2]) if row[2] else 0.0,
"total_ttc": float(row[3]) if row[3] else 0.0,
"montant_regle": float(row[4]) if row[4] else 0.0,
"client_code": row[5].strip() if row[5] else "",
}
def introspecter_document_complet(connector, numero_facture: str) -> Dict:
if not connector.cial:
raise RuntimeError("Connexion Sage non établie")
result = {
"numero_facture": numero_facture,
"persist": {},
"IBODocumentVente3": {},
"IBODocument3": {},
"IPMDocument": {},
}
try:
with connector._com_context(), connector._lock_com:
factory = connector.cial.FactoryDocumentVente
if not factory.ExistPiece(60, numero_facture):
raise ValueError(f"Facture {numero_facture} introuvable")
persist = factory.ReadPiece(60, numero_facture)
# 1. Attributs du persist brut
persist_attrs = [a for a in dir(persist) if not a.startswith("_")]
result["persist"]["all_attrs"] = persist_attrs
result["persist"]["methods"] = []
result["persist"]["properties"] = []
for attr in persist_attrs:
try:
val = getattr(persist, attr, None)
if callable(val):
result["persist"]["methods"].append(attr)
else:
result["persist"]["properties"].append(
{
"name": attr,
"value": str(val)[:100] if val is not None else None,
}
)
except Exception as e:
result["persist"]["properties"].append(
{"name": attr, "error": str(e)[:50]}
)
# Chercher spécifiquement les attributs liés à validation/valide
result["persist"]["validation_related"] = [
a
for a in persist_attrs
if any(
x in a.lower()
for x in ["valid", "lock", "confirm", "statut", "etat"]
)
]
# 2. IBODocumentVente3
try:
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
doc_attrs = [a for a in dir(doc) if not a.startswith("_")]
result["IBODocumentVente3"]["all_attrs"] = doc_attrs
result["IBODocumentVente3"]["methods"] = []
result["IBODocumentVente3"]["properties_with_values"] = []
# Lister les méthodes
for attr in doc_attrs:
try:
val = getattr(doc, attr, None)
if callable(val):
result["IBODocumentVente3"]["methods"].append(attr)
except Exception:
pass
# Chercher DO_* properties
result["IBODocumentVente3"]["DO_properties"] = []
for attr in doc_attrs:
if attr.startswith("DO_"):
try:
val = getattr(doc, attr, "ERROR")
result["IBODocumentVente3"]["DO_properties"].append(
{"name": attr, "value": str(val)[:50]}
)
except Exception as e:
result["IBODocumentVente3"]["DO_properties"].append(
{"name": attr, "error": str(e)[:50]}
)
# Chercher les attributs liés à validation
result["IBODocumentVente3"]["validation_related"] = [
a
for a in doc_attrs
if any(
x in a.lower()
for x in ["valid", "lock", "confirm", "statut", "etat"]
)
]
except Exception as e:
result["IBODocumentVente3"]["error"] = str(e)
# 3. IBODocument3
try:
doc3 = win32com.client.CastTo(persist, "IBODocument3")
doc3.Read()
doc3_attrs = [a for a in dir(doc3) if not a.startswith("_")]
result["IBODocument3"]["all_attrs"] = doc3_attrs
result["IBODocument3"]["validation_related"] = [
a
for a in doc3_attrs
if any(
x in a.lower()
for x in ["valid", "lock", "confirm", "statut", "etat"]
)
]
except Exception as e:
result["IBODocument3"]["error"] = str(e)
# 4. IPMDocument
try:
pmdoc = win32com.client.CastTo(persist, "IPMDocument")
pmdoc_attrs = [a for a in dir(pmdoc) if not a.startswith("_")]
result["IPMDocument"]["all_attrs"] = pmdoc_attrs
result["IPMDocument"]["methods"] = [
a for a in pmdoc_attrs if callable(getattr(pmdoc, a, None))
]
except Exception as e:
result["IPMDocument"]["error"] = str(e)
# 5. Chercher FactoryDocument* sur le document
result["factories_on_doc"] = []
for attr in persist_attrs:
if "Factory" in attr:
result["factories_on_doc"].append(attr)
except Exception as e:
result["global_error"] = str(e)
return result
def introspecter_validation(connector, numero_facture: str = None) -> Dict:
if not connector.cial:
raise RuntimeError("Connexion Sage non établie")
result = {}
try:
with connector._com_context(), connector._lock_com:
# Tous les CreateProcess
cial_attrs = [a for a in dir(connector.cial) if not a.startswith("_")]
result["all_createprocess"] = [
a for a in cial_attrs if "CreateProcess" in a
]
# Explorer chaque process
for process_name in result["all_createprocess"]:
try:
process = getattr(connector.cial, process_name)()
process_attrs = [a for a in dir(process) if not a.startswith("_")]
result[process_name] = {
"attrs": process_attrs,
"has_valider": "Valider" in process_attrs
or "Valid" in str(process_attrs),
"has_document": "Document" in process_attrs,
}
except Exception as e:
result[process_name] = {"error": str(e)}
# Introspection document si fourni
if numero_facture:
result["document"] = introspecter_document_complet(
connector, numero_facture
)
except Exception as e:
result["global_error"] = str(e)
return result
def valider_facture(connector, numero_facture: str) -> Dict:
logger.info(f" Validation facture {numero_facture} (SQL direct)")
# Vérifications préalables
with connector._get_sql_connection() as conn:
cursor = conn.cursor()
# Vérifier que la facture existe et peut être validée
cursor.execute(
"""
SELECT DO_Valide, DO_Statut, DO_TotalTTC, DO_MontantRegle
FROM F_DOCENTETE
WHERE DO_Piece = ? AND DO_Type = 6
""",
(numero_facture,),
)
row = cursor.fetchone()
if not row:
raise ValueError(f"Facture {numero_facture} introuvable")
valide_avant, statut, total_ttc, montant_regle = row
if valide_avant == 1:
return {"numero_facture": numero_facture, "deja_valide": True}
if statut == 6: # Annulé
raise ValueError("Facture annulée, validation impossible")
# Valider via SQL
cursor.execute(
"""
UPDATE F_DOCENTETE
SET DO_Valide = 1, DO_Imprim = 0
WHERE DO_Piece = ? AND DO_Type = 6
""",
(numero_facture,),
)
conn.commit()
# Vérifier
cursor.execute(
"""
SELECT DO_Valide, DO_Imprim
FROM F_DOCENTETE
WHERE DO_Piece = ? AND DO_Type = 6
""",
(numero_facture,),
)
valide_apres, imprim_apres = cursor.fetchone()
logger.info(f" SQL: DO_Valide={valide_apres}, DO_Imprim={imprim_apres}")
return {
"numero_facture": numero_facture,
"methode": "SQL_DIRECT",
"DO_Valide": valide_apres == 1,
"DO_Imprim": imprim_apres == 1,
"success": valide_apres == 1,
"warning": "Validation SQL directe - règles métier Sage contournées",
}
def devalider_facture(connector, numero_facture: str) -> Dict:
if not connector.cial:
raise RuntimeError("Connexion Sage non établie")
logger.info(f" Dévalidation facture {numero_facture}")
info = _get_facture_info_sql(connector, numero_facture)
if not info:
raise ValueError(f"Facture {numero_facture} introuvable")
if info["statut"] == 6:
raise ValueError(f"Facture {numero_facture} annulée")
if info["statut"] == 5:
raise ValueError(
f"Facture {numero_facture} transformée, dévalidation impossible"
)
if info["montant_regle"] > 0.01:
raise ValueError(
f"Facture {numero_facture} partiellement réglée ({info['montant_regle']:.2f}€), "
"dévalidation impossible"
)
if info["valide"] == 0:
logger.info(f"Facture {numero_facture} déjà non validée")
return _build_response_sql(
connector, numero_facture, deja_valide=True, action="devalidation"
)
try:
with connector._com_context(), connector._lock_com:
success = _valider_document_com(connector, numero_facture, valider=False)
if not success:
raise RuntimeError("La dévalidation COM a échoué")
logger.info(f" Facture {numero_facture} dévalidée")
except ValueError:
raise
except Exception as e:
logger.error(f"❌ Erreur COM dévalidation {numero_facture}: {e}", exc_info=True)
raise RuntimeError(f"Échec dévalidation: {str(e)}")
info_apres = _get_facture_info_sql(connector, numero_facture)
if info_apres and info_apres["valide"] != 0:
raise RuntimeError(
"Échec dévalidation: DO_Valide non modifié après l'opération COM"
)
return _build_response_sql(
connector, numero_facture, deja_valide=False, action="devalidation"
)
def _valider_document_com(connector, numero_facture: str, valider: bool = True) -> bool:
erreurs = []
action = "validation" if valider else "dévalidation"
valeur_cible = 1 if valider else 0
factory = connector.cial.FactoryDocumentVente
if not factory.ExistPiece(60, numero_facture):
raise ValueError(f"Facture {numero_facture} introuvable")
persist = factory.ReadPiece(60, numero_facture)
if not persist:
raise ValueError(f"Impossible de lire la facture {numero_facture}")
# APPROCHE 1: Accès direct à DO_Valide sur IBODocumentVente3
try:
logger.info(
" APPROCHE 1: Modification directe DO_Valide sur IBODocumentVente3..."
)
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
# Vérifier la valeur actuelle
valeur_avant = getattr(doc, "DO_Valide", None)
logger.info(f" DO_Valide avant: {valeur_avant}")
# Tenter la modification
doc.DO_Valide = valeur_cible
doc.Write()
# Relire pour vérifier
doc.Read()
valeur_apres = getattr(doc, "DO_Valide", None)
logger.info(f" DO_Valide après: {valeur_apres}")
if valeur_apres == valeur_cible:
logger.info(" DO_Valide modifié avec succès!")
return True
else:
erreurs.append(
f"DO_Valide non modifié (avant={valeur_avant}, après={valeur_apres})"
)
except Exception as e:
erreurs.append(f"IBODocumentVente3.DO_Valide: {e}")
logger.warning(f" Erreur: {e}")
# APPROCHE 2: Via IBODocument3 (interface parent)
try:
logger.info(" APPROCHE 2: Via IBODocument3...")
doc3 = win32com.client.CastTo(persist, "IBODocument3")
doc3.Read()
if hasattr(doc3, "DO_Valide"):
doc3.DO_Valide = valeur_cible
doc3.Write()
logger.info(f" IBODocument3.DO_Valide = {valeur_cible} OK")
return True
else:
erreurs.append("IBODocument3 n'a pas DO_Valide")
except Exception as e:
erreurs.append(f"IBODocument3: {e}")
# APPROCHE 3: Chercher un CreateProcess de validation
try:
logger.info(" APPROCHE 3: Recherche CreateProcess de validation...")
cial_attrs = [a for a in dir(connector.cial) if "CreateProcess" in a]
validation_processes = [
a
for a in cial_attrs
if any(x in a.lower() for x in ["valid", "confirm", "lock"])
]
logger.info(f" CreateProcess trouvés: {cial_attrs}")
logger.info(f" Liés à validation: {validation_processes}")
for proc_name in validation_processes:
try:
process = getattr(connector.cial, proc_name)()
# Lister les attributs du process
proc_attrs = [a for a in dir(process) if not a.startswith("_")]
logger.info(f" {proc_name} attrs: {proc_attrs}")
if hasattr(process, "Document"):
process.Document = persist
if hasattr(process, "Valider"):
process.Valider = valider
if hasattr(process, "Process"):
process.Process()
logger.info(f" {proc_name}.Process() exécuté!")
return True
except Exception as e:
erreurs.append(f"{proc_name}: {e}")
except Exception as e:
erreurs.append(f"CreateProcess: {e}")
# APPROCHE 4: WriteDefault avec paramètres
try:
logger.info(" APPROCHE 4: WriteDefault...")
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
doc.Read()
doc.DO_Valide = valeur_cible
if hasattr(doc, "WriteDefault"):
doc.WriteDefault()
logger.info(" WriteDefault() exécuté")
return True
except Exception as e:
erreurs.append(f"WriteDefault: {e}")
logger.error(f" Toutes les approches de {action} ont échoué: {erreurs}")
raise RuntimeError(f"Échec {action}: {'; '.join(erreurs[:5])}")
def explorer_toutes_interfaces_validation(connector, numero_facture: str) -> Dict:
"""Explorer TOUTES les interfaces possibles pour trouver un setter DO_Valide"""
result = {"numero_facture": numero_facture, "interfaces": {}}
with connector._com_context(), connector._lock_com:
factory = connector.cial.FactoryDocumentVente
persist = factory.ReadPiece(60, numero_facture)
# Liste des interfaces à tester
interfaces = [
"IBODocumentVente3",
"IBODocument3",
"IBIPersistObject",
"IBIDocument",
"IPMDocument",
"IDispatch",
]
for iface_name in interfaces:
try:
obj = win32com.client.CastTo(persist, iface_name)
if hasattr(obj, "Read"):
obj.Read()
oleobj = obj._oleobj_
type_info = oleobj.GetTypeInfo()
type_attr = type_info.GetTypeAttr()
props = {}
for i in range(type_attr.cFuncs):
func_desc = type_info.GetFuncDesc(i)
names = type_info.GetNames(func_desc.memid)
if names and names[0] in (
"DO_Valide",
"DO_Imprim",
"Valider",
"Validate",
"Lock",
):
props[names[0]] = {
"memid": func_desc.memid,
"invkind": func_desc.invkind,
# invkind: 1=METHOD, 2=GET, 4=PUT, 8=PUTREF
"has_setter": (func_desc.invkind & 4) == 4,
}
result["interfaces"][iface_name] = {
"success": True,
"properties": props,
}
except Exception as e:
result["interfaces"][iface_name] = {"error": str(e)[:100]}
# Explorer aussi FactoryDocumentVente pour des méthodes de validation
try:
factory_attrs = [a for a in dir(factory) if not a.startswith("_")]
result["factory_methods"] = [
a
for a in factory_attrs
if any(x in a.lower() for x in ["valid", "lock", "confirm", "imprim"])
]
except Exception:
pass
return result
def _build_response_sql(
connector, numero_facture: str, deja_valide: bool, action: str
) -> Dict:
"""Construit la réponse via SQL"""
info = _get_facture_info_sql(connector, numero_facture)
if action == "validation":
message = (
"Facture déjà validée" if deja_valide else "Facture validée avec succès"
)
else:
message = (
"Facture déjà non validée"
if deja_valide
else "Facture dévalidée avec succès"
)
return {
"numero_facture": numero_facture,
"est_validee": info["valide"] == 1 if info else False,
"statut": info["statut"] if info else 0,
"statut_libelle": _get_statut_libelle(info["statut"]) if info else "Inconnu",
"total_ht": info["total_ht"] if info else 0.0,
"total_ttc": info["total_ttc"] if info else 0.0,
"client_code": info["client_code"] if info else "",
"message": message,
"action_effectuee": not deja_valide,
}
def _get_statut_libelle(statut: int) -> str:
"""Retourne le libellé d'un statut de document"""
statuts = {
0: "Brouillon",
1: "Confirmé",
2: "En cours",
3: "Imprimé",
4: "Suspendu",
5: "Transformé",
6: "Annulé",
}
return statuts.get(statut, f"Statut {statut}")
__all__ = [
"valider_facture",
"devalider_facture",
"get_statut_validation",
"introspecter_validation",
"introspecter_document_complet",
]