Compare commits
127 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dd863accf | |||
| 31dec46226 | |||
| 9ffad8287d | |||
| fd60bd3bc7 | |||
| 437ac743c8 | |||
| 06a5fc8df4 | |||
| 5ad54a2ff0 | |||
| f5c5a87d0d | |||
| 7dc5d03c4c | |||
| 5c80a5e912 | |||
| 6b4b603aee | |||
| 7d58a607f5 | |||
| 68c70de7d9 | |||
| b66525c00e | |||
| bd48bf5aac | |||
| e281751c5e | |||
| ff8c35fcfa | |||
| b6416487c0 | |||
| e47e14f1b4 | |||
| 2b15e7b3e2 | |||
| 3fd3b7c45a | |||
| 317a7312cc | |||
| 4a642fa654 | |||
| fd0385d417 | |||
| 97a2bc01f0 | |||
| 69114ba0c3 | |||
| 57103d406d | |||
| f80ad1adee | |||
| 96021205a4 | |||
| 6a346876aa | |||
| ea344b2669 | |||
| 3fa98a168c | |||
| 4ae0178090 | |||
| cc1f3aa8b1 | |||
| a0f9eeedec | |||
| c32a9171a5 | |||
| 06c9fbb929 | |||
| 5e4231e115 | |||
| 0788f46bde | |||
| 3bb1aee4b4 | |||
| 935e43487f | |||
| 2ea7fa8371 | |||
| d6d01fee9f | |||
| 2f3b0ade5e | |||
| 2819578ca2 | |||
| 6bb1253a1a | |||
| 09e3589132 | |||
| 800d828f75 | |||
| 7f64a2a548 | |||
| 3e617d070a | |||
| 08686a7b2f | |||
| d9506337ff | |||
| 659dac81c9 | |||
| 679ae6a0e4 | |||
| e8ea774c41 | |||
| 5591bff7b3 | |||
| c9497adad2 | |||
| fde2b4615e | |||
| 557f43bd18 | |||
| 57c05082c0 | |||
| 7b451223ef | |||
| 0f9b3dfa0d | |||
| 5257be0680 | |||
| f0c84c9cb6 | |||
| c840cd5035 | |||
| 5ccf470167 | |||
| 42048d11ee | |||
| 6a4f7aaf2c | |||
| eba4011dd4 | |||
|
|
a729b812eb | ||
|
|
b799efef75 | ||
|
|
99eda1c127 | ||
|
|
b619915ac1 | ||
|
|
bf986bef5a | ||
|
|
51f49298c2 | ||
|
|
ec5a0f0089 | ||
|
|
5a5b6307b9 | ||
|
|
5c374892d0 | ||
|
|
fc7216fe2f | ||
|
|
cc2b86d533 | ||
|
|
c4ddf03ae5 | ||
|
|
9d569a8e03 | ||
|
|
de1771749d | ||
|
|
876e050bff | ||
|
|
c66280b305 | ||
|
|
3aadc67abf | ||
|
|
7ca64e2ea6 | ||
|
|
be7bc287c0 | ||
|
|
3013f6589c | ||
|
|
6aebf00653 | ||
|
|
015943bdfa | ||
|
|
9d4f620bdc | ||
|
|
53517136f2 | ||
|
|
c48a3f033a | ||
|
|
49fdc8425b | ||
|
|
914ea61243 | ||
|
|
e8558e207b | ||
|
|
2e96cec20d | ||
|
|
cc56821c70 | ||
|
|
ae5fa9e0be | ||
|
|
53ecccd712 | ||
|
|
cec8389302 | ||
|
|
de0053b98b | ||
|
|
e6c2ab6670 | ||
|
|
a4dd2c40ba | ||
|
|
b06720eace | ||
|
|
5abeaebf56 | ||
|
|
8b676f7195 | ||
|
|
0763a56b06 | ||
|
|
6733f506eb | ||
|
|
6c1de3583c | ||
|
|
9b17149b07 | ||
|
|
9d0c26b5d8 | ||
|
|
92c79f1362 | ||
|
|
3505ecfd2b | ||
|
|
c522aa5a64 | ||
|
|
8ce32fe8df | ||
|
|
02b6780d3f | ||
|
|
6e8aa332ce | ||
|
|
d9fe626cbd | ||
|
|
62077b5862 | ||
|
|
c4d2185c22 | ||
|
|
96c9c5e7df | ||
|
|
efa4edcae0 | ||
|
|
4da650e361 | ||
|
|
a1f7026fd9 | ||
|
|
7ee3751ee2 |
51 changed files with 18736 additions and 1204 deletions
|
|
@ -1,14 +1,9 @@
|
||||||
# ============================================================================
|
|
||||||
# SAGE 100 CLOUD - CONNEXION BOI/COM
|
|
||||||
# ============================================================================
|
|
||||||
CHEMIN_BASE=<CHEMIN_VERS_LE_FICHIER_GCM>
|
CHEMIN_BASE=<CHEMIN_VERS_LE_FICHIER_GCM>
|
||||||
UTILISATEUR=<UTILISATEUR_SAGE100>
|
UTILISATEUR=<UTILISATEUR_SAGE100>
|
||||||
MOT_DE_PASSE=<MOT_DE_PASSE_SAGE100>
|
MOT_DE_PASSE=<MOT_DE_PASSE_SAGE100>
|
||||||
|
|
||||||
SAGE_GATEWAY_TOKEN=<TOKEN_SAGE_GATEWAY>
|
SAGE_GATEWAY_TOKEN=<TOKEN_SAGE_GATEWAY>
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# API - CONFIGURATION SERVEUR
|
|
||||||
# ============================================================================
|
|
||||||
API_HOST=0.0.0.0
|
API_HOST=0.0.0.0
|
||||||
API_PORT=8000
|
API_PORT=8000
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,7 +1,3 @@
|
||||||
# ================================
|
|
||||||
# Python / FastAPI
|
|
||||||
# ================================
|
|
||||||
|
|
||||||
# Environnements virtuels
|
# Environnements virtuels
|
||||||
venv/
|
venv/
|
||||||
.env
|
.env
|
||||||
|
|
@ -36,3 +32,6 @@ htmlcov/
|
||||||
*~
|
*~
|
||||||
.build/
|
.build/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
|
||||||
|
*clean*.py
|
||||||
36
config.py
36
config.py
|
|
@ -1,42 +1,50 @@
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=".env",
|
env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore"
|
||||||
env_file_encoding="utf-8",
|
|
||||||
case_sensitive=False,
|
|
||||||
extra="ignore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# === SAGE 100c (Windows uniquement) ===
|
SAGE_TYPE_DEVIS: int = 0
|
||||||
|
SAGE_TYPE_BON_COMMANDE: int = 10
|
||||||
|
SAGE_TYPE_PREPARATION: int = 20
|
||||||
|
SAGE_TYPE_BON_LIVRAISON: int = 30
|
||||||
|
SAGE_TYPE_BON_RETOUR: int = 40
|
||||||
|
SAGE_TYPE_BON_AVOIR: int = 50
|
||||||
|
SAGE_TYPE_FACTURE: int = 60
|
||||||
|
|
||||||
chemin_base: str
|
chemin_base: str
|
||||||
utilisateur: str = "Administrateur"
|
utilisateur: str = "Administrateur"
|
||||||
mot_de_passe: str
|
mot_de_passe: str
|
||||||
|
|
||||||
|
sql_server_name: str
|
||||||
|
sql_server_database: str
|
||||||
|
|
||||||
# === Sécurité Gateway ===
|
sage_gateway_token: str
|
||||||
sage_gateway_token: str # Token partagé avec le VPS Linux
|
|
||||||
|
|
||||||
# === SMTP (optionnel sur Windows) ===
|
|
||||||
smtp_host: Optional[str] = None
|
smtp_host: Optional[str] = None
|
||||||
smtp_port: int = 587
|
smtp_port: int = 587
|
||||||
smtp_user: Optional[str] = None
|
smtp_user: Optional[str] = None
|
||||||
smtp_password: Optional[str] = None
|
smtp_password: Optional[str] = None
|
||||||
smtp_from: Optional[str] = None
|
smtp_from: Optional[str] = None
|
||||||
|
|
||||||
# === API Windows ===
|
|
||||||
api_host: str = "0.0.0.0"
|
api_host: str = "0.0.0.0"
|
||||||
api_port: int = 8000
|
api_port: int = 8000
|
||||||
|
|
||||||
# === CORS ===
|
|
||||||
cors_origins: List[str] = ["*"]
|
cors_origins: List[str] = ["*"]
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
def validate_settings():
|
def validate_settings():
|
||||||
"""Validation au démarrage"""
|
"""Validation au démarrage"""
|
||||||
if not settings.chemin_base or not settings.mot_de_passe:
|
if not settings.chemin_base or not settings.mot_de_passe:
|
||||||
raise ValueError("❌ CHEMIN_BASE et MOT_DE_PASSE requis dans .env")
|
raise ValueError(" CHEMIN_BASE ou MOT_DE_PASSE requis dans .env")
|
||||||
|
if not settings.sql_server_database or not settings.sql_server_name:
|
||||||
|
raise ValueError(" Infos de la base de données requises dans .env")
|
||||||
if not settings.sage_gateway_token:
|
if not settings.sage_gateway_token:
|
||||||
raise ValueError("❌ SAGE_GATEWAY_TOKEN requis (doit être identique sur Linux)")
|
raise ValueError(" SAGE_GATEWAY_TOKEN requis (doit être identique sur Linux)")
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,6 @@ pydantic
|
||||||
pydantic-settings
|
pydantic-settings
|
||||||
python-multipart
|
python-multipart
|
||||||
python-dotenv
|
python-dotenv
|
||||||
pywin32
|
pywin32
|
||||||
|
pyodbc
|
||||||
|
reportlab
|
||||||
8892
sage_connector.py
8892
sage_connector.py
File diff suppressed because it is too large
Load diff
94
schemas/__init__.py
Normal file
94
schemas/__init__.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
from schemas.tiers.tiers import TiersList, TypeTiers
|
||||||
|
from schemas.tiers.contact import (
|
||||||
|
ContactCreate,
|
||||||
|
ContactDelete,
|
||||||
|
ContactGet,
|
||||||
|
ContactList,
|
||||||
|
ContactUpdate,
|
||||||
|
)
|
||||||
|
from schemas.tiers.clients import ClientCreate, ClientUpdate
|
||||||
|
|
||||||
|
from schemas.others.general_schema import (
|
||||||
|
FiltreRequest,
|
||||||
|
ChampLibre,
|
||||||
|
CodeRequest,
|
||||||
|
StatutRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
from schemas.documents.documents import (
|
||||||
|
TransformationRequest,
|
||||||
|
TypeDocument,
|
||||||
|
DocumentGet,
|
||||||
|
PDFGeneration,
|
||||||
|
)
|
||||||
|
|
||||||
|
from schemas.documents.devis import DevisRequest, DevisUpdate
|
||||||
|
|
||||||
|
from schemas.tiers.fournisseurs import FournisseurCreate, FournisseurUpdate
|
||||||
|
|
||||||
|
from schemas.documents.avoirs import AvoirCreate, AvoirUpdate
|
||||||
|
|
||||||
|
from schemas.documents.commandes import CommandeCreate, CommandeUpdate
|
||||||
|
|
||||||
|
from schemas.documents.factures import FactureCreate, FactureUpdate
|
||||||
|
|
||||||
|
from schemas.documents.livraisons import LivraisonCreate, LivraisonUpdate
|
||||||
|
|
||||||
|
from schemas.articles.articles import (
|
||||||
|
ArticleCreate,
|
||||||
|
ArticleUpdate,
|
||||||
|
MouvementStockLigneRequest,
|
||||||
|
EntreeStock,
|
||||||
|
SortieStock,
|
||||||
|
)
|
||||||
|
|
||||||
|
from schemas.articles.famille_d_articles import FamilleCreate
|
||||||
|
|
||||||
|
from schemas.tiers.commercial import (
|
||||||
|
CollaborateurCreateRequest,
|
||||||
|
CollaborateurListRequest,
|
||||||
|
CollaborateurNumeroRequest,
|
||||||
|
CollaborateurUpdateRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"TiersList",
|
||||||
|
"ContactCreate",
|
||||||
|
"ContactDelete",
|
||||||
|
"ContactGet",
|
||||||
|
"ContactList",
|
||||||
|
"ContactUpdate",
|
||||||
|
"ClientCreate",
|
||||||
|
"ClientUpdate",
|
||||||
|
"FiltreRequest",
|
||||||
|
"ChampLibre",
|
||||||
|
"CodeRequest",
|
||||||
|
"TransformationRequest",
|
||||||
|
"TypeDocument",
|
||||||
|
"DevisRequest",
|
||||||
|
"DocumentGet",
|
||||||
|
"StatutRequest",
|
||||||
|
"TypeTiers",
|
||||||
|
"DevisUpdate",
|
||||||
|
"FournisseurCreate",
|
||||||
|
"FournisseurUpdate",
|
||||||
|
"AvoirCreate",
|
||||||
|
"AvoirUpdate",
|
||||||
|
"CommandeCreate",
|
||||||
|
"CommandeUpdate",
|
||||||
|
"FactureCreate",
|
||||||
|
"FactureUpdate",
|
||||||
|
"LivraisonCreate",
|
||||||
|
"LivraisonUpdate",
|
||||||
|
"ArticleCreate",
|
||||||
|
"ArticleUpdate",
|
||||||
|
"MouvementStockLigneRequest",
|
||||||
|
"EntreeStock",
|
||||||
|
"SortieStock",
|
||||||
|
"FamilleCreate",
|
||||||
|
"PDFGeneration",
|
||||||
|
"CollaborateurCreateRequest",
|
||||||
|
"CollaborateurListRequest",
|
||||||
|
"CollaborateurNumeroRequest",
|
||||||
|
"CollaborateurUpdateRequest",
|
||||||
|
]
|
||||||
149
schemas/articles/articles.py
Normal file
149
schemas/articles/articles.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
from pydantic import BaseModel, Field, validator
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleCreate(BaseModel):
|
||||||
|
reference: str = Field(..., description="Référence article (max 18 car)")
|
||||||
|
designation: str = Field(..., description="Désignation (max 69 car)")
|
||||||
|
|
||||||
|
famille: Optional[str] = Field(None, description="Code famille")
|
||||||
|
|
||||||
|
prix_vente: Optional[float] = Field(None, ge=0, description="Prix vente HT")
|
||||||
|
prix_achat: Optional[float] = Field(None, ge=0, description="Prix achat HT")
|
||||||
|
coef: Optional[float] = Field(None, ge=0, description="Coefficient")
|
||||||
|
|
||||||
|
stock_reel: Optional[float] = Field(None, ge=0, description="Stock initial")
|
||||||
|
stock_mini: Optional[float] = Field(None, ge=0, description="Stock minimum")
|
||||||
|
stock_maxi: Optional[float] = Field(None, ge=0, description="Stock maximum")
|
||||||
|
|
||||||
|
code_ean: Optional[str] = Field(None, description="Code-barres EAN")
|
||||||
|
unite_vente: Optional[str] = Field("UN", description="Unité de vente")
|
||||||
|
tva_code: Optional[str] = Field(None, description="Code TVA")
|
||||||
|
code_fiscal: Optional[str] = Field(None, description="Code fiscal")
|
||||||
|
|
||||||
|
description: Optional[str] = Field(None, description="Description/Commentaire")
|
||||||
|
|
||||||
|
pays: Optional[str] = Field(None, description="Pays d'origine")
|
||||||
|
garantie: Optional[int] = Field(None, ge=0, description="Garantie en mois")
|
||||||
|
delai: Optional[int] = Field(None, ge=0, description="Délai livraison jours")
|
||||||
|
|
||||||
|
poids_net: Optional[float] = Field(None, ge=0, description="Poids net kg")
|
||||||
|
poids_brut: Optional[float] = Field(None, ge=0, description="Poids brut kg")
|
||||||
|
|
||||||
|
stat_01: Optional[str] = Field(None, description="Statistique 1")
|
||||||
|
stat_02: Optional[str] = Field(None, description="Statistique 2")
|
||||||
|
stat_03: Optional[str] = Field(None, description="Statistique 3")
|
||||||
|
stat_04: Optional[str] = Field(None, description="Statistique 4")
|
||||||
|
stat_05: Optional[str] = Field(None, description="Statistique 5")
|
||||||
|
|
||||||
|
soumis_escompte: Optional[bool] = Field(None, description="Soumis à escompte")
|
||||||
|
publie: Optional[bool] = Field(None, description="Publié web/catalogue")
|
||||||
|
en_sommeil: Optional[bool] = Field(None, description="Article en sommeil")
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleUpdate(BaseModel):
|
||||||
|
reference: str = Field(..., description="Référence de l'article à modifier")
|
||||||
|
article_data: Dict = Field(..., description="Données à modifier")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"reference": "ART001",
|
||||||
|
"article_data": {
|
||||||
|
"designation": "Nouvelle désignation",
|
||||||
|
"prix_vente": 150.0,
|
||||||
|
"famille": "FAM01",
|
||||||
|
"stock_reel": 100.0,
|
||||||
|
"stock_mini": 10.0,
|
||||||
|
"code_fiscal": "V19",
|
||||||
|
"garantie": 24,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MouvementStockLigneRequest(BaseModel):
|
||||||
|
article_ref: str = Field(..., description="Référence de l'article")
|
||||||
|
quantite: float = Field(..., gt=0, description="Quantité (>0)")
|
||||||
|
depot_code: Optional[str] = Field(None, description="Code du dépôt (ex: '01')")
|
||||||
|
prix_unitaire: Optional[float] = Field(
|
||||||
|
None, ge=0, description="Prix unitaire (optionnel)"
|
||||||
|
)
|
||||||
|
commentaire: Optional[str] = Field(None, description="Commentaire ligne")
|
||||||
|
numero_lot: Optional[str] = Field(
|
||||||
|
None, description="Numéro de lot (pour FIFO/LIFO)"
|
||||||
|
)
|
||||||
|
stock_mini: Optional[float] = Field(
|
||||||
|
None,
|
||||||
|
ge=0,
|
||||||
|
description="""Stock minimum à définir pour cet article.
|
||||||
|
Si fourni, met à jour AS_QteMini dans F_ARTSTOCK.
|
||||||
|
Laisser None pour ne pas modifier.""",
|
||||||
|
)
|
||||||
|
stock_maxi: Optional[float] = Field(
|
||||||
|
None,
|
||||||
|
ge=0,
|
||||||
|
description="""Stock maximum à définir pour cet article.
|
||||||
|
Doit être > stock_mini si les deux sont fournis.""",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"article_ref": "ARTS-001",
|
||||||
|
"quantite": 50.0,
|
||||||
|
"depot_code": "01",
|
||||||
|
"prix_unitaire": 100.0,
|
||||||
|
"commentaire": "Réapprovisionnement",
|
||||||
|
"numero_lot": "LOT20241217",
|
||||||
|
"stock_mini": 10.0,
|
||||||
|
"stock_maxi": 200.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@validator("stock_maxi")
|
||||||
|
def validate_stock_maxi(cls, v, values):
|
||||||
|
"""Valide que stock_maxi > stock_mini si les deux sont fournis"""
|
||||||
|
if (
|
||||||
|
v is not None
|
||||||
|
and "stock_mini" in values
|
||||||
|
and values["stock_mini"] is not None
|
||||||
|
):
|
||||||
|
if v <= values["stock_mini"]:
|
||||||
|
raise ValueError(
|
||||||
|
"stock_maxi doit être strictement supérieur à stock_mini"
|
||||||
|
)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class EntreeStock(BaseModel):
|
||||||
|
"""Création d'un bon d'entrée en stock"""
|
||||||
|
|
||||||
|
date_entree: Optional[date] = Field(
|
||||||
|
None, description="Date du mouvement (aujourd'hui par défaut)"
|
||||||
|
)
|
||||||
|
reference: Optional[str] = Field(None, description="Référence externe")
|
||||||
|
depot_code: Optional[str] = Field(
|
||||||
|
None, description="Dépôt principal (si applicable)"
|
||||||
|
)
|
||||||
|
lignes: List[MouvementStockLigneRequest] = Field(
|
||||||
|
..., min_items=1, description="Lignes du mouvement"
|
||||||
|
)
|
||||||
|
commentaire: Optional[str] = Field(None, description="Commentaire général")
|
||||||
|
|
||||||
|
|
||||||
|
class SortieStock(BaseModel):
|
||||||
|
"""Création d'un bon de sortie de stock"""
|
||||||
|
|
||||||
|
date_sortie: Optional[date] = Field(
|
||||||
|
None, description="Date du mouvement (aujourd'hui par défaut)"
|
||||||
|
)
|
||||||
|
reference: Optional[str] = Field(None, description="Référence externe")
|
||||||
|
depot_code: Optional[str] = Field(
|
||||||
|
None, description="Dépôt principal (si applicable)"
|
||||||
|
)
|
||||||
|
lignes: List[MouvementStockLigneRequest] = Field(
|
||||||
|
..., min_items=1, description="Lignes du mouvement"
|
||||||
|
)
|
||||||
|
commentaire: Optional[str] = Field(None, description="Commentaire général")
|
||||||
16
schemas/articles/famille_d_articles.py
Normal file
16
schemas/articles/famille_d_articles.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class FamilleCreate(BaseModel):
|
||||||
|
"""Modèle pour créer une famille d'articles"""
|
||||||
|
|
||||||
|
code: str = Field(..., description="Code famille (max 18 car)", max_length=18)
|
||||||
|
intitule: str = Field(..., description="Intitulé (max 69 car)", max_length=69)
|
||||||
|
type: int = Field(0, description="0=Détail, 1=Total")
|
||||||
|
compte_achat: Optional[str] = Field(
|
||||||
|
None, description="Compte général d'achat (ex: 607000)"
|
||||||
|
)
|
||||||
|
compte_vente: Optional[str] = Field(
|
||||||
|
None, description="Compte général de vente (ex: 707000)"
|
||||||
|
)
|
||||||
20
schemas/documents/avoirs.py
Normal file
20
schemas/documents/avoirs.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class AvoirCreate(BaseModel):
|
||||||
|
"""Création d'un avoir côté gateway"""
|
||||||
|
|
||||||
|
client_id: str
|
||||||
|
date_avoir: Optional[datetime] = None
|
||||||
|
date_livraison: Optional[datetime] = None
|
||||||
|
lignes: List[Dict]
|
||||||
|
reference: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AvoirUpdate(BaseModel):
|
||||||
|
"""Modèle pour modification avoir côté gateway"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
avoir_data: Dict
|
||||||
20
schemas/documents/commandes.py
Normal file
20
schemas/documents/commandes.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class CommandeCreate(BaseModel):
|
||||||
|
"""Création d'une commande"""
|
||||||
|
|
||||||
|
client_id: str
|
||||||
|
date_commande: Optional[datetime] = None
|
||||||
|
date_livraison: Optional[datetime] = None
|
||||||
|
reference: Optional[str] = None
|
||||||
|
lignes: List[Dict]
|
||||||
|
|
||||||
|
|
||||||
|
class CommandeUpdate(BaseModel):
|
||||||
|
"""Modèle pour modification commande côté gateway"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
commande_data: Dict
|
||||||
18
schemas/documents/devis.py
Normal file
18
schemas/documents/devis.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class DevisRequest(BaseModel):
|
||||||
|
client_id: str
|
||||||
|
date_devis: Optional[datetime] = None
|
||||||
|
date_livraison: Optional[datetime] = None
|
||||||
|
reference: Optional[str] = None
|
||||||
|
lignes: List[Dict]
|
||||||
|
|
||||||
|
|
||||||
|
class DevisUpdate(BaseModel):
|
||||||
|
"""Modèle pour modification devis côté gateway"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
devis_data: Dict
|
||||||
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]
|
||||||
29
schemas/documents/documents.py
Normal file
29
schemas/documents/documents.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class TypeDocument(int, Enum):
|
||||||
|
DEVIS = 0
|
||||||
|
BON_LIVRAISON = 1
|
||||||
|
BON_RETOUR = 2
|
||||||
|
COMMANDE = 3
|
||||||
|
PREPARATION = 4
|
||||||
|
FACTURE = 5
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentGet(BaseModel):
|
||||||
|
numero: str
|
||||||
|
type_doc: int
|
||||||
|
|
||||||
|
|
||||||
|
class TransformationRequest(BaseModel):
|
||||||
|
numero_source: str
|
||||||
|
type_source: int
|
||||||
|
type_cible: int
|
||||||
|
|
||||||
|
|
||||||
|
class PDFGeneration(BaseModel):
|
||||||
|
"""Modèle pour génération PDF"""
|
||||||
|
|
||||||
|
doc_id: str = Field(..., description="Numéro du document")
|
||||||
|
type_doc: int = Field(..., ge=0, le=60, description="Type de document Sage")
|
||||||
20
schemas/documents/factures.py
Normal file
20
schemas/documents/factures.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class FactureCreate(BaseModel):
|
||||||
|
"""Création d'une facture côté gateway"""
|
||||||
|
|
||||||
|
client_id: str
|
||||||
|
date_facture: Optional[datetime] = None
|
||||||
|
date_livraison: Optional[datetime] = None
|
||||||
|
lignes: List[Dict]
|
||||||
|
reference: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FactureUpdate(BaseModel):
|
||||||
|
"""Modèle pour modification facture côté gateway"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
facture_data: Dict
|
||||||
20
schemas/documents/livraisons.py
Normal file
20
schemas/documents/livraisons.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class LivraisonCreate(BaseModel):
|
||||||
|
"""Création d'une livraison côté gateway"""
|
||||||
|
|
||||||
|
client_id: str
|
||||||
|
date_livraison: Optional[datetime] = None
|
||||||
|
date_livraison_prevue: Optional[datetime] = None
|
||||||
|
lignes: List[Dict]
|
||||||
|
reference: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class LivraisonUpdate(BaseModel):
|
||||||
|
"""Modèle pour modification livraison côté gateway"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
livraison_data: Dict
|
||||||
238
schemas/documents/reglements.py
Normal file
238
schemas/documents/reglements.py
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class ModeReglement:
|
||||||
|
"""Modes de règlement Sage 100c"""
|
||||||
|
|
||||||
|
VIREMENT = 1
|
||||||
|
CHEQUE = 2
|
||||||
|
TRAITE = 3
|
||||||
|
CARTE_BANCAIRE = 4
|
||||||
|
LCR = 5
|
||||||
|
PRELEVEMENT = 6
|
||||||
|
ESPECES = 7
|
||||||
|
|
||||||
|
LIBELLES = {
|
||||||
|
1: "Virement",
|
||||||
|
2: "Chèque",
|
||||||
|
3: "Traite",
|
||||||
|
4: "Carte bancaire",
|
||||||
|
5: "LCR",
|
||||||
|
6: "Prélèvement",
|
||||||
|
7: "Espèces",
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_libelle(cls, code: int) -> str:
|
||||||
|
return cls.LIBELLES.get(code, f"Mode {code}")
|
||||||
|
|
||||||
|
|
||||||
|
class ModeReglementEnum(IntEnum):
|
||||||
|
"""Modes de règlement Sage 100c"""
|
||||||
|
|
||||||
|
VIREMENT = 1
|
||||||
|
CHEQUE = 2
|
||||||
|
TRAITE = 3
|
||||||
|
CARTE_BANCAIRE = 4
|
||||||
|
LCR = 5
|
||||||
|
PRELEVEMENT = 6
|
||||||
|
ESPECES = 7
|
||||||
|
|
||||||
|
|
||||||
|
class ReglementFactureRequest(BaseModel):
|
||||||
|
"""Requête de règlement d'une facture"""
|
||||||
|
|
||||||
|
montant: float = Field(..., gt=0, description="Montant du règlement")
|
||||||
|
mode_reglement: int = Field(
|
||||||
|
default=2,
|
||||||
|
ge=1,
|
||||||
|
le=7,
|
||||||
|
description="Mode de règlement (1=Virement, 2=Chèque, 3=Traite, 4=CB, 5=LCR, 6=Prélèvement, 7=Espèces)",
|
||||||
|
)
|
||||||
|
date_reglement: Optional[datetime] = Field(
|
||||||
|
default=None, description="Date du règlement (défaut: aujourd'hui)"
|
||||||
|
)
|
||||||
|
reference: Optional[str] = Field(
|
||||||
|
default="", max_length=35, description="Référence du règlement"
|
||||||
|
)
|
||||||
|
libelle: Optional[str] = Field(
|
||||||
|
default="", max_length=69, description="Libellé du règlement"
|
||||||
|
)
|
||||||
|
code_journal: str = Field(
|
||||||
|
default="BEU", max_length=6, description="Code journal comptable"
|
||||||
|
)
|
||||||
|
devise_code: Optional[int] = Field(0)
|
||||||
|
cours_devise: Optional[float] = Field(1.0)
|
||||||
|
tva_encaissement: Optional[bool] = Field(False)
|
||||||
|
compte_general: Optional[str] = Field(None)
|
||||||
|
|
||||||
|
@field_validator("montant")
|
||||||
|
def validate_montant(cls, v):
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("Le montant doit être positif")
|
||||||
|
return round(v, 2)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"montant": 375.12,
|
||||||
|
"mode_reglement": 2,
|
||||||
|
"date_reglement": "2026-01-06T00:00:00",
|
||||||
|
"reference": "CHQ-001",
|
||||||
|
"libelle": "Règlement facture",
|
||||||
|
"code_journal": "BEU",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ReglementMultipleRequest(BaseModel):
|
||||||
|
"""Requête de règlement multiple pour un client"""
|
||||||
|
|
||||||
|
client_code: str = Field(..., description="Code client")
|
||||||
|
montant_total: float = Field(..., gt=0, description="Montant total à régler")
|
||||||
|
mode_reglement: int = Field(default=2, ge=1, le=7)
|
||||||
|
date_reglement: Optional[datetime] = None
|
||||||
|
reference: Optional[str] = Field(default="", max_length=35)
|
||||||
|
libelle: Optional[str] = Field(default="", max_length=69)
|
||||||
|
code_journal: str = Field(default="BEU", max_length=6)
|
||||||
|
numeros_factures: Optional[List[str]] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Liste des factures à régler (sinon: plus anciennes d'abord)",
|
||||||
|
)
|
||||||
|
devise_code: Optional[int] = Field(0)
|
||||||
|
cours_devise: Optional[float] = Field(1.0)
|
||||||
|
tva_encaissement: Optional[bool] = Field(False)
|
||||||
|
|
||||||
|
@field_validator("client_code", mode="before")
|
||||||
|
def strip_client_code(cls, v):
|
||||||
|
return v.replace("\xa0", "").strip() if v else v
|
||||||
|
|
||||||
|
@field_validator("montant_total")
|
||||||
|
def validate_montant(cls, v):
|
||||||
|
if v <= 0:
|
||||||
|
raise ValueError("Le montant doit être positif")
|
||||||
|
return round(v, 2)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"client_code": "CLI000001",
|
||||||
|
"montant_total": 1000.00,
|
||||||
|
"mode_reglement": 2,
|
||||||
|
"reference": "VIR-MULTI-001",
|
||||||
|
"numeros_factures": ["FA00081", "FA00082"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ReglementResponse(BaseModel):
|
||||||
|
"""Réponse d'un règlement effectué"""
|
||||||
|
|
||||||
|
numero_facture: str
|
||||||
|
numero_reglement: Optional[str]
|
||||||
|
montant_regle: float
|
||||||
|
date_reglement: str
|
||||||
|
mode_reglement: int
|
||||||
|
mode_reglement_libelle: str
|
||||||
|
reference: str
|
||||||
|
libelle: str
|
||||||
|
code_journal: str
|
||||||
|
total_facture: float
|
||||||
|
solde_restant: float
|
||||||
|
facture_soldee: bool
|
||||||
|
client_code: str
|
||||||
|
|
||||||
|
|
||||||
|
class ReglementMultipleResponse(BaseModel):
|
||||||
|
"""Réponse d'un règlement multiple"""
|
||||||
|
|
||||||
|
client_code: str
|
||||||
|
montant_demande: float
|
||||||
|
montant_effectif: float
|
||||||
|
nb_factures_reglees: int
|
||||||
|
nb_factures_soldees: int
|
||||||
|
date_reglement: str
|
||||||
|
mode_reglement: int
|
||||||
|
mode_reglement_libelle: str
|
||||||
|
reference: str
|
||||||
|
reglements: List[ReglementResponse]
|
||||||
|
|
||||||
|
|
||||||
|
class ReglementDetail(BaseModel):
|
||||||
|
"""Détail d'un règlement"""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
date: Optional[str]
|
||||||
|
montant: float
|
||||||
|
reference: str
|
||||||
|
libelle: str
|
||||||
|
mode_reglement: int
|
||||||
|
mode_reglement_libelle: str
|
||||||
|
code_journal: str
|
||||||
|
|
||||||
|
|
||||||
|
class ReglementsFactureResponse(BaseModel):
|
||||||
|
"""Réponse: tous les règlements d'une facture"""
|
||||||
|
|
||||||
|
numero_facture: str
|
||||||
|
client_code: str
|
||||||
|
date_facture: Optional[str]
|
||||||
|
reference: str
|
||||||
|
total_ttc: float
|
||||||
|
total_regle: float
|
||||||
|
solde_restant: float
|
||||||
|
est_soldee: bool
|
||||||
|
nb_reglements: int
|
||||||
|
reglements: List[ReglementDetail]
|
||||||
|
|
||||||
|
|
||||||
|
class FactureAvecReglements(BaseModel):
|
||||||
|
"""Facture avec ses règlements"""
|
||||||
|
|
||||||
|
numero_facture: str
|
||||||
|
date_facture: Optional[str]
|
||||||
|
total_ttc: float
|
||||||
|
reference: str
|
||||||
|
total_regle: float
|
||||||
|
solde_restant: float
|
||||||
|
est_soldee: bool
|
||||||
|
nb_reglements: int
|
||||||
|
reglements: List[ReglementDetail]
|
||||||
|
|
||||||
|
|
||||||
|
class ReglementsClientResponse(BaseModel):
|
||||||
|
"""Réponse: tous les règlements d'un client"""
|
||||||
|
|
||||||
|
client_code: str
|
||||||
|
client_intitule: str
|
||||||
|
nb_factures: int
|
||||||
|
nb_factures_soldees: int
|
||||||
|
nb_factures_en_cours: int
|
||||||
|
total_factures: float
|
||||||
|
total_regle: float
|
||||||
|
solde_global: float
|
||||||
|
factures: List[FactureAvecReglements]
|
||||||
|
|
||||||
|
|
||||||
|
class ModeReglementInfo(BaseModel):
|
||||||
|
"""Information sur un mode de règlement"""
|
||||||
|
|
||||||
|
code: int
|
||||||
|
libelle: str
|
||||||
|
|
||||||
|
|
||||||
|
class ModesReglementResponse(BaseModel):
|
||||||
|
"""Liste des modes de règlement disponibles"""
|
||||||
|
|
||||||
|
modes: List[ModeReglementInfo] = [
|
||||||
|
ModeReglementInfo(code=1, libelle="Virement"),
|
||||||
|
ModeReglementInfo(code=2, libelle="Chèque"),
|
||||||
|
ModeReglementInfo(code=3, libelle="Traite"),
|
||||||
|
ModeReglementInfo(code=4, libelle="Carte bancaire"),
|
||||||
|
ModeReglementInfo(code=5, libelle="LCR"),
|
||||||
|
ModeReglementInfo(code=6, libelle="Prélèvement"),
|
||||||
|
ModeReglementInfo(code=7, libelle="Espèces"),
|
||||||
|
]
|
||||||
21
schemas/others/general_schema.py
Normal file
21
schemas/others/general_schema.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class FiltreRequest(BaseModel):
|
||||||
|
filtre: Optional[str] = ""
|
||||||
|
|
||||||
|
|
||||||
|
class CodeRequest(BaseModel):
|
||||||
|
code: str
|
||||||
|
|
||||||
|
|
||||||
|
class ChampLibre(BaseModel):
|
||||||
|
doc_id: str
|
||||||
|
type_doc: int
|
||||||
|
nom_champ: str
|
||||||
|
valeur: str
|
||||||
|
|
||||||
|
|
||||||
|
class StatutRequest(BaseModel):
|
||||||
|
nouveau_statut: int
|
||||||
411
schemas/tiers/clients.py
Normal file
411
schemas/tiers/clients.py
Normal file
|
|
@ -0,0 +1,411 @@
|
||||||
|
from pydantic import BaseModel, Field, field_validator
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
|
class ClientCreate(BaseModel):
|
||||||
|
intitule: str = Field(
|
||||||
|
..., max_length=69, description="Nom du client (CT_Intitule) - OBLIGATOIRE"
|
||||||
|
)
|
||||||
|
|
||||||
|
numero: Optional[str] = Field(
|
||||||
|
None, max_length=17, description="Numéro client CT_Num (auto si None)"
|
||||||
|
)
|
||||||
|
|
||||||
|
type_tiers: int = Field(
|
||||||
|
0,
|
||||||
|
ge=0,
|
||||||
|
le=3,
|
||||||
|
description="CT_Type: 0=Client, 1=Fournisseur, 2=Salarié, 3=Autre",
|
||||||
|
)
|
||||||
|
|
||||||
|
qualite: Optional[str] = Field(
|
||||||
|
"CLI", max_length=17, description="CT_Qualite: CLI/FOU/SAL/DIV/AUT"
|
||||||
|
)
|
||||||
|
|
||||||
|
classement: Optional[str] = Field(None, max_length=17, description="CT_Classement")
|
||||||
|
|
||||||
|
raccourci: Optional[str] = Field(
|
||||||
|
None, max_length=7, description="CT_Raccourci (7 chars max, unique)"
|
||||||
|
)
|
||||||
|
|
||||||
|
siret: Optional[str] = Field(
|
||||||
|
None, max_length=15, description="CT_Siret (14-15 chars)"
|
||||||
|
)
|
||||||
|
|
||||||
|
tva_intra: Optional[str] = Field(
|
||||||
|
None, max_length=25, description="CT_Identifiant (TVA intracommunautaire)"
|
||||||
|
)
|
||||||
|
|
||||||
|
code_naf: Optional[str] = Field(
|
||||||
|
None, max_length=7, description="CT_Ape (Code NAF/APE)"
|
||||||
|
)
|
||||||
|
|
||||||
|
contact: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
max_length=35,
|
||||||
|
description="CT_Contact (double affectation: client + adresse)",
|
||||||
|
)
|
||||||
|
|
||||||
|
adresse: Optional[str] = Field(None, max_length=35, description="Adresse.Adresse")
|
||||||
|
|
||||||
|
complement: Optional[str] = Field(
|
||||||
|
None, max_length=35, description="Adresse.Complement"
|
||||||
|
)
|
||||||
|
|
||||||
|
code_postal: Optional[str] = Field(
|
||||||
|
None, max_length=9, description="Adresse.CodePostal"
|
||||||
|
)
|
||||||
|
|
||||||
|
ville: Optional[str] = Field(None, max_length=35, description="Adresse.Ville")
|
||||||
|
|
||||||
|
region: Optional[str] = Field(None, max_length=25, description="Adresse.CodeRegion")
|
||||||
|
|
||||||
|
pays: Optional[str] = Field(None, max_length=35, description="Adresse.Pays")
|
||||||
|
|
||||||
|
telephone: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="Telecom.Telephone"
|
||||||
|
)
|
||||||
|
|
||||||
|
telecopie: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="Telecom.Telecopie (fax)"
|
||||||
|
)
|
||||||
|
|
||||||
|
email: Optional[str] = Field(None, max_length=69, description="Telecom.EMail")
|
||||||
|
|
||||||
|
site_web: Optional[str] = Field(None, max_length=69, description="Telecom.Site")
|
||||||
|
|
||||||
|
portable: Optional[str] = Field(None, max_length=21, description="Telecom.Portable")
|
||||||
|
|
||||||
|
facebook: Optional[str] = Field(
|
||||||
|
None, max_length=69, description="Telecom.Facebook ou CT_Facebook"
|
||||||
|
)
|
||||||
|
|
||||||
|
linkedin: Optional[str] = Field(
|
||||||
|
None, max_length=69, description="Telecom.LinkedIn ou CT_LinkedIn"
|
||||||
|
)
|
||||||
|
|
||||||
|
compte_general: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
max_length=13,
|
||||||
|
description="CompteGPrinc (défaut selon type_tiers: 4110000, 4010000, 421, 471)",
|
||||||
|
)
|
||||||
|
|
||||||
|
categorie_tarifaire: Optional[str] = Field(
|
||||||
|
None, description="N_CatTarif (ID catégorie tarifaire, défaut '0' ou '1')"
|
||||||
|
)
|
||||||
|
|
||||||
|
categorie_comptable: Optional[str] = Field(
|
||||||
|
None, description="N_CatCompta (ID catégorie comptable, défaut '0' ou '1')"
|
||||||
|
)
|
||||||
|
|
||||||
|
taux01: Optional[float] = Field(None, description="CT_Taux01")
|
||||||
|
taux02: Optional[float] = Field(None, description="CT_Taux02")
|
||||||
|
taux03: Optional[float] = Field(None, description="CT_Taux03")
|
||||||
|
taux04: Optional[float] = Field(None, description="CT_Taux04")
|
||||||
|
|
||||||
|
secteur: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="Alias de statistique01 (CT_Statistique01)"
|
||||||
|
)
|
||||||
|
|
||||||
|
statistique01: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique01"
|
||||||
|
)
|
||||||
|
statistique02: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique02"
|
||||||
|
)
|
||||||
|
statistique03: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique03"
|
||||||
|
)
|
||||||
|
statistique04: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique04"
|
||||||
|
)
|
||||||
|
statistique05: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique05"
|
||||||
|
)
|
||||||
|
statistique06: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique06"
|
||||||
|
)
|
||||||
|
statistique07: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique07"
|
||||||
|
)
|
||||||
|
statistique08: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique08"
|
||||||
|
)
|
||||||
|
statistique09: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique09"
|
||||||
|
)
|
||||||
|
statistique10: Optional[str] = Field(
|
||||||
|
None, max_length=21, description="CT_Statistique10"
|
||||||
|
)
|
||||||
|
|
||||||
|
encours_autorise: Optional[float] = Field(
|
||||||
|
None, description="CT_Encours (montant max autorisé)"
|
||||||
|
)
|
||||||
|
|
||||||
|
assurance_credit: Optional[float] = Field(
|
||||||
|
None, description="CT_Assurance (montant assurance crédit)"
|
||||||
|
)
|
||||||
|
|
||||||
|
langue: Optional[int] = Field(
|
||||||
|
None, ge=0, description="CT_Langue (0=Français, 1=Anglais, etc.)"
|
||||||
|
)
|
||||||
|
|
||||||
|
commercial_code: Optional[int] = Field(
|
||||||
|
None, description="CO_No (ID du collaborateur commercial)"
|
||||||
|
)
|
||||||
|
|
||||||
|
lettrage_auto: Optional[bool] = Field(
|
||||||
|
True, description="CT_Lettrage (1=oui, 0=non)"
|
||||||
|
)
|
||||||
|
|
||||||
|
est_actif: Optional[bool] = Field(
|
||||||
|
True, description="Inverse de CT_Sommeil (True=actif, False=en sommeil)"
|
||||||
|
)
|
||||||
|
|
||||||
|
type_facture: Optional[int] = Field(
|
||||||
|
1, ge=0, le=2, description="CT_Facture: 0=aucune, 1=normale, 2=regroupée"
|
||||||
|
)
|
||||||
|
|
||||||
|
est_prospect: Optional[bool] = Field(
|
||||||
|
False, description="CT_Prospect (1=oui, 0=non)"
|
||||||
|
)
|
||||||
|
|
||||||
|
bl_en_facture: Optional[int] = Field(
|
||||||
|
None, ge=0, le=1, description="CT_BLFact (impression BL sur facture)"
|
||||||
|
)
|
||||||
|
|
||||||
|
saut_page: Optional[int] = Field(
|
||||||
|
None, ge=0, le=1, description="CT_Saut (saut de page après impression)"
|
||||||
|
)
|
||||||
|
|
||||||
|
validation_echeance: Optional[int] = Field(
|
||||||
|
None, ge=0, le=1, description="CT_ValidEch"
|
||||||
|
)
|
||||||
|
|
||||||
|
controle_encours: Optional[int] = Field(
|
||||||
|
None, ge=0, le=1, description="CT_ControlEnc"
|
||||||
|
)
|
||||||
|
|
||||||
|
exclure_relance: Optional[int] = Field(None, ge=0, le=1, description="CT_NotRappel")
|
||||||
|
|
||||||
|
exclure_penalites: Optional[int] = Field(
|
||||||
|
None, ge=0, le=1, description="CT_NotPenal"
|
||||||
|
)
|
||||||
|
|
||||||
|
bon_a_payer: Optional[int] = Field(None, ge=0, le=1, description="CT_BonAPayer")
|
||||||
|
|
||||||
|
priorite_livraison: Optional[int] = Field(
|
||||||
|
None, ge=0, le=5, description="CT_PrioriteLivr"
|
||||||
|
)
|
||||||
|
|
||||||
|
livraison_partielle: Optional[int] = Field(
|
||||||
|
None, ge=0, le=1, description="CT_LivrPartielle"
|
||||||
|
)
|
||||||
|
|
||||||
|
delai_transport: Optional[int] = Field(
|
||||||
|
None, ge=0, description="CT_DelaiTransport (jours)"
|
||||||
|
)
|
||||||
|
|
||||||
|
delai_appro: Optional[int] = Field(None, ge=0, description="CT_DelaiAppro (jours)")
|
||||||
|
|
||||||
|
commentaire: Optional[str] = Field(
|
||||||
|
None, max_length=35, description="CT_Commentaire"
|
||||||
|
)
|
||||||
|
|
||||||
|
section_analytique: Optional[str] = Field(None, max_length=13, description="CA_Num")
|
||||||
|
|
||||||
|
mode_reglement_code: Optional[int] = Field(
|
||||||
|
None, description="MR_No (ID du mode de règlement)"
|
||||||
|
)
|
||||||
|
|
||||||
|
surveillance_active: Optional[int] = Field(
|
||||||
|
None, ge=0, le=1, description="CT_Surveillance (DOIT être défini AVANT coface)"
|
||||||
|
)
|
||||||
|
|
||||||
|
coface: Optional[str] = Field(
|
||||||
|
None, max_length=25, description="CT_Coface (code Coface)"
|
||||||
|
)
|
||||||
|
|
||||||
|
forme_juridique: Optional[str] = Field(
|
||||||
|
None, max_length=33, description="CT_SvFormeJuri (SARL, SA, etc.)"
|
||||||
|
)
|
||||||
|
|
||||||
|
effectif: Optional[str] = Field(None, max_length=11, description="CT_SvEffectif")
|
||||||
|
|
||||||
|
sv_regularite: Optional[str] = Field(None, max_length=3, description="CT_SvRegul")
|
||||||
|
|
||||||
|
sv_cotation: Optional[str] = Field(None, max_length=5, description="CT_SvCotation")
|
||||||
|
|
||||||
|
sv_objet_maj: Optional[str] = Field(
|
||||||
|
None, max_length=61, description="CT_SvObjetMaj"
|
||||||
|
)
|
||||||
|
|
||||||
|
ca_annuel: Optional[float] = Field(
|
||||||
|
None,
|
||||||
|
description="CT_SvCA (Chiffre d'affaires annuel) - alias: sv_chiffre_affaires",
|
||||||
|
)
|
||||||
|
|
||||||
|
sv_chiffre_affaires: Optional[float] = Field(
|
||||||
|
None, description="CT_SvCA (alias de ca_annuel)"
|
||||||
|
)
|
||||||
|
|
||||||
|
sv_resultat: Optional[float] = Field(None, description="CT_SvResultat")
|
||||||
|
|
||||||
|
@field_validator("siret")
|
||||||
|
@classmethod
|
||||||
|
def validate_siret(cls, v):
|
||||||
|
"""Valide et nettoie le SIRET"""
|
||||||
|
if v and v.lower() not in ("none", "null", ""):
|
||||||
|
cleaned = v.replace(" ", "").replace("-", "")
|
||||||
|
if len(cleaned) not in (14, 15):
|
||||||
|
raise ValueError("Le SIRET doit contenir 14 ou 15 caractères")
|
||||||
|
return cleaned
|
||||||
|
return None
|
||||||
|
|
||||||
|
@field_validator("email")
|
||||||
|
@classmethod
|
||||||
|
def validate_email(cls, v):
|
||||||
|
"""Valide le format email"""
|
||||||
|
if v and v.lower() not in ("none", "null", ""):
|
||||||
|
v = v.strip()
|
||||||
|
if "@" not in v:
|
||||||
|
raise ValueError("Format email invalide")
|
||||||
|
return v
|
||||||
|
return None
|
||||||
|
|
||||||
|
@field_validator("raccourci")
|
||||||
|
@classmethod
|
||||||
|
def validate_raccourci(cls, v):
|
||||||
|
"""Force le raccourci en majuscules"""
|
||||||
|
if v and v.lower() not in ("none", "null", ""):
|
||||||
|
return v.upper().strip()[:7]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@field_validator(
|
||||||
|
"adresse",
|
||||||
|
"code_postal",
|
||||||
|
"ville",
|
||||||
|
"pays",
|
||||||
|
"telephone",
|
||||||
|
"tva_intra",
|
||||||
|
"contact",
|
||||||
|
"complement",
|
||||||
|
mode="before",
|
||||||
|
)
|
||||||
|
@classmethod
|
||||||
|
def clean_none_strings(cls, v):
|
||||||
|
"""Convertit les chaînes 'None'/'null'/'' en None"""
|
||||||
|
if isinstance(v, str) and v.lower() in ("none", "null", ""):
|
||||||
|
return None
|
||||||
|
return v
|
||||||
|
|
||||||
|
def to_sage_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Convertit le modèle en dictionnaire compatible avec creer_client()
|
||||||
|
Mapping 1:1 avec les paramètres réels de la fonction
|
||||||
|
"""
|
||||||
|
stat01 = self.statistique01 or self.secteur
|
||||||
|
|
||||||
|
ca = self.ca_annuel or self.sv_chiffre_affaires
|
||||||
|
|
||||||
|
return {
|
||||||
|
"intitule": self.intitule,
|
||||||
|
"numero": self.numero,
|
||||||
|
"type_tiers": self.type_tiers,
|
||||||
|
"qualite": self.qualite,
|
||||||
|
"classement": self.classement,
|
||||||
|
"raccourci": self.raccourci,
|
||||||
|
"siret": self.siret,
|
||||||
|
"tva_intra": self.tva_intra,
|
||||||
|
"code_naf": self.code_naf,
|
||||||
|
"contact": self.contact,
|
||||||
|
"adresse": self.adresse,
|
||||||
|
"complement": self.complement,
|
||||||
|
"code_postal": self.code_postal,
|
||||||
|
"ville": self.ville,
|
||||||
|
"region": self.region,
|
||||||
|
"pays": self.pays,
|
||||||
|
"telephone": self.telephone,
|
||||||
|
"telecopie": self.telecopie,
|
||||||
|
"email": self.email,
|
||||||
|
"site_web": self.site_web,
|
||||||
|
"portable": self.portable,
|
||||||
|
"facebook": self.facebook,
|
||||||
|
"linkedin": self.linkedin,
|
||||||
|
"compte_general": self.compte_general,
|
||||||
|
"categorie_tarifaire": self.categorie_tarifaire,
|
||||||
|
"categorie_comptable": self.categorie_comptable,
|
||||||
|
"taux01": self.taux01,
|
||||||
|
"taux02": self.taux02,
|
||||||
|
"taux03": self.taux03,
|
||||||
|
"taux04": self.taux04,
|
||||||
|
"statistique01": stat01,
|
||||||
|
"statistique02": self.statistique02,
|
||||||
|
"statistique03": self.statistique03,
|
||||||
|
"statistique04": self.statistique04,
|
||||||
|
"statistique05": self.statistique05,
|
||||||
|
"statistique06": self.statistique06,
|
||||||
|
"statistique07": self.statistique07,
|
||||||
|
"statistique08": self.statistique08,
|
||||||
|
"statistique09": self.statistique09,
|
||||||
|
"statistique10": self.statistique10,
|
||||||
|
"secteur": self.secteur,
|
||||||
|
"encours_autorise": self.encours_autorise,
|
||||||
|
"assurance_credit": self.assurance_credit,
|
||||||
|
"langue": self.langue,
|
||||||
|
"commercial_code": self.commercial_code,
|
||||||
|
"lettrage_auto": self.lettrage_auto,
|
||||||
|
"est_actif": self.est_actif,
|
||||||
|
"type_facture": self.type_facture,
|
||||||
|
"est_prospect": self.est_prospect,
|
||||||
|
"bl_en_facture": self.bl_en_facture,
|
||||||
|
"saut_page": self.saut_page,
|
||||||
|
"validation_echeance": self.validation_echeance,
|
||||||
|
"controle_encours": self.controle_encours,
|
||||||
|
"exclure_relance": self.exclure_relance,
|
||||||
|
"exclure_penalites": self.exclure_penalites,
|
||||||
|
"bon_a_payer": self.bon_a_payer,
|
||||||
|
"priorite_livraison": self.priorite_livraison,
|
||||||
|
"livraison_partielle": self.livraison_partielle,
|
||||||
|
"delai_transport": self.delai_transport,
|
||||||
|
"delai_appro": self.delai_appro,
|
||||||
|
"commentaire": self.commentaire,
|
||||||
|
"section_analytique": self.section_analytique,
|
||||||
|
"mode_reglement_code": self.mode_reglement_code,
|
||||||
|
"surveillance_active": self.surveillance_active,
|
||||||
|
"coface": self.coface,
|
||||||
|
"forme_juridique": self.forme_juridique,
|
||||||
|
"effectif": self.effectif,
|
||||||
|
"sv_regularite": self.sv_regularite,
|
||||||
|
"sv_cotation": self.sv_cotation,
|
||||||
|
"sv_objet_maj": self.sv_objet_maj,
|
||||||
|
"ca_annuel": ca,
|
||||||
|
"sv_chiffre_affaires": self.sv_chiffre_affaires,
|
||||||
|
"sv_resultat": self.sv_resultat,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"intitule": "ENTREPRISE EXEMPLE SARL",
|
||||||
|
"numero": "CLI00123",
|
||||||
|
"type_tiers": 0,
|
||||||
|
"qualite": "CLI",
|
||||||
|
"compte_general": "411000",
|
||||||
|
"est_prospect": False,
|
||||||
|
"est_actif": True,
|
||||||
|
"email": "contact@exemple.fr",
|
||||||
|
"telephone": "0123456789",
|
||||||
|
"adresse": "123 Rue de la Paix",
|
||||||
|
"code_postal": "75001",
|
||||||
|
"ville": "Paris",
|
||||||
|
"pays": "France",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ClientUpdate(BaseModel):
|
||||||
|
"""Modèle pour modification client côté gateway"""
|
||||||
|
|
||||||
|
code: str
|
||||||
|
client_data: Dict
|
||||||
65
schemas/tiers/commercial.py
Normal file
65
schemas/tiers/commercial.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborateurListRequest(BaseModel):
|
||||||
|
filtre: str = ""
|
||||||
|
actifs_seulement: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborateurNumeroRequest(BaseModel):
|
||||||
|
numero: int
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborateurCreateRequest(BaseModel):
|
||||||
|
nom: str
|
||||||
|
prenom: Optional[str] = None
|
||||||
|
fonction: Optional[str] = None
|
||||||
|
adresse: Optional[str] = None
|
||||||
|
complement: Optional[str] = None
|
||||||
|
code_postal: Optional[str] = None
|
||||||
|
ville: Optional[str] = None
|
||||||
|
code_region: Optional[str] = None
|
||||||
|
pays: Optional[str] = None
|
||||||
|
service: Optional[str] = None
|
||||||
|
vendeur: bool = False
|
||||||
|
caissier: bool = False
|
||||||
|
acheteur: bool = False
|
||||||
|
chef_ventes: bool = False
|
||||||
|
numero_chef_ventes: Optional[int] = None
|
||||||
|
telephone: Optional[str] = None
|
||||||
|
telecopie: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
tel_portable: Optional[str] = None
|
||||||
|
facebook: Optional[str] = None
|
||||||
|
linkedin: Optional[str] = None
|
||||||
|
skype: Optional[str] = None
|
||||||
|
matricule: Optional[str] = None
|
||||||
|
sommeil: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class CollaborateurUpdateRequest(CollaborateurNumeroRequest):
|
||||||
|
nom: Optional[str] = None
|
||||||
|
prenom: Optional[str] = None
|
||||||
|
fonction: Optional[str] = None
|
||||||
|
adresse: Optional[str] = None
|
||||||
|
complement: Optional[str] = None
|
||||||
|
code_postal: Optional[str] = None
|
||||||
|
ville: Optional[str] = None
|
||||||
|
code_region: Optional[str] = None
|
||||||
|
pays: Optional[str] = None
|
||||||
|
service: Optional[str] = None
|
||||||
|
vendeur: Optional[bool] = None
|
||||||
|
caissier: Optional[bool] = None
|
||||||
|
acheteur: Optional[bool] = None
|
||||||
|
chef_ventes: Optional[bool] = None
|
||||||
|
numero_chef_ventes: Optional[int] = None
|
||||||
|
telephone: Optional[str] = None
|
||||||
|
telecopie: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
tel_portable: Optional[str] = None
|
||||||
|
facebook: Optional[str] = None
|
||||||
|
linkedin: Optional[str] = None
|
||||||
|
skype: Optional[str] = None
|
||||||
|
matricule: Optional[str] = None
|
||||||
|
sommeil: Optional[bool] = None
|
||||||
48
schemas/tiers/contact.py
Normal file
48
schemas/tiers/contact.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
|
class ContactCreate(BaseModel):
|
||||||
|
"""Requête de création de contact"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
civilite: Optional[str] = None
|
||||||
|
nom: str
|
||||||
|
prenom: Optional[str] = None
|
||||||
|
fonction: Optional[str] = None
|
||||||
|
service_code: Optional[int] = None
|
||||||
|
telephone: Optional[str] = None
|
||||||
|
portable: Optional[str] = None
|
||||||
|
telecopie: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
facebook: Optional[str] = None
|
||||||
|
linkedin: Optional[str] = None
|
||||||
|
skype: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ContactList(BaseModel):
|
||||||
|
"""Requête de liste des contacts"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
|
||||||
|
|
||||||
|
class ContactGet(BaseModel):
|
||||||
|
"""Requête de récupération d'un contact"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
contact_numero: int
|
||||||
|
|
||||||
|
|
||||||
|
class ContactUpdate(BaseModel):
|
||||||
|
"""Requête de modification d'un contact"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
contact_numero: int
|
||||||
|
updates: Dict
|
||||||
|
|
||||||
|
|
||||||
|
class ContactDelete(BaseModel):
|
||||||
|
"""Requête de suppression d'un contact"""
|
||||||
|
|
||||||
|
numero: str
|
||||||
|
contact_numero: int
|
||||||
23
schemas/tiers/fournisseurs.py
Normal file
23
schemas/tiers/fournisseurs.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
|
class FournisseurCreate(BaseModel):
|
||||||
|
intitule: str = Field(..., description="Raison sociale du fournisseur")
|
||||||
|
compte_collectif: str = Field("401000", description="Compte général rattaché")
|
||||||
|
num: Optional[str] = Field(None, description="Code fournisseur (auto si vide)")
|
||||||
|
adresse: Optional[str] = None
|
||||||
|
code_postal: Optional[str] = None
|
||||||
|
ville: Optional[str] = None
|
||||||
|
pays: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
telephone: Optional[str] = None
|
||||||
|
siret: Optional[str] = None
|
||||||
|
tva_intra: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FournisseurUpdate(BaseModel):
|
||||||
|
"""Modèle pour modification fournisseur côté gateway"""
|
||||||
|
|
||||||
|
code: str
|
||||||
|
fournisseur_data: Dict
|
||||||
21
schemas/tiers/tiers.py
Normal file
21
schemas/tiers/tiers.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class TiersList(BaseModel):
|
||||||
|
"""Requête de listage des tiers"""
|
||||||
|
|
||||||
|
type_tiers: Optional[str] = Field(
|
||||||
|
None, description="Type: client, fournisseur, prospect, all"
|
||||||
|
)
|
||||||
|
filtre: str = Field("", description="Filtre sur code ou intitulé")
|
||||||
|
|
||||||
|
|
||||||
|
class TypeTiers(IntEnum):
|
||||||
|
"""CT_Type - Type de tiers"""
|
||||||
|
|
||||||
|
CLIENT = 0
|
||||||
|
FOURNISSEUR = 1
|
||||||
|
SALARIE = 2
|
||||||
|
AUTRE = 3
|
||||||
48
utils/__init__.py
Normal file
48
utils/__init__.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
from .enums import (
|
||||||
|
TypeArticle,
|
||||||
|
TypeCompta,
|
||||||
|
TypeRessource,
|
||||||
|
TypeTiers,
|
||||||
|
TypeEmplacement,
|
||||||
|
TypeFamille,
|
||||||
|
NomenclatureType,
|
||||||
|
SuiviStockType,
|
||||||
|
normalize_enum_to_string,
|
||||||
|
normalize_enum_to_int,
|
||||||
|
normalize_string_field,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .article_fields import (
|
||||||
|
valider_donnees_creation,
|
||||||
|
mapper_champ_api_vers_sage,
|
||||||
|
CHAMPS_STOCK_INITIAL,
|
||||||
|
CHAMPS_ASSIGNABLES_CREATION,
|
||||||
|
CHAMPS_ASSIGNABLES_MODIFICATION,
|
||||||
|
CHAMPS_OBJETS_SPECIAUX,
|
||||||
|
valider_champ,
|
||||||
|
valider_donnees_modification,
|
||||||
|
obtenir_champs_assignables
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"TypeArticle",
|
||||||
|
"TypeCompta",
|
||||||
|
"TypeRessource",
|
||||||
|
"TypeTiers",
|
||||||
|
"TypeEmplacement",
|
||||||
|
"TypeFamille",
|
||||||
|
"NomenclatureType",
|
||||||
|
"SuiviStockType",
|
||||||
|
"normalize_enum_to_string",
|
||||||
|
"normalize_enum_to_int",
|
||||||
|
"normalize_string_field",
|
||||||
|
"valider_donnees_creation",
|
||||||
|
"mapper_champ_api_vers_sage",
|
||||||
|
"CHAMPS_STOCK_INITIAL",
|
||||||
|
"CHAMPS_ASSIGNABLES_MODIFICATION",
|
||||||
|
"CHAMPS_OBJETS_SPECIAUX",
|
||||||
|
"CHAMPS_ASSIGNABLES_CREATION",
|
||||||
|
"valider_champ",
|
||||||
|
"valider_donnees_modification",
|
||||||
|
"obtenir_champs_assignables"
|
||||||
|
]
|
||||||
170
utils/article_fields.py
Normal file
170
utils/article_fields.py
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
CHAMPS_ASSIGNABLES_CREATION = {
|
||||||
|
"AR_Design": {"max_length": 69, "required": True, "description": "Désignation"},
|
||||||
|
"AR_PrixVen": {"type": float, "min": 0, "description": "Prix de vente HT"},
|
||||||
|
"AR_PrixAch": {"type": float, "min": 0, "description": "Prix achat HT"},
|
||||||
|
"AR_PrixAchat": {"type": float, "min": 0, "description": "Prix achat HT (alias)"},
|
||||||
|
"AR_CodeBarre": {"max_length": 13, "description": "Code-barres EAN"},
|
||||||
|
"AR_Commentaire": {"max_length": 255, "description": "Description/Commentaire"},
|
||||||
|
"AR_UniteVen": {"max_length": 10, "description": "Unité de vente"},
|
||||||
|
"AR_CodeFiscal": {"max_length": 10, "description": "Code fiscal/TVA"},
|
||||||
|
"AR_Pays": {"max_length": 3, "description": "Pays d'origine"},
|
||||||
|
"AR_Garantie": {"type": int, "min": 0, "description": "Garantie en mois"},
|
||||||
|
"AR_Delai": {"type": int, "min": 0, "description": "Délai livraison jours"},
|
||||||
|
"AR_Coef": {"type": float, "min": 0, "description": "Coefficient"},
|
||||||
|
"AR_PoidsNet": {"type": float, "min": 0, "description": "Poids net kg"},
|
||||||
|
"AR_PoidsBrut": {"type": float, "min": 0, "description": "Poids brut kg"},
|
||||||
|
"AR_Stat01": {"max_length": 20, "description": "Statistique 1"},
|
||||||
|
"AR_Stat02": {"max_length": 20, "description": "Statistique 2"},
|
||||||
|
"AR_Stat03": {"max_length": 20, "description": "Statistique 3"},
|
||||||
|
"AR_Stat04": {"max_length": 20, "description": "Statistique 4"},
|
||||||
|
"AR_Stat05": {"max_length": 20, "description": "Statistique 5"},
|
||||||
|
"AR_Escompte": {"type": bool, "description": "Soumis à escompte"},
|
||||||
|
"AR_Publie": {"type": bool, "description": "Publié web/catalogue"},
|
||||||
|
"AR_Sommeil": {"type": int, "values": [0, 1], "description": "Actif/Sommeil"},
|
||||||
|
}
|
||||||
|
|
||||||
|
CHAMPS_ASSIGNABLES_MODIFICATION = {
|
||||||
|
**CHAMPS_ASSIGNABLES_CREATION,
|
||||||
|
"AR_Stock": {"type": float, "min": 0, "description": "Stock réel"},
|
||||||
|
"AR_StockMini": {"type": float, "min": 0, "description": "Stock minimum"},
|
||||||
|
"AR_StockMaxi": {"type": float, "min": 0, "description": "Stock maximum"},
|
||||||
|
}
|
||||||
|
|
||||||
|
CHAMPS_OBJETS_SPECIAUX = {
|
||||||
|
"Unite": {"description": "Unité de vente (objet)", "copie_modele": True},
|
||||||
|
"Famille": {"description": "Famille article (objet)", "validation_sql": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
CHAMPS_STOCK_INITIAL = {
|
||||||
|
"stock_reel": {"type": float, "min": 0, "description": "Stock initial"},
|
||||||
|
"stock_mini": {"type": float, "min": 0, "description": "Stock minimum"},
|
||||||
|
"stock_maxi": {"type": float, "min": 0, "description": "Stock maximum"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def valider_champ(
|
||||||
|
nom_champ: str, valeur: Any, config: Dict
|
||||||
|
) -> tuple[bool, Optional[str]]:
|
||||||
|
if valeur is None:
|
||||||
|
if config.get("required"):
|
||||||
|
return False, f"Le champ {nom_champ} est obligatoire"
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
if "type" in config:
|
||||||
|
expected_type = config["type"]
|
||||||
|
try:
|
||||||
|
if expected_type is float:
|
||||||
|
valeur = float(valeur)
|
||||||
|
elif expected_type is int:
|
||||||
|
valeur = int(valeur)
|
||||||
|
elif expected_type is bool:
|
||||||
|
valeur = bool(valeur)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Le champ {nom_champ} doit être de type {expected_type.__name__}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if "min" in config:
|
||||||
|
if isinstance(valeur, (int, float)) and valeur < config["min"]:
|
||||||
|
return False, f"Le champ {nom_champ} doit être >= {config['min']}"
|
||||||
|
|
||||||
|
if "max_length" in config:
|
||||||
|
if isinstance(valeur, str) and len(valeur) > config["max_length"]:
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Le champ {nom_champ} ne peut dépasser {config['max_length']} caractères",
|
||||||
|
)
|
||||||
|
|
||||||
|
if "values" in config:
|
||||||
|
if valeur not in config["values"]:
|
||||||
|
return False, f"Le champ {nom_champ} doit être parmi {config['values']}"
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def valider_donnees_creation(data: Dict) -> tuple[bool, Optional[str]]:
|
||||||
|
if "reference" not in data or not data["reference"]:
|
||||||
|
return False, "Le champ 'reference' est obligatoire"
|
||||||
|
|
||||||
|
if len(str(data["reference"])) > 18:
|
||||||
|
return False, "La référence ne peut dépasser 18 caractères"
|
||||||
|
|
||||||
|
if "designation" not in data or not data["designation"]:
|
||||||
|
return False, "Le champ 'designation' est obligatoire"
|
||||||
|
|
||||||
|
for champ, valeur in data.items():
|
||||||
|
if champ in CHAMPS_ASSIGNABLES_CREATION:
|
||||||
|
valide, erreur = valider_champ(
|
||||||
|
champ, valeur, CHAMPS_ASSIGNABLES_CREATION[champ]
|
||||||
|
)
|
||||||
|
if not valide:
|
||||||
|
return False, erreur
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def valider_donnees_modification(data: Dict) -> tuple[bool, Optional[str]]:
|
||||||
|
if not data:
|
||||||
|
return False, "Aucun champ à modifier"
|
||||||
|
|
||||||
|
for champ, valeur in data.items():
|
||||||
|
if champ in ["famille", "stock_reel", "stock_mini", "stock_maxi"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if champ in CHAMPS_ASSIGNABLES_MODIFICATION:
|
||||||
|
valide, erreur = valider_champ(
|
||||||
|
champ, valeur, CHAMPS_ASSIGNABLES_MODIFICATION[champ]
|
||||||
|
)
|
||||||
|
if not valide:
|
||||||
|
return False, erreur
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def mapper_champ_api_vers_sage(champ_api: str) -> Optional[str]:
|
||||||
|
mapping = {
|
||||||
|
"designation": "AR_Design",
|
||||||
|
"prix_vente": "AR_PrixVen",
|
||||||
|
"prix_achat": "AR_PrixAch",
|
||||||
|
"code_ean": "AR_CodeBarre",
|
||||||
|
"code_barre": "AR_CodeBarre",
|
||||||
|
"description": "AR_Commentaire",
|
||||||
|
"unite_vente": "AR_UniteVen",
|
||||||
|
"code_fiscal": "AR_CodeFiscal",
|
||||||
|
"tva_code": "AR_CodeFiscal",
|
||||||
|
"pays": "AR_Pays",
|
||||||
|
"garantie": "AR_Garantie",
|
||||||
|
"delai": "AR_Delai",
|
||||||
|
"coef": "AR_Coef",
|
||||||
|
"coefficient": "AR_Coef",
|
||||||
|
"poids_net": "AR_PoidsNet",
|
||||||
|
"poids_brut": "AR_PoidsBrut",
|
||||||
|
"stat_01": "AR_Stat01",
|
||||||
|
"stat_02": "AR_Stat02",
|
||||||
|
"stat_03": "AR_Stat03",
|
||||||
|
"stat_04": "AR_Stat04",
|
||||||
|
"stat_05": "AR_Stat05",
|
||||||
|
"soumis_escompte": "AR_Escompte",
|
||||||
|
"publie": "AR_Publie",
|
||||||
|
"en_sommeil": "AR_Sommeil",
|
||||||
|
"stock_reel": "AR_Stock",
|
||||||
|
"stock_mini": "AR_StockMini",
|
||||||
|
"stock_maxi": "AR_StockMaxi",
|
||||||
|
}
|
||||||
|
return mapping.get(champ_api, champ_api)
|
||||||
|
|
||||||
|
|
||||||
|
def obtenir_champs_assignables() -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"creation": list(CHAMPS_ASSIGNABLES_CREATION.keys()),
|
||||||
|
"modification": list(CHAMPS_ASSIGNABLES_MODIFICATION.keys()),
|
||||||
|
"objets_speciaux": list(CHAMPS_OBJETS_SPECIAUX.keys()),
|
||||||
|
"stock_initial": list(CHAMPS_STOCK_INITIAL.keys()),
|
||||||
|
}
|
||||||
0
utils/articles/__init__.py
Normal file
0
utils/articles/__init__.py
Normal file
241
utils/articles/articles_data_com.py
Normal file
241
utils/articles/articles_data_com.py
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _extraire_article(article_obj):
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
"reference": getattr(article_obj, "AR_Ref", "").strip(),
|
||||||
|
"designation": getattr(article_obj, "AR_Design", "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
data["code_ean"] = ""
|
||||||
|
data["code_barre"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
code_barre = getattr(article_obj, "AR_CodeBarre", "").strip()
|
||||||
|
if code_barre:
|
||||||
|
data["code_ean"] = code_barre
|
||||||
|
data["code_barre"] = code_barre
|
||||||
|
|
||||||
|
if not data["code_ean"]:
|
||||||
|
code_barre1 = getattr(article_obj, "AR_CodeBarre1", "").strip()
|
||||||
|
if code_barre1:
|
||||||
|
data["code_ean"] = code_barre1
|
||||||
|
data["code_barre"] = code_barre1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["prix_vente"] = float(getattr(article_obj, "AR_PrixVen", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["prix_vente"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["prix_achat"] = float(getattr(article_obj, "AR_PrixAch", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["prix_achat"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["prix_revient"] = float(getattr(article_obj, "AR_PrixRevient", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["prix_revient"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["stock_reel"] = float(getattr(article_obj, "AR_Stock", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["stock_reel"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["stock_mini"] = float(getattr(article_obj, "AR_StockMini", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["stock_mini"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["stock_maxi"] = float(getattr(article_obj, "AR_StockMaxi", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["stock_maxi"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["stock_reserve"] = float(getattr(article_obj, "AR_QteCom", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["stock_reserve"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["stock_commande"] = float(getattr(article_obj, "AR_QteComFou", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["stock_commande"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["stock_disponible"] = data["stock_reel"] - data["stock_reserve"]
|
||||||
|
except Exception:
|
||||||
|
data["stock_disponible"] = data["stock_reel"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
commentaire = getattr(article_obj, "AR_Commentaire", "").strip()
|
||||||
|
data["description"] = commentaire
|
||||||
|
except Exception:
|
||||||
|
data["description"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
design2 = getattr(article_obj, "AR_Design2", "").strip()
|
||||||
|
data["designation_complementaire"] = design2
|
||||||
|
except Exception:
|
||||||
|
data["designation_complementaire"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
type_art = getattr(article_obj, "AR_Type", 0)
|
||||||
|
data["type_article"] = type_art
|
||||||
|
data["type_article_libelle"] = {
|
||||||
|
0: "Article",
|
||||||
|
1: "Prestation",
|
||||||
|
2: "Divers",
|
||||||
|
}.get(type_art, "Inconnu")
|
||||||
|
except Exception:
|
||||||
|
data["type_article"] = 0
|
||||||
|
data["type_article_libelle"] = "Article"
|
||||||
|
|
||||||
|
try:
|
||||||
|
famille_code = getattr(article_obj, "FA_CodeFamille", "").strip()
|
||||||
|
data["famille_code"] = famille_code
|
||||||
|
|
||||||
|
if famille_code:
|
||||||
|
try:
|
||||||
|
famille_obj = getattr(article_obj, "Famille", None)
|
||||||
|
if famille_obj:
|
||||||
|
famille_obj.Read()
|
||||||
|
data["famille_libelle"] = getattr(
|
||||||
|
famille_obj, "FA_Intitule", ""
|
||||||
|
).strip()
|
||||||
|
else:
|
||||||
|
data["famille_libelle"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["famille_libelle"] = ""
|
||||||
|
else:
|
||||||
|
data["famille_libelle"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["famille_code"] = ""
|
||||||
|
data["famille_libelle"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
fournisseur_code = getattr(article_obj, "CT_Num", "").strip()
|
||||||
|
data["fournisseur_principal"] = fournisseur_code
|
||||||
|
|
||||||
|
if fournisseur_code:
|
||||||
|
try:
|
||||||
|
fourn_obj = getattr(article_obj, "Fournisseur", None)
|
||||||
|
if fourn_obj:
|
||||||
|
fourn_obj.Read()
|
||||||
|
data["fournisseur_nom"] = getattr(
|
||||||
|
fourn_obj, "CT_Intitule", ""
|
||||||
|
).strip()
|
||||||
|
else:
|
||||||
|
data["fournisseur_nom"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["fournisseur_nom"] = ""
|
||||||
|
else:
|
||||||
|
data["fournisseur_nom"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["fournisseur_principal"] = ""
|
||||||
|
data["fournisseur_nom"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["unite_vente"] = getattr(article_obj, "AR_UniteVen", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["unite_vente"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["unite_achat"] = getattr(article_obj, "AR_UniteAch", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["unite_achat"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["poids"] = float(getattr(article_obj, "AR_Poids", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["poids"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["volume"] = float(getattr(article_obj, "AR_Volume", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["volume"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
sommeil = getattr(article_obj, "AR_Sommeil", 0)
|
||||||
|
data["est_actif"] = sommeil == 0
|
||||||
|
data["en_sommeil"] = sommeil == 1
|
||||||
|
except Exception:
|
||||||
|
data["est_actif"] = True
|
||||||
|
data["en_sommeil"] = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
tva_code = getattr(article_obj, "TA_Code", "").strip()
|
||||||
|
data["tva_code"] = tva_code
|
||||||
|
|
||||||
|
try:
|
||||||
|
tva_obj = getattr(article_obj, "Taxe1", None)
|
||||||
|
if tva_obj:
|
||||||
|
tva_obj.Read()
|
||||||
|
data["tva_taux"] = float(getattr(tva_obj, "TA_Taux", 20.0))
|
||||||
|
else:
|
||||||
|
data["tva_taux"] = 20.0
|
||||||
|
except Exception:
|
||||||
|
data["tva_taux"] = 20.0
|
||||||
|
except Exception:
|
||||||
|
data["tva_code"] = ""
|
||||||
|
data["tva_taux"] = 20.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
date_creation = getattr(article_obj, "AR_DateCreate", None)
|
||||||
|
data["date_creation"] = str(date_creation) if date_creation else ""
|
||||||
|
except Exception:
|
||||||
|
data["date_creation"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
date_modif = getattr(article_obj, "AR_DateModif", None)
|
||||||
|
data["date_modification"] = str(date_modif) if date_modif else ""
|
||||||
|
except Exception:
|
||||||
|
data["date_modification"] = ""
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" Erreur extraction article: {e}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"reference": getattr(article_obj, "AR_Ref", "").strip(),
|
||||||
|
"designation": getattr(article_obj, "AR_Design", "").strip(),
|
||||||
|
"prix_vente": 0.0,
|
||||||
|
"stock_reel": 0.0,
|
||||||
|
"code_ean": "",
|
||||||
|
"description": "",
|
||||||
|
"designation_complementaire": "",
|
||||||
|
"prix_achat": 0.0,
|
||||||
|
"prix_revient": 0.0,
|
||||||
|
"stock_mini": 0.0,
|
||||||
|
"stock_maxi": 0.0,
|
||||||
|
"stock_reserve": 0.0,
|
||||||
|
"stock_commande": 0.0,
|
||||||
|
"stock_disponible": 0.0,
|
||||||
|
"code_barre": "",
|
||||||
|
"type_article": 0,
|
||||||
|
"type_article_libelle": "Article",
|
||||||
|
"famille_code": "",
|
||||||
|
"famille_libelle": "",
|
||||||
|
"fournisseur_principal": "",
|
||||||
|
"fournisseur_nom": "",
|
||||||
|
"unite_vente": "",
|
||||||
|
"unite_achat": "",
|
||||||
|
"poids": 0.0,
|
||||||
|
"volume": 0.0,
|
||||||
|
"est_actif": True,
|
||||||
|
"en_sommeil": False,
|
||||||
|
"tva_code": "",
|
||||||
|
"tva_taux": 20.0,
|
||||||
|
"date_creation": "",
|
||||||
|
"date_modification": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_extraire_article",
|
||||||
|
]
|
||||||
1497
utils/articles/articles_data_sql.py
Normal file
1497
utils/articles/articles_data_sql.py
Normal file
File diff suppressed because it is too large
Load diff
41
utils/articles/stock_check.py
Normal file
41
utils/articles/stock_check.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def verifier_stock_suffisant(article_ref, quantite, cursor, depot=None):
|
||||||
|
"""Version thread-safe avec lock SQL"""
|
||||||
|
try:
|
||||||
|
cursor.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
|
||||||
|
cursor.execute("BEGIN TRANSACTION")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT SUM(AS_QteSto)
|
||||||
|
FROM F_ARTSTOCK WITH (UPDLOCK, ROWLOCK)
|
||||||
|
WHERE AR_Ref = ?
|
||||||
|
""",
|
||||||
|
(article_ref.upper(),),
|
||||||
|
)
|
||||||
|
|
||||||
|
row = cursor.fetchone()
|
||||||
|
stock_dispo = float(row[0]) if row and row[0] else 0.0
|
||||||
|
|
||||||
|
suffisant = stock_dispo >= quantite
|
||||||
|
|
||||||
|
cursor.execute("COMMIT")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"suffisant": suffisant,
|
||||||
|
"stock_disponible": stock_dispo,
|
||||||
|
"quantite_demandee": quantite,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
cursor.execute("ROLLBACK")
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur vérification stock: {e}")
|
||||||
|
raise
|
||||||
0
utils/documents/__init__.py
Normal file
0
utils/documents/__init__.py
Normal file
61
utils/documents/devis/devis_check.py
Normal file
61
utils/documents/devis/devis_check.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import win32com.client
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _rechercher_devis_dans_liste(numero_devis, factory_doc):
|
||||||
|
"""Recherche un devis dans les 100 premiers éléments de la liste."""
|
||||||
|
index = 1
|
||||||
|
while index < 100:
|
||||||
|
try:
|
||||||
|
persist_test = factory_doc.List(index)
|
||||||
|
if persist_test is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3")
|
||||||
|
doc_test.Read()
|
||||||
|
|
||||||
|
if (
|
||||||
|
getattr(doc_test, "DO_Type", -1) == 0
|
||||||
|
and getattr(doc_test, "DO_Piece", "") == numero_devis
|
||||||
|
):
|
||||||
|
logger.info(f" Document trouvé à l'index {index}")
|
||||||
|
return persist_test
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
except Exception:
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _recuperer_numero_devis(process, doc):
|
||||||
|
"""Récupère le numéro du devis créé via plusieurs méthodes."""
|
||||||
|
numero_devis = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc_result = process.DocumentResult
|
||||||
|
if doc_result:
|
||||||
|
doc_result = win32com.client.CastTo(doc_result, "IBODocumentVente3")
|
||||||
|
doc_result.Read()
|
||||||
|
numero_devis = getattr(doc_result, "DO_Piece", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not numero_devis:
|
||||||
|
numero_devis = getattr(doc, "DO_Piece", "")
|
||||||
|
|
||||||
|
if not numero_devis:
|
||||||
|
try:
|
||||||
|
doc.SetDefaultNumPiece()
|
||||||
|
doc.Write()
|
||||||
|
doc.Read()
|
||||||
|
numero_devis = getattr(doc, "DO_Piece", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return numero_devis
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["_recuperer_numero_devis", "_rechercher_devis_dans_liste"]
|
||||||
51
utils/documents/devis/devis_extraction.py
Normal file
51
utils/documents/devis/devis_extraction.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
def _extraire_infos_devis(doc, numero: str, champs_modifies: list) -> Dict:
|
||||||
|
"""Extrait les informations complètes du devis."""
|
||||||
|
total_ht = float(getattr(doc, "DO_TotalHT", 0.0))
|
||||||
|
total_ttc = float(getattr(doc, "DO_TotalTTC", 0.0))
|
||||||
|
statut = getattr(doc, "DO_Statut", 0)
|
||||||
|
reference = getattr(doc, "DO_Ref", "")
|
||||||
|
|
||||||
|
date_devis = None
|
||||||
|
try:
|
||||||
|
date_doc = getattr(doc, "DO_Date", None)
|
||||||
|
if date_doc:
|
||||||
|
date_devis = date_doc.strftime("%Y-%m-%d")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
date_livraison = None
|
||||||
|
try:
|
||||||
|
date_livr = getattr(doc, "DO_DateLivr", None)
|
||||||
|
if date_livr:
|
||||||
|
date_livraison = date_livr.strftime("%Y-%m-%d")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
client_code = ""
|
||||||
|
try:
|
||||||
|
client_obj = getattr(doc, "Client", None)
|
||||||
|
if client_obj:
|
||||||
|
client_obj.Read()
|
||||||
|
client_code = getattr(client_obj, "CT_Num", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"numero": numero,
|
||||||
|
"total_ht": total_ht,
|
||||||
|
"total_ttc": total_ttc,
|
||||||
|
"reference": reference,
|
||||||
|
"date_devis": date_devis,
|
||||||
|
"date_livraison": date_livraison,
|
||||||
|
"champs_modifies": champs_modifies,
|
||||||
|
"statut": statut,
|
||||||
|
"client_code": client_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_extraire_infos_devis",
|
||||||
|
]
|
||||||
793
utils/documents/documents_data_sql.py
Normal file
793
utils/documents/documents_data_sql.py
Normal file
|
|
@ -0,0 +1,793 @@
|
||||||
|
import win32com.client
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from utils.functions.functions import (
|
||||||
|
_convertir_type_depuis_sql,
|
||||||
|
_convertir_type_pour_sql,
|
||||||
|
_safe_strip,
|
||||||
|
_combiner_date_heure,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _afficher_etat_document(doc, titre: str):
|
||||||
|
"""Affiche l'état complet d'un document."""
|
||||||
|
logger.info("-" * 80)
|
||||||
|
logger.info(titre)
|
||||||
|
logger.info("-" * 80)
|
||||||
|
try:
|
||||||
|
logger.info(f" DO_Piece: {getattr(doc, 'DO_Piece', 'N/A')}")
|
||||||
|
logger.info(f" DO_Ref: '{getattr(doc, 'DO_Ref', 'N/A')}'")
|
||||||
|
logger.info(f" DO_Statut: {getattr(doc, 'DO_Statut', 'N/A')}")
|
||||||
|
|
||||||
|
date_doc = getattr(doc, "DO_Date", None)
|
||||||
|
date_str = date_doc.strftime("%Y-%m-%d") if date_doc else "None"
|
||||||
|
logger.info(f" DO_Date: {date_str}")
|
||||||
|
|
||||||
|
date_livr = getattr(doc, "DO_DateLivr", None)
|
||||||
|
date_livr_str = date_livr.strftime("%Y-%m-%d") if date_livr else "None"
|
||||||
|
logger.info(f" DO_DateLivr: {date_livr_str}")
|
||||||
|
|
||||||
|
logger.info(f" DO_TotalHT: {getattr(doc, 'DO_TotalHT', 0)}€")
|
||||||
|
logger.info(f" DO_TotalTTC: {getattr(doc, 'DO_TotalTTC', 0)}€")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" Erreur affichage état: {e}")
|
||||||
|
logger.info("-" * 80)
|
||||||
|
|
||||||
|
|
||||||
|
def _compter_lignes_document(doc) -> int:
|
||||||
|
"""Compte les lignes d'un document."""
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
factory_lignes = doc.FactoryDocumentLigne
|
||||||
|
except Exception:
|
||||||
|
factory_lignes = doc.FactoryDocumentVenteLigne
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
index = 1
|
||||||
|
while index <= 100:
|
||||||
|
try:
|
||||||
|
ligne_p = factory_lignes.List(index)
|
||||||
|
if ligne_p is None:
|
||||||
|
break
|
||||||
|
count += 1
|
||||||
|
index += 1
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
return count
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" Erreur comptage lignes: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _rechercher_devis_par_numero(numero: str, factory):
|
||||||
|
"""Recherche un devis par numéro dans la liste."""
|
||||||
|
logger.info(f" Recherche de {numero} dans la liste...")
|
||||||
|
|
||||||
|
index = 1
|
||||||
|
while index < 10000:
|
||||||
|
try:
|
||||||
|
persist_test = factory.List(index)
|
||||||
|
if persist_test is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
doc_test = win32com.client.CastTo(persist_test, "IBODocumentVente3")
|
||||||
|
doc_test.Read()
|
||||||
|
|
||||||
|
if (
|
||||||
|
getattr(doc_test, "DO_Type", -1) == 0
|
||||||
|
and getattr(doc_test, "DO_Piece", "") == numero
|
||||||
|
):
|
||||||
|
logger.info(f" Trouvé à l'index {index}")
|
||||||
|
return persist_test
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
except Exception:
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
logger.error(f" Devis {numero} non trouvé dans la liste")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _lire_document_sql(cursor, numero: str, type_doc: int):
|
||||||
|
try:
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
d.DO_Piece, d.DO_Date, d.DO_Ref, d.DO_TotalHT, d.DO_TotalTTC,
|
||||||
|
d.DO_Statut, d.DO_Tiers, d.DO_DateLivr, d.DO_DateExpedition,
|
||||||
|
d.DO_Contact, d.DO_TotalHTNet, d.DO_NetAPayer,
|
||||||
|
d.DO_MontantRegle, d.DO_Reliquat, d.DO_TxEscompte, d.DO_Escompte,
|
||||||
|
d.DO_Taxe1, d.DO_Taxe2, d.DO_Taxe3,
|
||||||
|
d.DO_CodeTaxe1, d.DO_CodeTaxe2, d.DO_CodeTaxe3,
|
||||||
|
d.DO_EStatut, d.DO_Imprim, d.DO_Valide, d.DO_Cloture,
|
||||||
|
d.DO_Transfere, d.DO_Souche, d.DO_PieceOrig, d.DO_GUID,
|
||||||
|
d.CA_Num, d.CG_Num, d.DO_Expedit, d.DO_Condition,
|
||||||
|
d.DO_Tarif, d.DO_TypeFrais, d.DO_ValFrais,
|
||||||
|
d.DO_TypeFranco, d.DO_ValFranco,
|
||||||
|
c.CT_Intitule, c.CT_Adresse, c.CT_CodePostal,
|
||||||
|
c.CT_Ville, c.CT_Telephone, c.CT_EMail,
|
||||||
|
d.DO_Heure
|
||||||
|
FROM F_DOCENTETE d
|
||||||
|
LEFT JOIN F_COMPTET c ON d.DO_Tiers = c.CT_Num
|
||||||
|
WHERE d.DO_Piece = ? AND d.DO_Type = ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"[SQL READ] Lecture directe de {numero} (type={type_doc})")
|
||||||
|
|
||||||
|
cursor.execute(query, (numero, type_doc))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
logger.warning(
|
||||||
|
f"[SQL READ] Document {numero} (type={type_doc}) INTROUVABLE"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
numero_piece = _safe_strip(row[0])
|
||||||
|
logger.info(f"[SQL READ] Document trouvé: {numero_piece}")
|
||||||
|
|
||||||
|
doc = {
|
||||||
|
"numero": numero_piece,
|
||||||
|
"reference": _safe_strip(row[2]),
|
||||||
|
"date": _combiner_date_heure(row[1], row[45]),
|
||||||
|
"date_livraison": str(row[7]) if row[7] else "",
|
||||||
|
"date_expedition": str(row[8]) if row[8] else "",
|
||||||
|
"client_code": _safe_strip(row[6]),
|
||||||
|
"client_intitule": _safe_strip(row[39]),
|
||||||
|
"client_adresse": _safe_strip(row[40]),
|
||||||
|
"client_code_postal": _safe_strip(row[41]),
|
||||||
|
"client_ville": _safe_strip(row[42]),
|
||||||
|
"client_telephone": _safe_strip(row[43]),
|
||||||
|
"client_email": _safe_strip(row[44]),
|
||||||
|
"contact": _safe_strip(row[9]),
|
||||||
|
"total_ht": float(row[3]) if row[3] else 0.0,
|
||||||
|
"total_ht_net": float(row[10]) if row[10] else 0.0,
|
||||||
|
"total_ttc": float(row[4]) if row[4] else 0.0,
|
||||||
|
"net_a_payer": float(row[11]) if row[11] else 0.0,
|
||||||
|
"montant_regle": float(row[12]) if row[12] else 0.0,
|
||||||
|
"reliquat": float(row[13]) if row[13] else 0.0,
|
||||||
|
"taux_escompte": float(row[14]) if row[14] else 0.0,
|
||||||
|
"escompte": float(row[15]) if row[15] else 0.0,
|
||||||
|
"taxe1": float(row[16]) if row[16] else 0.0,
|
||||||
|
"taxe2": float(row[17]) if row[17] else 0.0,
|
||||||
|
"taxe3": float(row[18]) if row[18] else 0.0,
|
||||||
|
"code_taxe1": _safe_strip(row[19]),
|
||||||
|
"code_taxe2": _safe_strip(row[20]),
|
||||||
|
"code_taxe3": _safe_strip(row[21]),
|
||||||
|
"statut": int(row[5]) if row[5] is not None else 0,
|
||||||
|
"statut_estatut": int(row[22]) if row[22] is not None else 0,
|
||||||
|
"imprime": int(row[23]) if row[23] is not None else 0,
|
||||||
|
"valide": int(row[24]) if row[24] is not None else 0,
|
||||||
|
"cloture": int(row[25]) if row[25] is not None else 0,
|
||||||
|
"transfere": int(row[26]) if row[26] is not None else 0,
|
||||||
|
"souche": int(row[27]) if row[27] is not None else 0,
|
||||||
|
"piece_origine": _safe_strip(row[28]),
|
||||||
|
"guid": _safe_strip(row[29]),
|
||||||
|
"ca_num": _safe_strip(row[30]),
|
||||||
|
"cg_num": _safe_strip(row[31]),
|
||||||
|
"expedition": int(row[32]) if row[32] is not None else 1,
|
||||||
|
"condition": int(row[33]) if row[33] is not None else 1,
|
||||||
|
"tarif": int(row[34]) if row[34] is not None else 1,
|
||||||
|
"type_frais": int(row[35]) if row[35] is not None else 0,
|
||||||
|
"valeur_frais": float(row[36]) if row[36] else 0.0,
|
||||||
|
"type_franco": int(row[37]) if row[37] is not None else 0,
|
||||||
|
"valeur_franco": float(row[38]) if row[38] else 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
dl.*,
|
||||||
|
a.AR_Design, a.FA_CodeFamille, a.AR_PrixTTC, a.AR_PrixVen, a.AR_PrixAch,
|
||||||
|
a.AR_Gamme1, a.AR_Gamme2, a.AR_CodeBarre, a.AR_CoutStd,
|
||||||
|
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_UniteVen,
|
||||||
|
a.AR_Type, a.AR_Nature, a.AR_Escompte, a.AR_Garantie
|
||||||
|
FROM F_DOCLIGNE dl
|
||||||
|
LEFT JOIN F_ARTICLE a ON dl.AR_Ref = a.AR_Ref
|
||||||
|
WHERE dl.DO_Piece = ? AND dl.DO_Type = ?
|
||||||
|
ORDER BY dl.DL_Ligne
|
||||||
|
""",
|
||||||
|
(numero, type_doc),
|
||||||
|
)
|
||||||
|
|
||||||
|
lignes = []
|
||||||
|
for ligne_row in cursor.fetchall():
|
||||||
|
montant_ht = (
|
||||||
|
float(ligne_row.DL_MontantHT) if ligne_row.DL_MontantHT else 0.0
|
||||||
|
)
|
||||||
|
montant_net = (
|
||||||
|
float(ligne_row.DL_MontantNet)
|
||||||
|
if hasattr(ligne_row, "DL_MontantNet") and ligne_row.DL_MontantNet
|
||||||
|
else montant_ht
|
||||||
|
)
|
||||||
|
|
||||||
|
taux_taxe1 = (
|
||||||
|
float(ligne_row.DL_Taxe1)
|
||||||
|
if hasattr(ligne_row, "DL_Taxe1") and ligne_row.DL_Taxe1
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
taux_taxe2 = (
|
||||||
|
float(ligne_row.DL_Taxe2)
|
||||||
|
if hasattr(ligne_row, "DL_Taxe2") and ligne_row.DL_Taxe2
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
taux_taxe3 = (
|
||||||
|
float(ligne_row.DL_Taxe3)
|
||||||
|
if hasattr(ligne_row, "DL_Taxe3") and ligne_row.DL_Taxe3
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
total_taux_taxes = taux_taxe1 + taux_taxe2 + taux_taxe3
|
||||||
|
montant_ttc = montant_net * (1 + total_taux_taxes / 100)
|
||||||
|
|
||||||
|
montant_taxe1 = montant_net * (taux_taxe1 / 100)
|
||||||
|
montant_taxe2 = montant_net * (taux_taxe2 / 100)
|
||||||
|
montant_taxe3 = montant_net * (taux_taxe3 / 100)
|
||||||
|
|
||||||
|
ligne = {
|
||||||
|
"numero_ligne": (int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0),
|
||||||
|
"article_code": _safe_strip(ligne_row.AR_Ref),
|
||||||
|
"designation": _safe_strip(ligne_row.DL_Design),
|
||||||
|
"designation_article": _safe_strip(ligne_row.AR_Design),
|
||||||
|
"quantite": (float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0),
|
||||||
|
"quantite_livree": (
|
||||||
|
float(ligne_row.DL_QteLiv)
|
||||||
|
if hasattr(ligne_row, "DL_QteLiv") and ligne_row.DL_QteLiv
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"quantite_reservee": (
|
||||||
|
float(ligne_row.DL_QteRes)
|
||||||
|
if hasattr(ligne_row, "DL_QteRes") and ligne_row.DL_QteRes
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"unite": (
|
||||||
|
_safe_strip(ligne_row.DL_Unite)
|
||||||
|
if hasattr(ligne_row, "DL_Unite")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"prix_unitaire_ht": (
|
||||||
|
float(ligne_row.DL_PrixUnitaire)
|
||||||
|
if ligne_row.DL_PrixUnitaire
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"prix_unitaire_achat": (
|
||||||
|
float(ligne_row.AR_PrixAch) if ligne_row.AR_PrixAch else 0.0
|
||||||
|
),
|
||||||
|
"prix_unitaire_vente": (
|
||||||
|
float(ligne_row.AR_PrixVen) if ligne_row.AR_PrixVen else 0.0
|
||||||
|
),
|
||||||
|
"prix_unitaire_ttc": (
|
||||||
|
float(ligne_row.AR_PrixTTC) if ligne_row.AR_PrixTTC else 0.0
|
||||||
|
),
|
||||||
|
"montant_ligne_ht": montant_ht,
|
||||||
|
"montant_ligne_net": montant_net,
|
||||||
|
"montant_ligne_ttc": montant_ttc,
|
||||||
|
"remise_valeur1": (
|
||||||
|
float(ligne_row.DL_Remise01REM_Valeur)
|
||||||
|
if hasattr(ligne_row, "DL_Remise01REM_Valeur")
|
||||||
|
and ligne_row.DL_Remise01REM_Valeur
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"remise_type1": (
|
||||||
|
int(ligne_row.DL_Remise01REM_Type)
|
||||||
|
if hasattr(ligne_row, "DL_Remise01REM_Type")
|
||||||
|
and ligne_row.DL_Remise01REM_Type
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"remise_valeur2": (
|
||||||
|
float(ligne_row.DL_Remise02REM_Valeur)
|
||||||
|
if hasattr(ligne_row, "DL_Remise02REM_Valeur")
|
||||||
|
and ligne_row.DL_Remise02REM_Valeur
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"remise_type2": (
|
||||||
|
int(ligne_row.DL_Remise02REM_Type)
|
||||||
|
if hasattr(ligne_row, "DL_Remise02REM_Type")
|
||||||
|
and ligne_row.DL_Remise02REM_Type
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"remise_article": (
|
||||||
|
float(ligne_row.AR_Escompte) if ligne_row.AR_Escompte else 0.0
|
||||||
|
),
|
||||||
|
"taux_taxe1": taux_taxe1,
|
||||||
|
"montant_taxe1": montant_taxe1,
|
||||||
|
"taux_taxe2": taux_taxe2,
|
||||||
|
"montant_taxe2": montant_taxe2,
|
||||||
|
"taux_taxe3": taux_taxe3,
|
||||||
|
"montant_taxe3": montant_taxe3,
|
||||||
|
"total_taxes": montant_taxe1 + montant_taxe2 + montant_taxe3,
|
||||||
|
"famille_article": _safe_strip(ligne_row.FA_CodeFamille),
|
||||||
|
"gamme1": _safe_strip(ligne_row.AR_Gamme1),
|
||||||
|
"gamme2": _safe_strip(ligne_row.AR_Gamme2),
|
||||||
|
"code_barre": _safe_strip(ligne_row.AR_CodeBarre),
|
||||||
|
"type_article": _safe_strip(ligne_row.AR_Type),
|
||||||
|
"nature_article": _safe_strip(ligne_row.AR_Nature),
|
||||||
|
"garantie": _safe_strip(ligne_row.AR_Garantie),
|
||||||
|
"cout_standard": (
|
||||||
|
float(ligne_row.AR_CoutStd) if ligne_row.AR_CoutStd else 0.0
|
||||||
|
),
|
||||||
|
"poids_net": (
|
||||||
|
float(ligne_row.AR_PoidsNet) if ligne_row.AR_PoidsNet else 0.0
|
||||||
|
),
|
||||||
|
"poids_brut": (
|
||||||
|
float(ligne_row.AR_PoidsBrut) if ligne_row.AR_PoidsBrut else 0.0
|
||||||
|
),
|
||||||
|
"unite_vente": _safe_strip(ligne_row.AR_UniteVen),
|
||||||
|
"date_livraison_ligne": (
|
||||||
|
str(ligne_row.DL_DateLivr)
|
||||||
|
if hasattr(ligne_row, "DL_DateLivr") and ligne_row.DL_DateLivr
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"statut_ligne": (
|
||||||
|
int(ligne_row.DL_Statut)
|
||||||
|
if hasattr(ligne_row, "DL_Statut")
|
||||||
|
and ligne_row.DL_Statut is not None
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"depot": (
|
||||||
|
_safe_strip(ligne_row.DE_No) if hasattr(ligne_row, "DE_No") else ""
|
||||||
|
),
|
||||||
|
"numero_commande": (
|
||||||
|
_safe_strip(ligne_row.DL_NoColis)
|
||||||
|
if hasattr(ligne_row, "DL_NoColis")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"num_colis": (
|
||||||
|
_safe_strip(ligne_row.DL_Colis)
|
||||||
|
if hasattr(ligne_row, "DL_Colis")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
lignes.append(ligne)
|
||||||
|
|
||||||
|
doc["lignes"] = lignes
|
||||||
|
doc["nb_lignes"] = len(lignes)
|
||||||
|
|
||||||
|
doc["total_ht_calcule"] = doc["total_ht_net"]
|
||||||
|
doc["total_ttc_calcule"] = doc["total_ttc"]
|
||||||
|
doc["total_taxes_calcule"] = doc["total_ttc"] - doc["total_ht_net"]
|
||||||
|
|
||||||
|
return doc
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" Erreur SQL lecture document {numero}: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _lister_documents_avec_lignes_sql(
|
||||||
|
cursor,
|
||||||
|
type_doc: int,
|
||||||
|
filtre: str = "",
|
||||||
|
limit: int = None,
|
||||||
|
):
|
||||||
|
"""Liste les documents avec leurs lignes."""
|
||||||
|
try:
|
||||||
|
type_doc_sql = _convertir_type_pour_sql(type_doc)
|
||||||
|
logger.info(f"[SQL LIST] ═══ Type COM {type_doc} → SQL {type_doc_sql} ═══")
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT DISTINCT
|
||||||
|
d.DO_Piece, d.DO_Type, d.DO_Date, d.DO_Ref, d.DO_Tiers,
|
||||||
|
d.DO_TotalHT, d.DO_TotalTTC, d.DO_NetAPayer, d.DO_Statut,
|
||||||
|
d.DO_DateLivr, d.DO_DateExpedition, d.DO_Contact, d.DO_TotalHTNet,
|
||||||
|
d.DO_MontantRegle, d.DO_Reliquat, d.DO_TxEscompte, d.DO_Escompte,
|
||||||
|
d.DO_Taxe1, d.DO_Taxe2, d.DO_Taxe3,
|
||||||
|
d.DO_CodeTaxe1, d.DO_CodeTaxe2, d.DO_CodeTaxe3,
|
||||||
|
d.DO_EStatut, d.DO_Imprim, d.DO_Valide, d.DO_Cloture, d.DO_Transfere,
|
||||||
|
d.DO_Souche, d.DO_PieceOrig, d.DO_GUID,
|
||||||
|
d.CA_Num, d.CG_Num, d.DO_Expedit, d.DO_Condition, d.DO_Tarif,
|
||||||
|
d.DO_TypeFrais, d.DO_ValFrais, d.DO_TypeFranco, d.DO_ValFranco,
|
||||||
|
c.CT_Intitule, c.CT_Adresse, c.CT_CodePostal,
|
||||||
|
c.CT_Ville, c.CT_Telephone, c.CT_EMail,
|
||||||
|
d.DO_Heure
|
||||||
|
FROM F_DOCENTETE d
|
||||||
|
LEFT JOIN F_COMPTET c ON d.DO_Tiers = c.CT_Num
|
||||||
|
WHERE d.DO_Type = ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = [type_doc_sql]
|
||||||
|
|
||||||
|
if filtre:
|
||||||
|
query += (
|
||||||
|
" AND (d.DO_Piece LIKE ? OR c.CT_Intitule LIKE ? OR d.DO_Ref LIKE ?)"
|
||||||
|
)
|
||||||
|
params.extend([f"%{filtre}%", f"%{filtre}%", f"%{filtre}%"])
|
||||||
|
|
||||||
|
query += " ORDER BY d.DO_Date DESC, d.DO_Heure DESC"
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
query = f"SELECT TOP ({limit}) * FROM ({query}) AS subquery"
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
entetes = cursor.fetchall()
|
||||||
|
|
||||||
|
logger.info(f"[SQL LIST] {len(entetes)} documents SQL")
|
||||||
|
|
||||||
|
documents = []
|
||||||
|
stats = {
|
||||||
|
"total": len(entetes),
|
||||||
|
"exclus_prefixe": 0,
|
||||||
|
"erreur_construction": 0,
|
||||||
|
"erreur_lignes": 0,
|
||||||
|
"erreur_transformations": 0,
|
||||||
|
"erreur_liaisons": 0,
|
||||||
|
"succes": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, entete in enumerate(entetes):
|
||||||
|
numero = _safe_strip(entete.DO_Piece)
|
||||||
|
|
||||||
|
try:
|
||||||
|
prefixes_vente = {
|
||||||
|
0: ["DE"],
|
||||||
|
10: ["BC"],
|
||||||
|
30: ["BL"],
|
||||||
|
50: ["AV", "AR"],
|
||||||
|
60: ["FA", "FC"],
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixes_acceptes = prefixes_vente.get(type_doc, [])
|
||||||
|
|
||||||
|
if prefixes_acceptes:
|
||||||
|
est_vente = any(
|
||||||
|
numero.upper().startswith(p) for p in prefixes_acceptes
|
||||||
|
)
|
||||||
|
if not est_vente:
|
||||||
|
logger.info(f"[SQL LIST] {numero} : exclu (préfixe achat)")
|
||||||
|
stats["exclus_prefixe"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.debug(f"[SQL LIST] {numero} : préfixe OK")
|
||||||
|
|
||||||
|
try:
|
||||||
|
type_doc_depuis_sql = _convertir_type_depuis_sql(
|
||||||
|
int(entete.DO_Type)
|
||||||
|
)
|
||||||
|
|
||||||
|
doc = {
|
||||||
|
"numero": numero,
|
||||||
|
"type": type_doc_depuis_sql,
|
||||||
|
"reference": _safe_strip(entete.DO_Ref),
|
||||||
|
"date": _combiner_date_heure(entete.DO_Date, entete.DO_Heure),
|
||||||
|
"date_livraison": str(entete.DO_DateLivr)
|
||||||
|
if entete.DO_DateLivr
|
||||||
|
else "",
|
||||||
|
"date_expedition": str(entete.DO_DateExpedition)
|
||||||
|
if entete.DO_DateExpedition
|
||||||
|
else "",
|
||||||
|
"client_code": _safe_strip(entete.DO_Tiers),
|
||||||
|
"client_intitule": _safe_strip(entete.CT_Intitule),
|
||||||
|
"client_adresse": _safe_strip(entete.CT_Adresse),
|
||||||
|
"client_code_postal": _safe_strip(entete.CT_CodePostal),
|
||||||
|
"client_ville": _safe_strip(entete.CT_Ville),
|
||||||
|
"client_telephone": _safe_strip(entete.CT_Telephone),
|
||||||
|
"client_email": _safe_strip(entete.CT_EMail),
|
||||||
|
"contact": _safe_strip(entete.DO_Contact),
|
||||||
|
"total_ht": (
|
||||||
|
float(entete.DO_TotalHT) if entete.DO_TotalHT else 0.0
|
||||||
|
),
|
||||||
|
"total_ht_net": (
|
||||||
|
float(entete.DO_TotalHTNet) if entete.DO_TotalHTNet else 0.0
|
||||||
|
),
|
||||||
|
"total_ttc": (
|
||||||
|
float(entete.DO_TotalTTC) if entete.DO_TotalTTC else 0.0
|
||||||
|
),
|
||||||
|
"net_a_payer": (
|
||||||
|
float(entete.DO_NetAPayer) if entete.DO_NetAPayer else 0.0
|
||||||
|
),
|
||||||
|
"montant_regle": (
|
||||||
|
float(entete.DO_MontantRegle)
|
||||||
|
if entete.DO_MontantRegle
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"reliquat": (
|
||||||
|
float(entete.DO_Reliquat) if entete.DO_Reliquat else 0.0
|
||||||
|
),
|
||||||
|
"taux_escompte": (
|
||||||
|
float(entete.DO_TxEscompte) if entete.DO_TxEscompte else 0.0
|
||||||
|
),
|
||||||
|
"escompte": (
|
||||||
|
float(entete.DO_Escompte) if entete.DO_Escompte else 0.0
|
||||||
|
),
|
||||||
|
"taxe1": (float(entete.DO_Taxe1) if entete.DO_Taxe1 else 0.0),
|
||||||
|
"taxe2": (float(entete.DO_Taxe2) if entete.DO_Taxe2 else 0.0),
|
||||||
|
"taxe3": (float(entete.DO_Taxe3) if entete.DO_Taxe3 else 0.0),
|
||||||
|
"code_taxe1": _safe_strip(entete.DO_CodeTaxe1),
|
||||||
|
"code_taxe2": _safe_strip(entete.DO_CodeTaxe2),
|
||||||
|
"code_taxe3": _safe_strip(entete.DO_CodeTaxe3),
|
||||||
|
"statut": (
|
||||||
|
int(entete.DO_Statut) if entete.DO_Statut is not None else 0
|
||||||
|
),
|
||||||
|
"statut_estatut": (
|
||||||
|
int(entete.DO_EStatut)
|
||||||
|
if entete.DO_EStatut is not None
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"imprime": (
|
||||||
|
int(entete.DO_Imprim) if entete.DO_Imprim is not None else 0
|
||||||
|
),
|
||||||
|
"valide": (
|
||||||
|
int(entete.DO_Valide) if entete.DO_Valide is not None else 0
|
||||||
|
),
|
||||||
|
"cloture": (
|
||||||
|
int(entete.DO_Cloture)
|
||||||
|
if entete.DO_Cloture is not None
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"transfere": (
|
||||||
|
int(entete.DO_Transfere)
|
||||||
|
if entete.DO_Transfere is not None
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"souche": _safe_strip(entete.DO_Souche),
|
||||||
|
"piece_origine": _safe_strip(entete.DO_PieceOrig),
|
||||||
|
"guid": _safe_strip(entete.DO_GUID),
|
||||||
|
"ca_num": _safe_strip(entete.CA_Num),
|
||||||
|
"cg_num": _safe_strip(entete.CG_Num),
|
||||||
|
"expedition": _safe_strip(entete.DO_Expedit),
|
||||||
|
"condition": _safe_strip(entete.DO_Condition),
|
||||||
|
"tarif": _safe_strip(entete.DO_Tarif),
|
||||||
|
"type_frais": (
|
||||||
|
int(entete.DO_TypeFrais)
|
||||||
|
if entete.DO_TypeFrais is not None
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"valeur_frais": (
|
||||||
|
float(entete.DO_ValFrais) if entete.DO_ValFrais else 0.0
|
||||||
|
),
|
||||||
|
"type_franco": (
|
||||||
|
int(entete.DO_TypeFranco)
|
||||||
|
if entete.DO_TypeFranco is not None
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"valeur_franco": (
|
||||||
|
float(entete.DO_ValFranco) if entete.DO_ValFranco else 0.0
|
||||||
|
),
|
||||||
|
"lignes": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(f"[SQL LIST] {numero} : document de base créé")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"[SQL LIST] {numero} : ERREUR construction base: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
stats["erreur_construction"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT dl.*,
|
||||||
|
a.AR_Design, a.FA_CodeFamille, a.AR_PrixTTC, a.AR_PrixVen, a.AR_PrixAch,
|
||||||
|
a.AR_Gamme1, a.AR_Gamme2, a.AR_CodeBarre, a.AR_CoutStd,
|
||||||
|
a.AR_PoidsNet, a.AR_PoidsBrut, a.AR_UniteVen,
|
||||||
|
a.AR_Type, a.AR_Nature, a.AR_Escompte, a.AR_Garantie
|
||||||
|
FROM F_DOCLIGNE dl
|
||||||
|
LEFT JOIN F_ARTICLE a ON dl.AR_Ref = a.AR_Ref
|
||||||
|
WHERE dl.DO_Piece = ? AND dl.DO_Type = ?
|
||||||
|
ORDER BY dl.DL_Ligne
|
||||||
|
""",
|
||||||
|
(numero, type_doc_sql),
|
||||||
|
)
|
||||||
|
|
||||||
|
for ligne_row in cursor.fetchall():
|
||||||
|
montant_ht = (
|
||||||
|
float(ligne_row.DL_MontantHT)
|
||||||
|
if ligne_row.DL_MontantHT
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
montant_net = (
|
||||||
|
float(ligne_row.DL_MontantNet)
|
||||||
|
if hasattr(ligne_row, "DL_MontantNet")
|
||||||
|
and ligne_row.DL_MontantNet
|
||||||
|
else montant_ht
|
||||||
|
)
|
||||||
|
|
||||||
|
taux_taxe1 = (
|
||||||
|
float(ligne_row.DL_Taxe1)
|
||||||
|
if hasattr(ligne_row, "DL_Taxe1") and ligne_row.DL_Taxe1
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
taux_taxe2 = (
|
||||||
|
float(ligne_row.DL_Taxe2)
|
||||||
|
if hasattr(ligne_row, "DL_Taxe2") and ligne_row.DL_Taxe2
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
taux_taxe3 = (
|
||||||
|
float(ligne_row.DL_Taxe3)
|
||||||
|
if hasattr(ligne_row, "DL_Taxe3") and ligne_row.DL_Taxe3
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
total_taux_taxes = taux_taxe1 + taux_taxe2 + taux_taxe3
|
||||||
|
montant_ttc = montant_net * (1 + total_taux_taxes / 100)
|
||||||
|
|
||||||
|
montant_taxe1 = montant_net * (taux_taxe1 / 100)
|
||||||
|
montant_taxe2 = montant_net * (taux_taxe2 / 100)
|
||||||
|
montant_taxe3 = montant_net * (taux_taxe3 / 100)
|
||||||
|
|
||||||
|
ligne = {
|
||||||
|
"numero_ligne": (
|
||||||
|
int(ligne_row.DL_Ligne) if ligne_row.DL_Ligne else 0
|
||||||
|
),
|
||||||
|
"article_code": _safe_strip(ligne_row.AR_Ref),
|
||||||
|
"designation": _safe_strip(ligne_row.DL_Design),
|
||||||
|
"designation_article": _safe_strip(ligne_row.AR_Design),
|
||||||
|
"quantite": (
|
||||||
|
float(ligne_row.DL_Qte) if ligne_row.DL_Qte else 0.0
|
||||||
|
),
|
||||||
|
"quantite_livree": (
|
||||||
|
float(ligne_row.DL_QteLiv)
|
||||||
|
if hasattr(ligne_row, "DL_QteLiv")
|
||||||
|
and ligne_row.DL_QteLiv
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"quantite_reservee": (
|
||||||
|
float(ligne_row.DL_QteRes)
|
||||||
|
if hasattr(ligne_row, "DL_QteRes")
|
||||||
|
and ligne_row.DL_QteRes
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"unite": (
|
||||||
|
_safe_strip(ligne_row.DL_Unite)
|
||||||
|
if hasattr(ligne_row, "DL_Unite")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"prix_unitaire_ht": (
|
||||||
|
float(ligne_row.DL_PrixUnitaire)
|
||||||
|
if ligne_row.DL_PrixUnitaire
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"prix_unitaire_achat": (
|
||||||
|
float(ligne_row.AR_PrixAch)
|
||||||
|
if ligne_row.AR_PrixAch
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"prix_unitaire_vente": (
|
||||||
|
float(ligne_row.AR_PrixVen)
|
||||||
|
if ligne_row.AR_PrixVen
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"prix_unitaire_ttc": (
|
||||||
|
float(ligne_row.AR_PrixTTC)
|
||||||
|
if ligne_row.AR_PrixTTC
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"montant_ligne_ht": montant_ht,
|
||||||
|
"montant_ligne_net": montant_net,
|
||||||
|
"montant_ligne_ttc": montant_ttc,
|
||||||
|
"remise_valeur1": (
|
||||||
|
float(ligne_row.DL_Remise01REM_Valeur)
|
||||||
|
if hasattr(ligne_row, "DL_Remise01REM_Valeur")
|
||||||
|
and ligne_row.DL_Remise01REM_Valeur
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"remise_type1": (
|
||||||
|
int(ligne_row.DL_Remise01REM_Type)
|
||||||
|
if hasattr(ligne_row, "DL_Remise01REM_Type")
|
||||||
|
and ligne_row.DL_Remise01REM_Type
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"remise_valeur2": (
|
||||||
|
float(ligne_row.DL_Remise02REM_Valeur)
|
||||||
|
if hasattr(ligne_row, "DL_Remise02REM_Valeur")
|
||||||
|
and ligne_row.DL_Remise02REM_Valeur
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"remise_type2": (
|
||||||
|
int(ligne_row.DL_Remise02REM_Type)
|
||||||
|
if hasattr(ligne_row, "DL_Remise02REM_Type")
|
||||||
|
and ligne_row.DL_Remise02REM_Type
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"remise_article": (
|
||||||
|
float(ligne_row.AR_Escompte)
|
||||||
|
if ligne_row.AR_Escompte
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"taux_taxe1": taux_taxe1,
|
||||||
|
"montant_taxe1": montant_taxe1,
|
||||||
|
"taux_taxe2": taux_taxe2,
|
||||||
|
"montant_taxe2": montant_taxe2,
|
||||||
|
"taux_taxe3": taux_taxe3,
|
||||||
|
"montant_taxe3": montant_taxe3,
|
||||||
|
"total_taxes": montant_taxe1
|
||||||
|
+ montant_taxe2
|
||||||
|
+ montant_taxe3,
|
||||||
|
"famille_article": _safe_strip(ligne_row.FA_CodeFamille),
|
||||||
|
"gamme1": _safe_strip(ligne_row.AR_Gamme1),
|
||||||
|
"gamme2": _safe_strip(ligne_row.AR_Gamme2),
|
||||||
|
"code_barre": _safe_strip(ligne_row.AR_CodeBarre),
|
||||||
|
"type_article": _safe_strip(ligne_row.AR_Type),
|
||||||
|
"nature_article": _safe_strip(ligne_row.AR_Nature),
|
||||||
|
"garantie": _safe_strip(ligne_row.AR_Garantie),
|
||||||
|
"cout_standard": (
|
||||||
|
float(ligne_row.AR_CoutStd)
|
||||||
|
if ligne_row.AR_CoutStd
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"poids_net": (
|
||||||
|
float(ligne_row.AR_PoidsNet)
|
||||||
|
if ligne_row.AR_PoidsNet
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"poids_brut": (
|
||||||
|
float(ligne_row.AR_PoidsBrut)
|
||||||
|
if ligne_row.AR_PoidsBrut
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"unite_vente": _safe_strip(ligne_row.AR_UniteVen),
|
||||||
|
"date_livraison_ligne": (
|
||||||
|
str(ligne_row.DL_DateLivr)
|
||||||
|
if hasattr(ligne_row, "DL_DateLivr")
|
||||||
|
and ligne_row.DL_DateLivr
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"statut_ligne": (
|
||||||
|
int(ligne_row.DL_Statut)
|
||||||
|
if hasattr(ligne_row, "DL_Statut")
|
||||||
|
and ligne_row.DL_Statut is not None
|
||||||
|
else 0
|
||||||
|
),
|
||||||
|
"depot": (
|
||||||
|
_safe_strip(ligne_row.DE_No)
|
||||||
|
if hasattr(ligne_row, "DE_No")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"numero_commande": (
|
||||||
|
_safe_strip(ligne_row.DL_NoColis)
|
||||||
|
if hasattr(ligne_row, "DL_NoColis")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
"num_colis": (
|
||||||
|
_safe_strip(ligne_row.DL_Colis)
|
||||||
|
if hasattr(ligne_row, "DL_Colis")
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
}
|
||||||
|
doc["lignes"].append(ligne)
|
||||||
|
|
||||||
|
doc["nb_lignes"] = len(doc["lignes"])
|
||||||
|
doc["total_ht_calcule"] = doc["total_ht_net"]
|
||||||
|
doc["total_ttc_calcule"] = doc["total_ttc"]
|
||||||
|
doc["total_taxes_calcule"] = doc["total_ttc"] - doc["total_ht_net"]
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"[SQL LIST] {numero} : {doc['nb_lignes']} lignes chargées"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"[SQL LIST] {numero} : ERREUR lignes: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
stats["erreur_lignes"] += 1
|
||||||
|
|
||||||
|
documents.append(doc)
|
||||||
|
stats["succes"] += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"[SQL LIST] {numero} : EXCEPTION GLOBALE - DOCUMENT EXCLU: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return documents
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" Erreur GLOBALE listage: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_afficher_etat_document",
|
||||||
|
"_compter_lignes_document",
|
||||||
|
"_rechercher_devis_par_numero",
|
||||||
|
"_lire_document_sql",
|
||||||
|
"_lister_documents_avec_lignes_sql",
|
||||||
|
]
|
||||||
1829
utils/documents/settle.py
Normal file
1829
utils/documents/settle.py
Normal file
File diff suppressed because it is too large
Load diff
596
utils/documents/validations.py
Normal file
596
utils/documents/validations.py
Normal file
|
|
@ -0,0 +1,596 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
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]}
|
||||||
|
)
|
||||||
|
|
||||||
|
result["persist"]["validation_related"] = [
|
||||||
|
a
|
||||||
|
for a in persist_attrs
|
||||||
|
if any(
|
||||||
|
x in a.lower()
|
||||||
|
for x in ["valid", "lock", "confirm", "statut", "etat"]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
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"] = []
|
||||||
|
|
||||||
|
for attr in doc_attrs:
|
||||||
|
try:
|
||||||
|
val = getattr(doc, attr, None)
|
||||||
|
if callable(val):
|
||||||
|
result["IBODocumentVente3"]["methods"].append(attr)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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]}
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
||||||
|
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)}
|
||||||
|
|
||||||
|
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)")
|
||||||
|
|
||||||
|
with connector._get_sql_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
UPDATE F_DOCENTETE
|
||||||
|
SET DO_Valide = 1, DO_Imprim = 0
|
||||||
|
WHERE DO_Piece = ? AND DO_Type = 6
|
||||||
|
""",
|
||||||
|
(numero_facture,),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
" APPROCHE 1: Modification directe DO_Valide sur IBODocumentVente3..."
|
||||||
|
)
|
||||||
|
doc = win32com.client.CastTo(persist, "IBODocumentVente3")
|
||||||
|
doc.Read()
|
||||||
|
|
||||||
|
valeur_avant = getattr(doc, "DO_Valide", None)
|
||||||
|
logger.info(f" DO_Valide avant: {valeur_avant}")
|
||||||
|
|
||||||
|
doc.DO_Valide = valeur_cible
|
||||||
|
doc.Write()
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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)()
|
||||||
|
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}")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
"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]}
|
||||||
|
|
||||||
|
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",
|
||||||
|
]
|
||||||
129
utils/enums.py
Normal file
129
utils/enums.py
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
from enum import IntEnum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class SuiviStockType(IntEnum):
|
||||||
|
AUCUN = 0
|
||||||
|
CMUP = 1
|
||||||
|
FIFO_LIFO = 2
|
||||||
|
SERIALISE = 3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, value: Optional[int]) -> Optional[str]:
|
||||||
|
labels = {0: "Aucun", 1: "CMUP", 2: "FIFO/LIFO", 3: "Sérialisé"}
|
||||||
|
return labels.get(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class NomenclatureType(IntEnum):
|
||||||
|
NON = 0
|
||||||
|
FABRICATION = 1
|
||||||
|
COMMERCIALE = 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, value: Optional[int]) -> Optional[str]:
|
||||||
|
labels = {0: "Non", 1: "Fabrication", 2: "Commerciale/Composé"}
|
||||||
|
return labels.get(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class TypeArticle(IntEnum):
|
||||||
|
ARTICLE = 0
|
||||||
|
PRESTATION = 1
|
||||||
|
DIVERS = 2
|
||||||
|
NOMENCLATURE = 3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, value: Optional[int]) -> Optional[str]:
|
||||||
|
labels = {
|
||||||
|
0: "Article",
|
||||||
|
1: "Prestation de service",
|
||||||
|
2: "Divers / Frais",
|
||||||
|
3: "Nomenclature",
|
||||||
|
}
|
||||||
|
return labels.get(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class TypeFamille(IntEnum):
|
||||||
|
DETAIL = 0
|
||||||
|
TOTAL = 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, value: Optional[int]) -> Optional[str]:
|
||||||
|
labels = {0: "Détail", 1: "Total"}
|
||||||
|
return labels.get(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class TypeCompta(IntEnum):
|
||||||
|
VENTE = 0
|
||||||
|
ACHAT = 1
|
||||||
|
STOCK = 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, value: Optional[int]) -> Optional[str]:
|
||||||
|
labels = {0: "Vente", 1: "Achat", 2: "Stock"}
|
||||||
|
return labels.get(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class TypeRessource(IntEnum):
|
||||||
|
MAIN_OEUVRE = 0
|
||||||
|
MACHINE = 1
|
||||||
|
SOUS_TRAITANCE = 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, value: Optional[int]) -> Optional[str]:
|
||||||
|
labels = {0: "Main d'œuvre", 1: "Machine", 2: "Sous-traitance"}
|
||||||
|
return labels.get(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class TypeTiers(IntEnum):
|
||||||
|
CLIENT = 0
|
||||||
|
FOURNISSEUR = 1
|
||||||
|
SALARIE = 2
|
||||||
|
AUTRE = 3
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, value: Optional[int]) -> Optional[str]:
|
||||||
|
labels = {0: "Client", 1: "Fournisseur", 2: "Salarié", 3: "Autre"}
|
||||||
|
return labels.get(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class TypeEmplacement(IntEnum):
|
||||||
|
NORMAL = 0
|
||||||
|
QUARANTAINE = 1
|
||||||
|
REBUT = 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_label(cls, value: Optional[int]) -> Optional[str]:
|
||||||
|
labels = {0: "Normal", 1: "Quarantaine", 2: "Rebut"}
|
||||||
|
return labels.get(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_enum_to_string(value, default="0") -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if value == 0:
|
||||||
|
return None
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_enum_to_int(value, default=0) -> Optional[int]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_string_field(value) -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, int):
|
||||||
|
if value == 0:
|
||||||
|
return None
|
||||||
|
return str(value)
|
||||||
|
if isinstance(value, str):
|
||||||
|
stripped = value.strip()
|
||||||
|
if stripped in ("", "0"):
|
||||||
|
return None
|
||||||
|
return stripped
|
||||||
|
return str(value)
|
||||||
0
utils/functions/__init__.py
Normal file
0
utils/functions/__init__.py
Normal file
0
utils/functions/data/__init__.py
Normal file
0
utils/functions/data/__init__.py
Normal file
652
utils/functions/data/create_doc.py
Normal file
652
utils/functions/data/create_doc.py
Normal file
|
|
@ -0,0 +1,652 @@
|
||||||
|
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",
|
||||||
|
]
|
||||||
214
utils/functions/functions.py
Normal file
214
utils/functions/functions.py
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime, date
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_str(value, max_len: int) -> str:
|
||||||
|
"""Nettoie et tronque une chaîne"""
|
||||||
|
if value is None or str(value).lower() in ("none", "null", ""):
|
||||||
|
return ""
|
||||||
|
return str(value)[:max_len].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_strip(value) -> Optional[str]:
|
||||||
|
"""Nettoie une valeur string en toute sécurité"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, str):
|
||||||
|
stripped = value.strip()
|
||||||
|
return stripped if stripped else None
|
||||||
|
return str(value).strip() or None
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_int(value, default=None):
|
||||||
|
"""Conversion sécurisée en entier"""
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _try_set_attribute(obj, attr_name, value, variants=None):
|
||||||
|
"""Essaie de définir un attribut avec plusieurs variantes"""
|
||||||
|
if variants is None:
|
||||||
|
variants = [attr_name]
|
||||||
|
else:
|
||||||
|
variants = [attr_name] + variants
|
||||||
|
|
||||||
|
for variant in variants:
|
||||||
|
try:
|
||||||
|
if hasattr(obj, variant):
|
||||||
|
setattr(obj, variant, value)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f" {variant} échec: {str(e)[:50]}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_type_libelle(type_doc: int) -> str:
|
||||||
|
types_officiels = {
|
||||||
|
0: "Devis",
|
||||||
|
10: "Bon de commande",
|
||||||
|
20: "Préparation",
|
||||||
|
30: "Bon de livraison",
|
||||||
|
40: "Bon de retour",
|
||||||
|
50: "Bon d'avoir",
|
||||||
|
60: "Facture",
|
||||||
|
}
|
||||||
|
|
||||||
|
types_alternatifs = {
|
||||||
|
1: "Bon de commande",
|
||||||
|
2: "Préparation",
|
||||||
|
3: "Bon de livraison",
|
||||||
|
4: "Bon de retour",
|
||||||
|
5: "Bon d'avoir",
|
||||||
|
6: "Facture",
|
||||||
|
}
|
||||||
|
|
||||||
|
if type_doc in types_officiels:
|
||||||
|
return types_officiels[type_doc]
|
||||||
|
|
||||||
|
if type_doc in types_alternatifs:
|
||||||
|
return types_alternatifs[type_doc]
|
||||||
|
|
||||||
|
return f"Type {type_doc}"
|
||||||
|
|
||||||
|
|
||||||
|
def _convertir_type_pour_sql(type_doc: int) -> int:
|
||||||
|
"""COM → SQL : 0, 10, 20, 30... → 0, 1, 2, 3..."""
|
||||||
|
mapping = {0: 0, 10: 1, 20: 2, 30: 3, 40: 4, 50: 5, 60: 6}
|
||||||
|
return mapping.get(type_doc, type_doc)
|
||||||
|
|
||||||
|
|
||||||
|
def _convertir_type_depuis_sql(type_sql: int) -> int:
|
||||||
|
"""SQL → COM : 0, 1, 2, 3... → 0, 10, 20, 30..."""
|
||||||
|
mapping = {0: 0, 1: 10, 2: 20, 3: 30, 4: 40, 5: 50, 6: 60}
|
||||||
|
return mapping.get(type_sql, type_sql)
|
||||||
|
|
||||||
|
|
||||||
|
def _normaliser_type_document(type_doc: int) -> int:
|
||||||
|
logger.info(f"[INFO] TYPE RECU{type_doc}")
|
||||||
|
|
||||||
|
if type_doc in [0, 10, 20, 30, 40, 50, 60]:
|
||||||
|
return type_doc
|
||||||
|
|
||||||
|
mapping_normalisation = {
|
||||||
|
1: 10,
|
||||||
|
2: 20,
|
||||||
|
3: 30,
|
||||||
|
4: 40,
|
||||||
|
5: 50,
|
||||||
|
6: 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping_normalisation.get(type_doc, type_doc)
|
||||||
|
|
||||||
|
|
||||||
|
def normaliser_date(valeur):
|
||||||
|
"""Parse flexible des dates - supporte ISO, datetime, YYYY-MM-DD HH:MM:SS"""
|
||||||
|
if valeur is None:
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
|
if isinstance(valeur, datetime):
|
||||||
|
return valeur
|
||||||
|
|
||||||
|
if isinstance(valeur, date):
|
||||||
|
return datetime.combine(valeur, datetime.min.time())
|
||||||
|
|
||||||
|
if isinstance(valeur, str):
|
||||||
|
valeur = valeur.strip()
|
||||||
|
|
||||||
|
if not valeur:
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
|
if valeur.endswith("Z"):
|
||||||
|
valeur = valeur[:-1] + "+00:00"
|
||||||
|
|
||||||
|
formats = [
|
||||||
|
"%Y-%m-%dT%H:%M:%S.%f%z",
|
||||||
|
"%Y-%m-%dT%H:%M:%S%z",
|
||||||
|
"%Y-%m-%dT%H:%M:%S.%f",
|
||||||
|
"%Y-%m-%dT%H:%M:%S",
|
||||||
|
"%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
"%Y-%m-%d %H:%M:%S",
|
||||||
|
"%Y-%m-%d %H:%M",
|
||||||
|
"%Y-%m-%d",
|
||||||
|
"%d/%m/%Y %H:%M:%S",
|
||||||
|
"%d/%m/%Y %H:%M",
|
||||||
|
"%d/%m/%Y",
|
||||||
|
]
|
||||||
|
|
||||||
|
for fmt in formats:
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(valeur, fmt)
|
||||||
|
return dt.replace(tzinfo=None) if dt.tzinfo else dt
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if "+" in valeur:
|
||||||
|
valeur = valeur.split("+")[0]
|
||||||
|
dt = datetime.fromisoformat(valeur)
|
||||||
|
return dt.replace(tzinfo=None) if dt.tzinfo else dt
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
def _parser_heure_sage(do_heure) -> str:
|
||||||
|
"""Parse DO_Heure format Sage (HHMMSS stocké en entier)"""
|
||||||
|
if not do_heure:
|
||||||
|
return "00:00:00"
|
||||||
|
|
||||||
|
try:
|
||||||
|
heure_int = int(str(do_heure).strip())
|
||||||
|
|
||||||
|
heure_str = str(heure_int).zfill(6)
|
||||||
|
|
||||||
|
hh = int(heure_str[0:2])
|
||||||
|
mm = int(heure_str[2:4])
|
||||||
|
ss = int(heure_str[4:6])
|
||||||
|
|
||||||
|
if 0 <= hh <= 23 and 0 <= mm <= 59 and 0 <= ss <= 59:
|
||||||
|
return f"{hh:02d}:{mm:02d}:{ss:02d}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return "00:00:00"
|
||||||
|
|
||||||
|
|
||||||
|
def _combiner_date_heure(do_date, do_heure) -> str:
|
||||||
|
"""Combine DO_Date et DO_Heure en datetime string"""
|
||||||
|
if not do_date:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
date_str = (
|
||||||
|
do_date.strftime("%Y-%m-%d")
|
||||||
|
if hasattr(do_date, "strftime")
|
||||||
|
else str(do_date)[:10]
|
||||||
|
)
|
||||||
|
heure_str = _parser_heure_sage(do_heure)
|
||||||
|
return f"{date_str} {heure_str}"
|
||||||
|
except Exception:
|
||||||
|
return str(do_date) if do_date else ""
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_clean_str",
|
||||||
|
"_safe_strip",
|
||||||
|
"_safe_int",
|
||||||
|
"_try_set_attribute",
|
||||||
|
"_get_type_libelle",
|
||||||
|
"_normaliser_type_document",
|
||||||
|
"_convertir_type_depuis_sql",
|
||||||
|
"_convertir_type_pour_sql",
|
||||||
|
"normaliser_date",
|
||||||
|
"_combiner_date_heure",
|
||||||
|
]
|
||||||
275
utils/functions/items_to_dict.py
Normal file
275
utils/functions/items_to_dict.py
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
from typing import Dict, Optional
|
||||||
|
import logging
|
||||||
|
from utils.functions.functions import _safe_strip
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def contacts_to_dict(
|
||||||
|
contact, numero_client=None, contact_numero=None, n_contact=None
|
||||||
|
) -> Dict:
|
||||||
|
try:
|
||||||
|
civilite_code = getattr(contact, "Civilite", None)
|
||||||
|
civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
|
||||||
|
civilite = (
|
||||||
|
civilite_map.get(civilite_code) if civilite_code is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
telephone = None
|
||||||
|
portable = None
|
||||||
|
telecopie = None
|
||||||
|
email = None
|
||||||
|
|
||||||
|
if hasattr(contact, "Telecom"):
|
||||||
|
try:
|
||||||
|
telecom = contact.Telecom
|
||||||
|
telephone = _safe_strip(getattr(telecom, "Telephone", None))
|
||||||
|
portable = _safe_strip(getattr(telecom, "Portable", None))
|
||||||
|
telecopie = _safe_strip(getattr(telecom, "Telecopie", None))
|
||||||
|
email = _safe_strip(getattr(telecom, "EMail", None))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"numero": numero_client,
|
||||||
|
"contact_numero": contact_numero,
|
||||||
|
"n_contact": n_contact or contact_numero,
|
||||||
|
"civilite": civilite,
|
||||||
|
"nom": _safe_strip(getattr(contact, "Nom", None)),
|
||||||
|
"prenom": _safe_strip(getattr(contact, "Prenom", None)),
|
||||||
|
"fonction": _safe_strip(getattr(contact, "Fonction", None)),
|
||||||
|
"service_code": getattr(contact, "ServiceContact", None),
|
||||||
|
"telephone": telephone,
|
||||||
|
"portable": portable,
|
||||||
|
"telecopie": telecopie,
|
||||||
|
"email": email,
|
||||||
|
"facebook": _safe_strip(getattr(contact, "Facebook", None)),
|
||||||
|
"linkedin": _safe_strip(getattr(contact, "LinkedIn", None)),
|
||||||
|
"skype": _safe_strip(getattr(contact, "Skype", None)),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Erreur conversion contact: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def contact_to_dict(row) -> Dict:
|
||||||
|
"""Convertit une ligne SQL en dictionnaire contact"""
|
||||||
|
civilite_code = row.CT_Civilite
|
||||||
|
civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"numero": _safe_strip(row.CT_Num),
|
||||||
|
"contact_numero": row.CT_No,
|
||||||
|
"n_contact": row.N_Contact,
|
||||||
|
"civilite": civilite_map.get(civilite_code)
|
||||||
|
if civilite_code is not None
|
||||||
|
else None,
|
||||||
|
"nom": _safe_strip(row.CT_Nom),
|
||||||
|
"prenom": _safe_strip(row.CT_Prenom),
|
||||||
|
"fonction": _safe_strip(row.CT_Fonction),
|
||||||
|
"service_code": row.N_Service,
|
||||||
|
"telephone": _safe_strip(row.CT_Telephone),
|
||||||
|
"portable": _safe_strip(row.CT_TelPortable),
|
||||||
|
"telecopie": _safe_strip(row.CT_Telecopie),
|
||||||
|
"email": _safe_strip(row.CT_EMail),
|
||||||
|
"facebook": _safe_strip(row.CT_Facebook),
|
||||||
|
"linkedin": _safe_strip(row.CT_LinkedIn),
|
||||||
|
"skype": _safe_strip(row.CT_Skype),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _collaborators_to_dict(row) -> Optional[dict]:
|
||||||
|
"""Convertit une ligne SQL en dictionnaire collaborateur"""
|
||||||
|
if not hasattr(row, "Collab_CO_No") or row.Collab_CO_No is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"numero": row.Collab_CO_No,
|
||||||
|
"nom": _safe_strip(row.Collab_CO_Nom),
|
||||||
|
"prenom": _safe_strip(row.Collab_CO_Prenom),
|
||||||
|
"fonction": _safe_strip(row.Collab_CO_Fonction),
|
||||||
|
"adresse": _safe_strip(row.Collab_CO_Adresse),
|
||||||
|
"complement": _safe_strip(row.Collab_CO_Complement),
|
||||||
|
"code_postal": _safe_strip(row.Collab_CO_CodePostal),
|
||||||
|
"ville": _safe_strip(row.Collab_CO_Ville),
|
||||||
|
"region": _safe_strip(row.Collab_CO_CodeRegion),
|
||||||
|
"pays": _safe_strip(row.Collab_CO_Pays),
|
||||||
|
"service": _safe_strip(row.Collab_CO_Service),
|
||||||
|
"est_vendeur": (row.Collab_CO_Vendeur == 1)
|
||||||
|
if row.Collab_CO_Vendeur is not None
|
||||||
|
else None,
|
||||||
|
"est_caissier": (row.Collab_CO_Caissier == 1)
|
||||||
|
if row.Collab_CO_Caissier is not None
|
||||||
|
else None,
|
||||||
|
"est_acheteur": (row.Collab_CO_Acheteur == 1)
|
||||||
|
if row.Collab_CO_Acheteur is not None
|
||||||
|
else None,
|
||||||
|
"telephone": _safe_strip(row.Collab_CO_Telephone),
|
||||||
|
"telecopie": _safe_strip(row.Collab_CO_Telecopie),
|
||||||
|
"email": _safe_strip(row.Collab_CO_EMail),
|
||||||
|
"tel_portable": _safe_strip(row.Collab_CO_TelPortable),
|
||||||
|
"matricule": _safe_strip(row.Collab_CO_Matricule),
|
||||||
|
"facebook": _safe_strip(row.Collab_CO_Facebook),
|
||||||
|
"linkedin": _safe_strip(row.Collab_CO_LinkedIn),
|
||||||
|
"skype": _safe_strip(row.Collab_CO_Skype),
|
||||||
|
"est_actif": (row.Collab_CO_Sommeil == 0)
|
||||||
|
if row.Collab_CO_Sommeil is not None
|
||||||
|
else None,
|
||||||
|
"est_chef_ventes": (row.Collab_CO_ChefVentes == 1)
|
||||||
|
if row.Collab_CO_ChefVentes is not None
|
||||||
|
else None,
|
||||||
|
"chef_ventes_numero": row.Collab_CO_NoChefVentes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def collaborators_to_dict(row):
|
||||||
|
"""Convertit une ligne SQL en dictionnaire collaborateur"""
|
||||||
|
return {
|
||||||
|
"numero": row.CO_No,
|
||||||
|
"nom": _safe_strip(row.CO_Nom),
|
||||||
|
"prenom": _safe_strip(row.CO_Prenom),
|
||||||
|
"fonction": _safe_strip(row.CO_Fonction),
|
||||||
|
"adresse": _safe_strip(row.CO_Adresse),
|
||||||
|
"complement": _safe_strip(row.CO_Complement),
|
||||||
|
"code_postal": _safe_strip(row.CO_CodePostal),
|
||||||
|
"ville": _safe_strip(row.CO_Ville),
|
||||||
|
"code_region": _safe_strip(row.CO_CodeRegion),
|
||||||
|
"pays": _safe_strip(row.CO_Pays),
|
||||||
|
"service": _safe_strip(row.CO_Service),
|
||||||
|
"vendeur": bool(row.CO_Vendeur),
|
||||||
|
"caissier": bool(row.CO_Caissier),
|
||||||
|
"acheteur": bool(row.CO_Acheteur),
|
||||||
|
"telephone": _safe_strip(row.CO_Telephone),
|
||||||
|
"telecopie": _safe_strip(row.CO_Telecopie),
|
||||||
|
"email": _safe_strip(row.CO_EMail),
|
||||||
|
"tel_portable": _safe_strip(row.CO_TelPortable),
|
||||||
|
"matricule": _safe_strip(row.CO_Matricule),
|
||||||
|
"facebook": _safe_strip(row.CO_Facebook),
|
||||||
|
"linkedin": _safe_strip(row.CO_LinkedIn),
|
||||||
|
"skype": _safe_strip(row.CO_Skype),
|
||||||
|
"sommeil": bool(row.CO_Sommeil),
|
||||||
|
"chef_ventes": bool(row.CO_ChefVentes),
|
||||||
|
"numero_chef_ventes": row.CO_NoChefVentes if row.CO_NoChefVentes else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def tiers_to_dict(row) -> dict:
|
||||||
|
"""Convertit une ligne SQL en dictionnaire tiers"""
|
||||||
|
tiers = {
|
||||||
|
"numero": _safe_strip(row.CT_Num),
|
||||||
|
"intitule": _safe_strip(row.CT_Intitule),
|
||||||
|
"type_tiers": row.CT_Type,
|
||||||
|
"qualite": _safe_strip(row.CT_Qualite),
|
||||||
|
"classement": _safe_strip(row.CT_Classement),
|
||||||
|
"raccourci": _safe_strip(row.CT_Raccourci),
|
||||||
|
"siret": _safe_strip(row.CT_Siret),
|
||||||
|
"tva_intra": _safe_strip(row.CT_Identifiant),
|
||||||
|
"code_naf": _safe_strip(row.CT_Ape),
|
||||||
|
"contact": _safe_strip(row.CT_Contact),
|
||||||
|
"adresse": _safe_strip(row.CT_Adresse),
|
||||||
|
"complement": _safe_strip(row.CT_Complement),
|
||||||
|
"code_postal": _safe_strip(row.CT_CodePostal),
|
||||||
|
"ville": _safe_strip(row.CT_Ville),
|
||||||
|
"region": _safe_strip(row.CT_CodeRegion),
|
||||||
|
"pays": _safe_strip(row.CT_Pays),
|
||||||
|
"telephone": _safe_strip(row.CT_Telephone),
|
||||||
|
"telecopie": _safe_strip(row.CT_Telecopie),
|
||||||
|
"email": _safe_strip(row.CT_EMail),
|
||||||
|
"site_web": _safe_strip(row.CT_Site),
|
||||||
|
"facebook": _safe_strip(row.CT_Facebook),
|
||||||
|
"linkedin": _safe_strip(row.CT_LinkedIn),
|
||||||
|
"taux01": row.CT_Taux01,
|
||||||
|
"taux02": row.CT_Taux02,
|
||||||
|
"taux03": row.CT_Taux03,
|
||||||
|
"taux04": row.CT_Taux04,
|
||||||
|
"statistique01": _safe_strip(row.CT_Statistique01),
|
||||||
|
"statistique02": _safe_strip(row.CT_Statistique02),
|
||||||
|
"statistique03": _safe_strip(row.CT_Statistique03),
|
||||||
|
"statistique04": _safe_strip(row.CT_Statistique04),
|
||||||
|
"statistique05": _safe_strip(row.CT_Statistique05),
|
||||||
|
"statistique06": _safe_strip(row.CT_Statistique06),
|
||||||
|
"statistique07": _safe_strip(row.CT_Statistique07),
|
||||||
|
"statistique08": _safe_strip(row.CT_Statistique08),
|
||||||
|
"statistique09": _safe_strip(row.CT_Statistique09),
|
||||||
|
"statistique10": _safe_strip(row.CT_Statistique10),
|
||||||
|
"encours_autorise": row.CT_Encours,
|
||||||
|
"assurance_credit": row.CT_Assurance,
|
||||||
|
"langue": row.CT_Langue,
|
||||||
|
"commercial_code": row.CO_No,
|
||||||
|
"commercial": _collaborators_to_dict(row),
|
||||||
|
"lettrage_auto": (row.CT_Lettrage == 1),
|
||||||
|
"est_actif": (row.CT_Sommeil == 0),
|
||||||
|
"type_facture": row.CT_Facture,
|
||||||
|
"est_prospect": (row.CT_Prospect == 1),
|
||||||
|
"bl_en_facture": row.CT_BLFact,
|
||||||
|
"saut_page": row.CT_Saut,
|
||||||
|
"validation_echeance": row.CT_ValidEch,
|
||||||
|
"controle_encours": row.CT_ControlEnc,
|
||||||
|
"exclure_relance": (row.CT_NotRappel == 1),
|
||||||
|
"exclure_penalites": (row.CT_NotPenal == 1),
|
||||||
|
"bon_a_payer": row.CT_BonAPayer,
|
||||||
|
"priorite_livraison": row.CT_PrioriteLivr,
|
||||||
|
"livraison_partielle": row.CT_LivrPartielle,
|
||||||
|
"delai_transport": row.CT_DelaiTransport,
|
||||||
|
"delai_appro": row.CT_DelaiAppro,
|
||||||
|
"commentaire": _safe_strip(row.CT_Commentaire),
|
||||||
|
"section_analytique": _safe_strip(row.CA_Num),
|
||||||
|
"mode_reglement_code": row.MR_No,
|
||||||
|
"surveillance_active": (row.CT_Surveillance == 1),
|
||||||
|
"coface": _safe_strip(row.CT_Coface),
|
||||||
|
"forme_juridique": _safe_strip(row.CT_SvFormeJuri),
|
||||||
|
"effectif": _safe_strip(row.CT_SvEffectif),
|
||||||
|
"sv_regularite": _safe_strip(row.CT_SvRegul),
|
||||||
|
"sv_cotation": _safe_strip(row.CT_SvCotation),
|
||||||
|
"sv_objet_maj": _safe_strip(row.CT_SvObjetMaj),
|
||||||
|
"sv_chiffre_affaires": row.CT_SvCA,
|
||||||
|
"sv_resultat": row.CT_SvResultat,
|
||||||
|
"compte_general": _safe_strip(row.CG_NumPrinc),
|
||||||
|
"categorie_tarif": row.N_CatTarif,
|
||||||
|
"categorie_compta": row.N_CatCompta,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tiers
|
||||||
|
|
||||||
|
|
||||||
|
def society_to_dict(row) -> dict:
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"raison_sociale": _safe_strip(row.D_RaisonSoc),
|
||||||
|
"numero_dossier": _safe_strip(row.D_NumDoss),
|
||||||
|
"siret": _safe_strip(row.D_Siret),
|
||||||
|
"code_ape": _safe_strip(row.D_Ape),
|
||||||
|
"numero_tva": _safe_strip(row.D_Identifiant),
|
||||||
|
"adresse": _safe_strip(row.D_Adresse),
|
||||||
|
"complement_adresse": _safe_strip(row.D_Complement),
|
||||||
|
"code_postal": _safe_strip(row.D_CodePostal),
|
||||||
|
"ville": _safe_strip(row.D_Ville),
|
||||||
|
"code_region": _safe_strip(row.D_CodeRegion),
|
||||||
|
"pays": _safe_strip(row.D_Pays),
|
||||||
|
"telephone": _safe_strip(row.D_Telephone),
|
||||||
|
"telecopie": _safe_strip(row.D_Telecopie),
|
||||||
|
"email": _safe_strip(row.D_EMail),
|
||||||
|
"email_societe": _safe_strip(row.D_EMailSoc),
|
||||||
|
"site_web": _safe_strip(row.D_Site),
|
||||||
|
"capital": float(row.D_Capital) if row.D_Capital else 0.0,
|
||||||
|
"forme_juridique": _safe_strip(row.D_FormeJuridique),
|
||||||
|
"devise_compte": row.N_DeviseCompte or 0,
|
||||||
|
"devise_equivalent": row.N_DeviseEquival or 0,
|
||||||
|
"longueur_compte_general": row.D_LgCg or 0,
|
||||||
|
"longueur_compte_analytique": row.D_LgAn or 0,
|
||||||
|
"regime_fec": row.D_RegimeFEC or 0,
|
||||||
|
"base_modele": _safe_strip(row.BM_Intitule),
|
||||||
|
"marqueur": row.cbMarq or 0,
|
||||||
|
"_logo_path": _safe_strip(row.D_Logo),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"contacts_to_dict",
|
||||||
|
"contact_to_dict",
|
||||||
|
"tiers_to_dict",
|
||||||
|
"collaborators_to_dict",
|
||||||
|
"society_to_dict"
|
||||||
|
]
|
||||||
232
utils/functions/sage_utilities.py
Normal file
232
utils/functions/sage_utilities.py
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
import logging
|
||||||
|
from utils.functions.functions import (
|
||||||
|
_convertir_type_depuis_sql,
|
||||||
|
_convertir_type_pour_sql,
|
||||||
|
_normaliser_type_document,
|
||||||
|
_get_type_libelle,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _verifier_devis_non_transforme(numero: str, doc, cursor):
|
||||||
|
"""Vérifie que le devis n'est pas transformé."""
|
||||||
|
verification = verifier_si_deja_transforme_sql(numero, cursor, 0)
|
||||||
|
|
||||||
|
if verification["deja_transforme"]:
|
||||||
|
docs_cibles = verification["documents_cibles"]
|
||||||
|
nums = [d["numero"] for d in docs_cibles]
|
||||||
|
raise ValueError(
|
||||||
|
f" Devis {numero} déjà transformé en {len(docs_cibles)} document(s): {', '.join(nums)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
statut_actuel = getattr(doc, "DO_Statut", 0)
|
||||||
|
if statut_actuel == 5:
|
||||||
|
raise ValueError(f" Devis {numero} déjà transformé (statut=5)")
|
||||||
|
|
||||||
|
|
||||||
|
def verifier_si_deja_transforme_sql(numero_source, cursor, type_source):
|
||||||
|
"""Version corrigée avec normalisation des types"""
|
||||||
|
logger.info(
|
||||||
|
f"[VERIF] Vérification transformations de {numero_source} (type {type_source})"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[VERIF] Vérification transformations de {numero_source} (type {type_source})"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"[DEBUG] Type source brut: {type_source}")
|
||||||
|
logger.info(
|
||||||
|
f"[DEBUG] Type source après normalisation: {_normaliser_type_document(type_source)}"
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"[DEBUG] Type source après normalisation SQL: {_convertir_type_pour_sql(type_source)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
type_source = _convertir_type_pour_sql(type_source)
|
||||||
|
|
||||||
|
champ_liaison_mapping = {
|
||||||
|
0: "DL_PieceDE",
|
||||||
|
1: "DL_PieceBC",
|
||||||
|
3: "DL_PieceBL",
|
||||||
|
}
|
||||||
|
|
||||||
|
champ_liaison = champ_liaison_mapping.get(type_source)
|
||||||
|
|
||||||
|
if not champ_liaison:
|
||||||
|
logger.warning(f"[VERIF] Type source {type_source} non géré")
|
||||||
|
return {"deja_transforme": False, "documents_cibles": []}
|
||||||
|
|
||||||
|
try:
|
||||||
|
query = f"""
|
||||||
|
SELECT DISTINCT
|
||||||
|
dc.DO_Piece,
|
||||||
|
dc.DO_Type,
|
||||||
|
dc.DO_Statut,
|
||||||
|
(SELECT COUNT(*) FROM F_DOCLIGNE
|
||||||
|
WHERE DO_Piece = dc.DO_Piece AND DO_Type = dc.DO_Type) as NbLignes
|
||||||
|
FROM F_DOCENTETE dc
|
||||||
|
INNER JOIN F_DOCLIGNE dl ON dc.DO_Piece = dl.DO_Piece AND dc.DO_Type = dl.DO_Type
|
||||||
|
WHERE dl.{champ_liaison} = ?
|
||||||
|
ORDER BY dc.DO_Type, dc.DO_Piece
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(query, (numero_source,))
|
||||||
|
resultats = cursor.fetchall()
|
||||||
|
|
||||||
|
documents_cibles = []
|
||||||
|
for row in resultats:
|
||||||
|
type_brut = int(row.DO_Type)
|
||||||
|
type_normalise = _convertir_type_depuis_sql(type_brut)
|
||||||
|
|
||||||
|
doc = {
|
||||||
|
"numero": row.DO_Piece.strip() if row.DO_Piece else "",
|
||||||
|
"type": type_normalise,
|
||||||
|
"type_brut": type_brut,
|
||||||
|
"type_libelle": _get_type_libelle(type_brut),
|
||||||
|
"statut": int(row.DO_Statut) if row.DO_Statut else 0,
|
||||||
|
"nb_lignes": int(row.NbLignes) if row.NbLignes else 0,
|
||||||
|
}
|
||||||
|
documents_cibles.append(doc)
|
||||||
|
logger.info(
|
||||||
|
f"[VERIF] Trouvé: {doc['numero']} "
|
||||||
|
f"(type {type_brut}→{type_normalise} - {doc['type_libelle']}) "
|
||||||
|
f"- {doc['nb_lignes']} lignes"
|
||||||
|
)
|
||||||
|
|
||||||
|
deja_transforme = len(documents_cibles) > 0
|
||||||
|
|
||||||
|
if deja_transforme:
|
||||||
|
logger.info(
|
||||||
|
f"[VERIF] Document {numero_source} a {len(documents_cibles)} transformation(s)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(f"[VERIF] Document {numero_source} pas encore transformé")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"deja_transforme": deja_transforme,
|
||||||
|
"documents_cibles": documents_cibles,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[VERIF] Erreur vérification: {e}")
|
||||||
|
return {"deja_transforme": False, "documents_cibles": []}
|
||||||
|
|
||||||
|
|
||||||
|
def peut_etre_transforme(cursor, numero_source, type_source, type_cible):
|
||||||
|
"""Version corrigée avec normalisation"""
|
||||||
|
type_source = _normaliser_type_document(type_source)
|
||||||
|
type_cible = _normaliser_type_document(type_cible)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[VERIF_TRANSFO] {numero_source} (type {type_source}) → type {type_cible}"
|
||||||
|
)
|
||||||
|
|
||||||
|
verif = verifier_si_deja_transforme_sql(cursor, numero_source, type_source)
|
||||||
|
|
||||||
|
docs_meme_type = [d for d in verif["documents_cibles"] if d["type"] == type_cible]
|
||||||
|
|
||||||
|
if docs_meme_type:
|
||||||
|
nums = [d["numero"] for d in docs_meme_type]
|
||||||
|
return {
|
||||||
|
"possible": False,
|
||||||
|
"raison": f"Document déjà transformé en {_get_type_libelle(type_cible)}",
|
||||||
|
"documents_existants": docs_meme_type,
|
||||||
|
"message_detaille": f"Document(s) existant(s): {', '.join(nums)}",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"possible": True,
|
||||||
|
"raison": "Transformation possible",
|
||||||
|
"documents_existants": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def lire_erreurs_sage(obj, nom_obj=""):
|
||||||
|
erreurs = []
|
||||||
|
try:
|
||||||
|
if not hasattr(obj, "Errors") or obj.Errors is None:
|
||||||
|
return erreurs
|
||||||
|
|
||||||
|
nb_erreurs = 0
|
||||||
|
try:
|
||||||
|
nb_erreurs = obj.Errors.Count
|
||||||
|
except Exception:
|
||||||
|
return erreurs
|
||||||
|
|
||||||
|
if nb_erreurs == 0:
|
||||||
|
return erreurs
|
||||||
|
|
||||||
|
for i in range(1, nb_erreurs + 1):
|
||||||
|
try:
|
||||||
|
err = None
|
||||||
|
try:
|
||||||
|
err = obj.Errors.Item(i)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
err = obj.Errors(i)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
err = obj.Errors.Item(i - 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if err is not None:
|
||||||
|
description = ""
|
||||||
|
field = ""
|
||||||
|
number = ""
|
||||||
|
|
||||||
|
for attr in ["Description", "Descr", "Message", "Text"]:
|
||||||
|
try:
|
||||||
|
val = getattr(err, attr, None)
|
||||||
|
if val:
|
||||||
|
description = str(val)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for attr in ["Field", "FieldName", "Champ", "Property"]:
|
||||||
|
try:
|
||||||
|
val = getattr(err, attr, None)
|
||||||
|
if val:
|
||||||
|
field = str(val)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for attr in ["Number", "Code", "ErrorCode", "Numero"]:
|
||||||
|
try:
|
||||||
|
val = getattr(err, attr, None)
|
||||||
|
if val is not None:
|
||||||
|
number = str(val)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if description or field or number:
|
||||||
|
erreurs.append(
|
||||||
|
{
|
||||||
|
"source": nom_obj,
|
||||||
|
"index": i,
|
||||||
|
"description": description or "Erreur inconnue",
|
||||||
|
"field": field or "?",
|
||||||
|
"number": number or "?",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur lecture erreur {i}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur globale lecture erreurs {nom_obj}: {e}")
|
||||||
|
|
||||||
|
return erreurs
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_verifier_devis_non_transforme",
|
||||||
|
"verifier_si_deja_transforme_sql",
|
||||||
|
"peut_etre_transforme",
|
||||||
|
"lire_erreurs_sage",
|
||||||
|
]
|
||||||
131
utils/functions/society/societe_data.py
Normal file
131
utils/functions/society/societe_data.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_societe_row(cursor):
|
||||||
|
"""Récupère la ligne P_DOSSIER"""
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
D_RaisonSoc, D_NumDoss, D_Siret, D_Ape, D_Identifiant,
|
||||||
|
D_Adresse, D_Complement, D_CodePostal, D_Ville,
|
||||||
|
D_CodeRegion, D_Pays,
|
||||||
|
D_Telephone, D_Telecopie, D_EMail, D_EMailSoc, D_Site,
|
||||||
|
D_Capital, D_FormeJuridique,
|
||||||
|
D_DebutExo01, D_FinExo01,
|
||||||
|
D_DebutExo02, D_FinExo02,
|
||||||
|
D_DebutExo03, D_FinExo03,
|
||||||
|
D_DebutExo04, D_FinExo04,
|
||||||
|
D_DebutExo05, D_FinExo05,
|
||||||
|
N_DeviseCompte, N_DeviseEquival,
|
||||||
|
D_LgCg, D_LgAn,
|
||||||
|
D_RegimeFEC,
|
||||||
|
BM_Intitule,
|
||||||
|
cbMarq,
|
||||||
|
D_Logo
|
||||||
|
FROM P_DOSSIER
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(query)
|
||||||
|
return cursor.fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def build_exercices(row) -> list:
|
||||||
|
"""Construit la liste des exercices"""
|
||||||
|
exercices_data = [
|
||||||
|
(1, row.D_DebutExo01, row.D_FinExo01),
|
||||||
|
(2, row.D_DebutExo02, row.D_FinExo02),
|
||||||
|
(3, row.D_DebutExo03, row.D_FinExo03),
|
||||||
|
(4, row.D_DebutExo04, row.D_FinExo04),
|
||||||
|
(5, row.D_DebutExo05, row.D_FinExo05),
|
||||||
|
]
|
||||||
|
|
||||||
|
exercices = []
|
||||||
|
for numero, debut, fin in exercices_data:
|
||||||
|
if debut and debut.year > 1753:
|
||||||
|
exercices.append(
|
||||||
|
{
|
||||||
|
"numero": numero,
|
||||||
|
"debut": debut.isoformat(),
|
||||||
|
"fin": fin.isoformat() if fin and fin.year > 1753 else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return exercices
|
||||||
|
|
||||||
|
|
||||||
|
def add_logo(societe_dict: dict) -> None:
|
||||||
|
"""Ajoute le logo en base64 au dict"""
|
||||||
|
logo_path = societe_dict.pop("_logo_path", None)
|
||||||
|
|
||||||
|
if logo_path and Path(logo_path).exists():
|
||||||
|
try:
|
||||||
|
ext = Path(logo_path).suffix.lower()
|
||||||
|
content_type = {
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".bmp": "image/bmp",
|
||||||
|
".gif": "image/gif",
|
||||||
|
}.get(ext, "image/png")
|
||||||
|
|
||||||
|
with open(logo_path, "rb") as f:
|
||||||
|
societe_dict["logo_base64"] = base64.b64encode(f.read()).decode("utf-8")
|
||||||
|
societe_dict["logo_content_type"] = content_type
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Erreur conversion logo: {e}")
|
||||||
|
|
||||||
|
societe_dict["logo_base64"] = None
|
||||||
|
societe_dict["logo_content_type"] = None
|
||||||
|
|
||||||
|
|
||||||
|
def recuperer_logo_com(sage_instance) -> dict:
|
||||||
|
"""Cherche le logo dans les répertoires standards"""
|
||||||
|
return _chercher_logo_standards()
|
||||||
|
|
||||||
|
|
||||||
|
def _chercher_logo_standards() -> dict:
|
||||||
|
"""Cherche dans les répertoires standards Sage"""
|
||||||
|
chemins = [
|
||||||
|
Path("C:/ProgramData/Sage/Logo"),
|
||||||
|
Path("C:/ProgramData/Sage/Sage 100/Logo"),
|
||||||
|
Path("C:/Users/Public/Documents/Sage"),
|
||||||
|
Path(r"C:\Program Files\Sage\Sage 100\Bitmap"),
|
||||||
|
Path(r"C:\Program Files (x86)\Sage\Sage 100\Bitmap"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for repertoire in chemins:
|
||||||
|
if not repertoire.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ext in [".bmp", ".jpg", ".jpeg", ".png", ".gif"]:
|
||||||
|
for fichier in repertoire.glob(f"*{ext}"):
|
||||||
|
logger.info(f"Logo trouvé: {fichier}")
|
||||||
|
return _convertir_fichier_logo(str(fichier))
|
||||||
|
|
||||||
|
logger.info("Aucun logo trouvé")
|
||||||
|
return {"logo_base64": None, "logo_content_type": None}
|
||||||
|
|
||||||
|
|
||||||
|
def _convertir_fichier_logo(chemin: str) -> dict:
|
||||||
|
"""Convertit image en base64"""
|
||||||
|
ext = Path(chemin).suffix.lower()
|
||||||
|
content_type = {
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".bmp": "image/bmp",
|
||||||
|
".gif": "image/gif",
|
||||||
|
}.get(ext, "image/png")
|
||||||
|
|
||||||
|
with open(chemin, "rb") as f:
|
||||||
|
return {
|
||||||
|
"logo_base64": base64.b64encode(f.read()).decode("utf-8"),
|
||||||
|
"logo_content_type": content_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["get_societe_row", "build_exercices", "add_logo", "recuperer_logo_com"]
|
||||||
0
utils/tiers/__init__.py
Normal file
0
utils/tiers/__init__.py
Normal file
0
utils/tiers/clients/__init__.py
Normal file
0
utils/tiers/clients/__init__.py
Normal file
326
utils/tiers/clients/clients_data.py
Normal file
326
utils/tiers/clients/clients_data.py
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
import win32com.client
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _cast_client(persist_obj):
|
||||||
|
try:
|
||||||
|
obj = win32com.client.CastTo(persist_obj, "IBOClient3")
|
||||||
|
obj.Read()
|
||||||
|
return obj
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f" _cast_client échoue: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extraire_client(client_obj):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
numero = getattr(client_obj, "CT_Num", "").strip()
|
||||||
|
if not numero:
|
||||||
|
logger.debug("Objet sans CT_Num, skip")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f" Erreur lecture CT_Num: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
intitule = getattr(client_obj, "CT_Intitule", "").strip()
|
||||||
|
if not intitule:
|
||||||
|
logger.debug(f"{numero} sans CT_Intitule")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur CT_Intitule sur {numero}: {e}")
|
||||||
|
intitule = ""
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"numero": numero,
|
||||||
|
"intitule": intitule,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
qualite_code = getattr(client_obj, "CT_Type", None)
|
||||||
|
|
||||||
|
qualite_map = {
|
||||||
|
0: "CLI",
|
||||||
|
1: "FOU",
|
||||||
|
2: "CLIFOU",
|
||||||
|
3: "SAL",
|
||||||
|
4: "PRO",
|
||||||
|
}
|
||||||
|
|
||||||
|
data["qualite"] = qualite_map.get(qualite_code, "CLI")
|
||||||
|
data["est_fournisseur"] = qualite_code in [1, 2]
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
data["qualite"] = "CLI"
|
||||||
|
data["est_fournisseur"] = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["est_prospect"] = getattr(client_obj, "CT_Prospect", 0) == 1
|
||||||
|
except Exception:
|
||||||
|
data["est_prospect"] = False
|
||||||
|
|
||||||
|
if data["est_prospect"]:
|
||||||
|
data["type_tiers"] = "prospect"
|
||||||
|
elif data["est_fournisseur"] and data["qualite"] != "CLIFOU":
|
||||||
|
data["type_tiers"] = "fournisseur"
|
||||||
|
elif data["qualite"] == "CLIFOU":
|
||||||
|
data["type_tiers"] = "client_fournisseur"
|
||||||
|
else:
|
||||||
|
data["type_tiers"] = "client"
|
||||||
|
|
||||||
|
try:
|
||||||
|
sommeil = getattr(client_obj, "CT_Sommeil", 0)
|
||||||
|
data["est_actif"] = sommeil == 0
|
||||||
|
data["est_en_sommeil"] = sommeil == 1
|
||||||
|
except Exception:
|
||||||
|
data["est_actif"] = True
|
||||||
|
data["est_en_sommeil"] = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
forme_juridique = getattr(client_obj, "CT_FormeJuridique", "").strip()
|
||||||
|
data["forme_juridique"] = forme_juridique
|
||||||
|
data["est_entreprise"] = bool(forme_juridique)
|
||||||
|
data["est_particulier"] = not bool(forme_juridique)
|
||||||
|
except Exception:
|
||||||
|
data["forme_juridique"] = ""
|
||||||
|
data["est_entreprise"] = False
|
||||||
|
data["est_particulier"] = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["civilite"] = getattr(client_obj, "CT_Civilite", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["civilite"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["nom"] = getattr(client_obj, "CT_Nom", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["nom"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["prenom"] = getattr(client_obj, "CT_Prenom", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["prenom"] = ""
|
||||||
|
|
||||||
|
if data.get("nom") or data.get("prenom"):
|
||||||
|
parts = []
|
||||||
|
if data.get("civilite"):
|
||||||
|
parts.append(data["civilite"])
|
||||||
|
if data.get("prenom"):
|
||||||
|
parts.append(data["prenom"])
|
||||||
|
if data.get("nom"):
|
||||||
|
parts.append(data["nom"])
|
||||||
|
data["nom_complet"] = " ".join(parts)
|
||||||
|
else:
|
||||||
|
data["nom_complet"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["contact"] = getattr(client_obj, "CT_Contact", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["contact"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
adresse_obj = getattr(client_obj, "Adresse", None)
|
||||||
|
if adresse_obj:
|
||||||
|
try:
|
||||||
|
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["adresse"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["complement"] = getattr(adresse_obj, "Complement", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["complement"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["code_postal"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["ville"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["region"] = getattr(adresse_obj, "Region", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["region"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["pays"] = getattr(adresse_obj, "Pays", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["pays"] = ""
|
||||||
|
else:
|
||||||
|
data["adresse"] = ""
|
||||||
|
data["complement"] = ""
|
||||||
|
data["code_postal"] = ""
|
||||||
|
data["ville"] = ""
|
||||||
|
data["region"] = ""
|
||||||
|
data["pays"] = ""
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur adresse sur {numero}: {e}")
|
||||||
|
data["adresse"] = ""
|
||||||
|
data["complement"] = ""
|
||||||
|
data["code_postal"] = ""
|
||||||
|
data["ville"] = ""
|
||||||
|
data["region"] = ""
|
||||||
|
data["pays"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
telecom = getattr(client_obj, "Telecom", None)
|
||||||
|
if telecom:
|
||||||
|
try:
|
||||||
|
data["telephone"] = getattr(telecom, "Telephone", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["telephone"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["portable"] = getattr(telecom, "Portable", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["portable"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["telecopie"] = getattr(telecom, "Telecopie", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["telecopie"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["email"] = getattr(telecom, "EMail", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["email"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
site = (
|
||||||
|
getattr(telecom, "Site", None)
|
||||||
|
or getattr(telecom, "Web", None)
|
||||||
|
or getattr(telecom, "SiteWeb", "")
|
||||||
|
)
|
||||||
|
data["site_web"] = str(site).strip() if site else ""
|
||||||
|
except Exception:
|
||||||
|
data["site_web"] = ""
|
||||||
|
else:
|
||||||
|
data["telephone"] = ""
|
||||||
|
data["portable"] = ""
|
||||||
|
data["telecopie"] = ""
|
||||||
|
data["email"] = ""
|
||||||
|
data["site_web"] = ""
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur telecom sur {numero}: {e}")
|
||||||
|
data["telephone"] = ""
|
||||||
|
data["portable"] = ""
|
||||||
|
data["telecopie"] = ""
|
||||||
|
data["email"] = ""
|
||||||
|
data["site_web"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["siret"] = getattr(client_obj, "CT_Siret", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["siret"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["siren"] = getattr(client_obj, "CT_Siren", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["siren"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["tva_intra"] = getattr(client_obj, "CT_Identifiant", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["tva_intra"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["code_naf"] = (
|
||||||
|
getattr(client_obj, "CT_CodeNAF", "").strip()
|
||||||
|
or getattr(client_obj, "CT_APE", "").strip()
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
data["code_naf"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["secteur"] = getattr(client_obj, "CT_Secteur", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["secteur"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
effectif = getattr(client_obj, "CT_Effectif", None)
|
||||||
|
data["effectif"] = int(effectif) if effectif is not None else None
|
||||||
|
except Exception:
|
||||||
|
data["effectif"] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ca = getattr(client_obj, "CT_ChiffreAffaire", None)
|
||||||
|
data["ca_annuel"] = float(ca) if ca is not None else None
|
||||||
|
except Exception:
|
||||||
|
data["ca_annuel"] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["commercial_code"] = getattr(client_obj, "CO_No", "").strip()
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
data["commercial_code"] = getattr(
|
||||||
|
client_obj, "CT_Commercial", ""
|
||||||
|
).strip()
|
||||||
|
except Exception:
|
||||||
|
data["commercial_code"] = ""
|
||||||
|
|
||||||
|
if data.get("commercial_code"):
|
||||||
|
try:
|
||||||
|
commercial_obj = getattr(client_obj, "Commercial", None)
|
||||||
|
if commercial_obj:
|
||||||
|
commercial_obj.Read()
|
||||||
|
data["commercial_nom"] = getattr(
|
||||||
|
commercial_obj, "CO_Nom", ""
|
||||||
|
).strip()
|
||||||
|
else:
|
||||||
|
data["commercial_nom"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["commercial_nom"] = ""
|
||||||
|
else:
|
||||||
|
data["commercial_nom"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["categorie_tarifaire"] = getattr(client_obj, "N_CatTarif", None)
|
||||||
|
except Exception:
|
||||||
|
data["categorie_tarifaire"] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["categorie_comptable"] = getattr(client_obj, "N_CatCompta", None)
|
||||||
|
except Exception:
|
||||||
|
data["categorie_comptable"] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["encours_autorise"] = float(getattr(client_obj, "CT_Encours", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["encours_autorise"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["assurance_credit"] = float(getattr(client_obj, "CT_Assurance", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["assurance_credit"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["compte_general"] = getattr(client_obj, "CG_Num", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["compte_general"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
date_creation = getattr(client_obj, "CT_DateCreate", None)
|
||||||
|
data["date_creation"] = str(date_creation) if date_creation else ""
|
||||||
|
except Exception:
|
||||||
|
data["date_creation"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
date_modif = getattr(client_obj, "CT_DateModif", None)
|
||||||
|
data["date_modification"] = str(date_modif) if date_modif else ""
|
||||||
|
except Exception:
|
||||||
|
data["date_modification"] = ""
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" ERREUR GLOBALE _extraire_client: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["_extraire_client", "_cast_client"]
|
||||||
0
utils/tiers/contacts/__init__.py
Normal file
0
utils/tiers/contacts/__init__.py
Normal file
168
utils/tiers/contacts/contacts.py
Normal file
168
utils/tiers/contacts/contacts.py
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
from typing import Dict, Optional
|
||||||
|
from utils.functions.items_to_dict import contact_to_dict
|
||||||
|
import logging
|
||||||
|
from utils.functions.functions import _safe_strip
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def _get_contacts(numero: str, conn) -> list:
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
CT_Num, CT_No, N_Contact,
|
||||||
|
CT_Civilite, CT_Nom, CT_Prenom, CT_Fonction,
|
||||||
|
N_Service,
|
||||||
|
CT_Telephone, CT_TelPortable, CT_Telecopie, CT_EMail,
|
||||||
|
CT_Facebook, CT_LinkedIn, CT_Skype
|
||||||
|
FROM F_CONTACTT
|
||||||
|
WHERE CT_Num = ?
|
||||||
|
ORDER BY N_Contact, CT_Nom, CT_Prenom
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(query, [numero])
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
query_client = """
|
||||||
|
SELECT CT_Contact
|
||||||
|
FROM F_COMPTET
|
||||||
|
WHERE CT_Num = ?
|
||||||
|
"""
|
||||||
|
cursor.execute(query_client, [numero])
|
||||||
|
client_row = cursor.fetchone()
|
||||||
|
|
||||||
|
nom_contact_defaut = None
|
||||||
|
if client_row:
|
||||||
|
nom_contact_defaut = _safe_strip(client_row.CT_Contact)
|
||||||
|
|
||||||
|
contacts = []
|
||||||
|
for row in rows:
|
||||||
|
contact = contact_to_dict(row)
|
||||||
|
|
||||||
|
if nom_contact_defaut:
|
||||||
|
nom_complet = f"{contact.get('prenom', '')} {contact['nom']}".strip()
|
||||||
|
contact["est_defaut"] = (
|
||||||
|
nom_complet == nom_contact_defaut or
|
||||||
|
contact['nom'] == nom_contact_defaut
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
contact["est_defaut"] = False
|
||||||
|
|
||||||
|
contacts.append(contact)
|
||||||
|
|
||||||
|
return contacts
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f" Impossible de récupérer contacts pour {numero}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _chercher_contact_en_base(
|
||||||
|
conn,
|
||||||
|
numero_client: str,
|
||||||
|
nom: str,
|
||||||
|
prenom: Optional[str] = None
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
if prenom:
|
||||||
|
query = """
|
||||||
|
SELECT TOP 1 CT_No, N_Contact
|
||||||
|
FROM F_CONTACTT
|
||||||
|
WHERE CT_Num = ?
|
||||||
|
AND LTRIM(RTRIM(CT_Nom)) = ?
|
||||||
|
AND LTRIM(RTRIM(CT_Prenom)) = ?
|
||||||
|
ORDER BY CT_No DESC
|
||||||
|
"""
|
||||||
|
cursor.execute(query, (numero_client.upper(), nom.strip(), prenom.strip()))
|
||||||
|
else:
|
||||||
|
query = """
|
||||||
|
SELECT TOP 1 CT_No, N_Contact
|
||||||
|
FROM F_CONTACTT
|
||||||
|
WHERE CT_Num = ?
|
||||||
|
AND LTRIM(RTRIM(CT_Nom)) = ?
|
||||||
|
AND (CT_Prenom IS NULL OR LTRIM(RTRIM(CT_Prenom)) = '')
|
||||||
|
ORDER BY CT_No DESC
|
||||||
|
"""
|
||||||
|
cursor.execute(query, (numero_client.upper(), nom.strip()))
|
||||||
|
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if row:
|
||||||
|
return {
|
||||||
|
"contact_numero": row.CT_No,
|
||||||
|
"n_contact": row.N_Contact
|
||||||
|
}
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Erreur recherche contact en base: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _lire_contact_depuis_base(
|
||||||
|
conn,
|
||||||
|
numero_client: str,
|
||||||
|
contact_no: int
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
CT_Num, CT_No, N_Contact,
|
||||||
|
CT_Civilite, CT_Nom, CT_Prenom, CT_Fonction,
|
||||||
|
N_Service,
|
||||||
|
CT_Telephone, CT_TelPortable, CT_Telecopie, CT_EMail,
|
||||||
|
CT_Facebook, CT_LinkedIn, CT_Skype
|
||||||
|
FROM F_CONTACTT
|
||||||
|
WHERE CT_Num = ? AND CT_No = ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f" Execution SQL: CT_Num='{numero_client.upper()}', CT_No={contact_no}")
|
||||||
|
cursor.execute(query, (numero_client.upper(), contact_no))
|
||||||
|
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
logger.warning(f" Aucune ligne retournée pour CT_Num={numero_client.upper()}, CT_No={contact_no}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(f" Ligne SQL trouvée: Nom={row.CT_Nom}, Prenom={row.CT_Prenom}")
|
||||||
|
|
||||||
|
civilite_map = {0: "M.", 1: "Mme", 2: "Mlle", 3: "Société"}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"numero": _safe_strip(row.CT_Num),
|
||||||
|
"contact_numero": row.CT_No,
|
||||||
|
"n_contact": row.N_Contact,
|
||||||
|
"civilite": civilite_map.get(row.CT_Civilite, None),
|
||||||
|
"nom": _safe_strip(row.CT_Nom),
|
||||||
|
"prenom": _safe_strip(row.CT_Prenom),
|
||||||
|
"fonction": _safe_strip(row.CT_Fonction),
|
||||||
|
"service_code": row.N_Service,
|
||||||
|
"telephone": _safe_strip(row.CT_Telephone),
|
||||||
|
"portable": _safe_strip(row.CT_TelPortable),
|
||||||
|
"telecopie": _safe_strip(row.CT_Telecopie),
|
||||||
|
"email": _safe_strip(row.CT_EMail),
|
||||||
|
"facebook": _safe_strip(row.CT_Facebook),
|
||||||
|
"linkedin": _safe_strip(row.CT_LinkedIn),
|
||||||
|
"skype": _safe_strip(row.CT_Skype),
|
||||||
|
"est_defaut": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f" Dict construit: numero={result['numero']}, nom={result['nom']}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" Exception dans : {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_get_contacts",
|
||||||
|
"_chercher_contact_en_base",
|
||||||
|
"_lire_contact_depuis_base"
|
||||||
|
]
|
||||||
0
utils/tiers/fournisseurs/__init__.py
Normal file
0
utils/tiers/fournisseurs/__init__.py
Normal file
362
utils/tiers/fournisseurs/fournisseurs_extraction.py
Normal file
362
utils/tiers/fournisseurs/fournisseurs_extraction.py
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
import win32com.client
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _extraire_fournisseur_enrichi(fourn_obj):
|
||||||
|
try:
|
||||||
|
numero = getattr(fourn_obj, "CT_Num", "").strip()
|
||||||
|
if not numero:
|
||||||
|
return None
|
||||||
|
|
||||||
|
intitule = getattr(fourn_obj, "CT_Intitule", "").strip()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"numero": numero,
|
||||||
|
"intitule": intitule,
|
||||||
|
"type": 1,
|
||||||
|
"est_fournisseur": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
sommeil = getattr(fourn_obj, "CT_Sommeil", 0)
|
||||||
|
data["est_actif"] = sommeil == 0
|
||||||
|
data["en_sommeil"] = sommeil == 1
|
||||||
|
except Exception:
|
||||||
|
data["est_actif"] = True
|
||||||
|
data["en_sommeil"] = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
adresse_obj = getattr(fourn_obj, "Adresse", None)
|
||||||
|
if adresse_obj:
|
||||||
|
data["adresse"] = getattr(adresse_obj, "Adresse", "").strip()
|
||||||
|
data["complement"] = getattr(adresse_obj, "Complement", "").strip()
|
||||||
|
data["code_postal"] = getattr(adresse_obj, "CodePostal", "").strip()
|
||||||
|
data["ville"] = getattr(adresse_obj, "Ville", "").strip()
|
||||||
|
data["region"] = getattr(adresse_obj, "Region", "").strip()
|
||||||
|
data["pays"] = getattr(adresse_obj, "Pays", "").strip()
|
||||||
|
|
||||||
|
parties_adresse = []
|
||||||
|
if data["adresse"]:
|
||||||
|
parties_adresse.append(data["adresse"])
|
||||||
|
if data["complement"]:
|
||||||
|
parties_adresse.append(data["complement"])
|
||||||
|
if data["code_postal"] or data["ville"]:
|
||||||
|
ville_cp = f"{data['code_postal']} {data['ville']}".strip()
|
||||||
|
if ville_cp:
|
||||||
|
parties_adresse.append(ville_cp)
|
||||||
|
if data["pays"]:
|
||||||
|
parties_adresse.append(data["pays"])
|
||||||
|
|
||||||
|
data["adresse_complete"] = ", ".join(parties_adresse)
|
||||||
|
else:
|
||||||
|
data["adresse"] = ""
|
||||||
|
data["complement"] = ""
|
||||||
|
data["code_postal"] = ""
|
||||||
|
data["ville"] = ""
|
||||||
|
data["region"] = ""
|
||||||
|
data["pays"] = ""
|
||||||
|
data["adresse_complete"] = ""
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur adresse fournisseur {numero}: {e}")
|
||||||
|
data["adresse"] = ""
|
||||||
|
data["complement"] = ""
|
||||||
|
data["code_postal"] = ""
|
||||||
|
data["ville"] = ""
|
||||||
|
data["region"] = ""
|
||||||
|
data["pays"] = ""
|
||||||
|
data["adresse_complete"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
telecom_obj = getattr(fourn_obj, "Telecom", None)
|
||||||
|
if telecom_obj:
|
||||||
|
data["telephone"] = getattr(telecom_obj, "Telephone", "").strip()
|
||||||
|
data["portable"] = getattr(telecom_obj, "Portable", "").strip()
|
||||||
|
data["telecopie"] = getattr(telecom_obj, "Telecopie", "").strip()
|
||||||
|
data["email"] = getattr(telecom_obj, "EMail", "").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
site = (
|
||||||
|
getattr(telecom_obj, "Site", None)
|
||||||
|
or getattr(telecom_obj, "Web", None)
|
||||||
|
or getattr(telecom_obj, "SiteWeb", "")
|
||||||
|
)
|
||||||
|
data["site_web"] = str(site).strip() if site else ""
|
||||||
|
except Exception:
|
||||||
|
data["site_web"] = ""
|
||||||
|
else:
|
||||||
|
data["telephone"] = ""
|
||||||
|
data["portable"] = ""
|
||||||
|
data["telecopie"] = ""
|
||||||
|
data["email"] = ""
|
||||||
|
data["site_web"] = ""
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur telecom fournisseur {numero}: {e}")
|
||||||
|
data["telephone"] = ""
|
||||||
|
data["portable"] = ""
|
||||||
|
data["telecopie"] = ""
|
||||||
|
data["email"] = ""
|
||||||
|
data["site_web"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["siret"] = getattr(fourn_obj, "CT_Siret", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["siret"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if data["siret"] and len(data["siret"]) >= 9:
|
||||||
|
data["siren"] = data["siret"][:9]
|
||||||
|
else:
|
||||||
|
data["siren"] = getattr(fourn_obj, "CT_Siren", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["siren"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["tva_intra"] = getattr(fourn_obj, "CT_Identifiant", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["tva_intra"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["code_naf"] = (
|
||||||
|
getattr(fourn_obj, "CT_CodeNAF", "").strip()
|
||||||
|
or getattr(fourn_obj, "CT_APE", "").strip()
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
data["code_naf"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["forme_juridique"] = getattr(
|
||||||
|
fourn_obj, "CT_FormeJuridique", ""
|
||||||
|
).strip()
|
||||||
|
except Exception:
|
||||||
|
data["forme_juridique"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
cat_tarif = getattr(fourn_obj, "N_CatTarif", None)
|
||||||
|
data["categorie_tarifaire"] = (
|
||||||
|
int(cat_tarif) if cat_tarif is not None else None
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
data["categorie_tarifaire"] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
cat_compta = getattr(fourn_obj, "N_CatCompta", None)
|
||||||
|
data["categorie_comptable"] = (
|
||||||
|
int(cat_compta) if cat_compta is not None else None
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
data["categorie_comptable"] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
cond_regl = getattr(fourn_obj, "CT_CondRegl", "").strip()
|
||||||
|
data["conditions_reglement_code"] = cond_regl
|
||||||
|
|
||||||
|
if cond_regl:
|
||||||
|
try:
|
||||||
|
cond_obj = getattr(fourn_obj, "ConditionReglement", None)
|
||||||
|
if cond_obj:
|
||||||
|
cond_obj.Read()
|
||||||
|
data["conditions_reglement_libelle"] = getattr(
|
||||||
|
cond_obj, "C_Intitule", ""
|
||||||
|
).strip()
|
||||||
|
else:
|
||||||
|
data["conditions_reglement_libelle"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["conditions_reglement_libelle"] = ""
|
||||||
|
else:
|
||||||
|
data["conditions_reglement_libelle"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["conditions_reglement_code"] = ""
|
||||||
|
data["conditions_reglement_libelle"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
mode_regl = getattr(fourn_obj, "CT_ModeRegl", "").strip()
|
||||||
|
data["mode_reglement_code"] = mode_regl
|
||||||
|
|
||||||
|
if mode_regl:
|
||||||
|
try:
|
||||||
|
mode_obj = getattr(fourn_obj, "ModeReglement", None)
|
||||||
|
if mode_obj:
|
||||||
|
mode_obj.Read()
|
||||||
|
data["mode_reglement_libelle"] = getattr(
|
||||||
|
mode_obj, "M_Intitule", ""
|
||||||
|
).strip()
|
||||||
|
else:
|
||||||
|
data["mode_reglement_libelle"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["mode_reglement_libelle"] = ""
|
||||||
|
else:
|
||||||
|
data["mode_reglement_libelle"] = ""
|
||||||
|
except Exception:
|
||||||
|
data["mode_reglement_code"] = ""
|
||||||
|
data["mode_reglement_libelle"] = ""
|
||||||
|
|
||||||
|
data["coordonnees_bancaires"] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
factory_banque = getattr(fourn_obj, "FactoryBanque", None)
|
||||||
|
|
||||||
|
if factory_banque:
|
||||||
|
index = 1
|
||||||
|
while index <= 5:
|
||||||
|
try:
|
||||||
|
banque_persist = factory_banque.List(index)
|
||||||
|
if banque_persist is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
banque = win32com.client.CastTo(banque_persist, "IBOBanque3")
|
||||||
|
banque.Read()
|
||||||
|
|
||||||
|
compte_bancaire = {
|
||||||
|
"banque_nom": getattr(banque, "BI_Intitule", "").strip(),
|
||||||
|
"iban": getattr(banque, "RIB_Iban", "").strip(),
|
||||||
|
"bic": getattr(banque, "RIB_Bic", "").strip(),
|
||||||
|
"code_banque": getattr(banque, "RIB_Banque", "").strip(),
|
||||||
|
"code_guichet": getattr(banque, "RIB_Guichet", "").strip(),
|
||||||
|
"numero_compte": getattr(banque, "RIB_Compte", "").strip(),
|
||||||
|
"cle_rib": getattr(banque, "RIB_Cle", "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if compte_bancaire["iban"] or compte_bancaire["numero_compte"]:
|
||||||
|
data["coordonnees_bancaires"].append(compte_bancaire)
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur coordonnées bancaires fournisseur {numero}: {e}")
|
||||||
|
|
||||||
|
if data["coordonnees_bancaires"]:
|
||||||
|
data["iban_principal"] = data["coordonnees_bancaires"][0].get("iban", "")
|
||||||
|
data["bic_principal"] = data["coordonnees_bancaires"][0].get("bic", "")
|
||||||
|
else:
|
||||||
|
data["iban_principal"] = ""
|
||||||
|
data["bic_principal"] = ""
|
||||||
|
|
||||||
|
data["contacts"] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
factory_contact = getattr(fourn_obj, "FactoryContact", None)
|
||||||
|
|
||||||
|
if factory_contact:
|
||||||
|
index = 1
|
||||||
|
while index <= 20:
|
||||||
|
try:
|
||||||
|
contact_persist = factory_contact.List(index)
|
||||||
|
if contact_persist is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
contact = win32com.client.CastTo(contact_persist, "IBOContact3")
|
||||||
|
contact.Read()
|
||||||
|
|
||||||
|
contact_data = {
|
||||||
|
"nom": getattr(contact, "CO_Nom", "").strip(),
|
||||||
|
"prenom": getattr(contact, "CO_Prenom", "").strip(),
|
||||||
|
"fonction": getattr(contact, "CO_Fonction", "").strip(),
|
||||||
|
"service": getattr(contact, "CO_Service", "").strip(),
|
||||||
|
"telephone": getattr(contact, "CO_Telephone", "").strip(),
|
||||||
|
"portable": getattr(contact, "CO_Portable", "").strip(),
|
||||||
|
"email": getattr(contact, "CO_EMail", "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
nom_complet = (
|
||||||
|
f"{contact_data['prenom']} {contact_data['nom']}".strip()
|
||||||
|
)
|
||||||
|
if nom_complet:
|
||||||
|
contact_data["nom_complet"] = nom_complet
|
||||||
|
else:
|
||||||
|
contact_data["nom_complet"] = contact_data["nom"]
|
||||||
|
|
||||||
|
if contact_data["nom"]:
|
||||||
|
data["contacts"].append(contact_data)
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur contacts fournisseur {numero}: {e}")
|
||||||
|
|
||||||
|
data["nb_contacts"] = len(data["contacts"])
|
||||||
|
|
||||||
|
if data["contacts"]:
|
||||||
|
data["contact_principal"] = data["contacts"][0]
|
||||||
|
else:
|
||||||
|
data["contact_principal"] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["encours_autorise"] = float(getattr(fourn_obj, "CT_Encours", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["encours_autorise"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["ca_annuel"] = float(getattr(fourn_obj, "CT_ChiffreAffaire", 0.0))
|
||||||
|
except Exception:
|
||||||
|
data["ca_annuel"] = 0.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
data["compte_general"] = getattr(fourn_obj, "CG_Num", "").strip()
|
||||||
|
except Exception:
|
||||||
|
data["compte_general"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
date_creation = getattr(fourn_obj, "CT_DateCreate", None)
|
||||||
|
data["date_creation"] = str(date_creation) if date_creation else ""
|
||||||
|
except Exception:
|
||||||
|
data["date_creation"] = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
date_modif = getattr(fourn_obj, "CT_DateModif", None)
|
||||||
|
data["date_modification"] = str(date_modif) if date_modif else ""
|
||||||
|
except Exception:
|
||||||
|
data["date_modification"] = ""
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f" Erreur extraction fournisseur: {e}", exc_info=True)
|
||||||
|
return {
|
||||||
|
"numero": getattr(fourn_obj, "CT_Num", "").strip(),
|
||||||
|
"intitule": getattr(fourn_obj, "CT_Intitule", "").strip(),
|
||||||
|
"type": 1,
|
||||||
|
"est_fournisseur": True,
|
||||||
|
"est_actif": True,
|
||||||
|
"en_sommeil": False,
|
||||||
|
"adresse": "",
|
||||||
|
"complement": "",
|
||||||
|
"code_postal": "",
|
||||||
|
"ville": "",
|
||||||
|
"region": "",
|
||||||
|
"pays": "",
|
||||||
|
"adresse_complete": "",
|
||||||
|
"telephone": "",
|
||||||
|
"portable": "",
|
||||||
|
"telecopie": "",
|
||||||
|
"email": "",
|
||||||
|
"site_web": "",
|
||||||
|
"siret": "",
|
||||||
|
"siren": "",
|
||||||
|
"tva_intra": "",
|
||||||
|
"code_naf": "",
|
||||||
|
"forme_juridique": "",
|
||||||
|
"categorie_tarifaire": None,
|
||||||
|
"categorie_comptable": None,
|
||||||
|
"conditions_reglement_code": "",
|
||||||
|
"conditions_reglement_libelle": "",
|
||||||
|
"mode_reglement_code": "",
|
||||||
|
"mode_reglement_libelle": "",
|
||||||
|
"iban_principal": "",
|
||||||
|
"bic_principal": "",
|
||||||
|
"coordonnees_bancaires": [],
|
||||||
|
"contacts": [],
|
||||||
|
"nb_contacts": 0,
|
||||||
|
"contact_principal": None,
|
||||||
|
"encours_autorise": 0.0,
|
||||||
|
"ca_annuel": 0.0,
|
||||||
|
"compte_general": "",
|
||||||
|
"date_creation": "",
|
||||||
|
"date_modification": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["_extraire_fournisseur_enrichi"]
|
||||||
81
utils/tiers/tiers_data_sql.py
Normal file
81
utils/tiers/tiers_data_sql.py
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
def _build_tiers_select_query():
|
||||||
|
return """
|
||||||
|
SELECT
|
||||||
|
-- IDENTIFICATION TIERS (9)
|
||||||
|
t.CT_Num, t.CT_Intitule, t.CT_Type, t.CT_Qualite,
|
||||||
|
t.CT_Classement, t.CT_Raccourci, t.CT_Siret, t.CT_Identifiant,
|
||||||
|
t.CT_Ape,
|
||||||
|
|
||||||
|
-- ADRESSE TIERS (7)
|
||||||
|
t.CT_Contact, t.CT_Adresse, t.CT_Complement,
|
||||||
|
t.CT_CodePostal, t.CT_Ville, t.CT_CodeRegion, t.CT_Pays,
|
||||||
|
|
||||||
|
-- TELECOM TIERS (6)
|
||||||
|
t.CT_Telephone, t.CT_Telecopie, t.CT_EMail, t.CT_Site,
|
||||||
|
t.CT_Facebook, t.CT_LinkedIn,
|
||||||
|
|
||||||
|
-- TAUX TIERS (4)
|
||||||
|
t.CT_Taux01, t.CT_Taux02, t.CT_Taux03, t.CT_Taux04,
|
||||||
|
|
||||||
|
-- STATISTIQUES TIERS (10)
|
||||||
|
t.CT_Statistique01, t.CT_Statistique02, t.CT_Statistique03,
|
||||||
|
t.CT_Statistique04, t.CT_Statistique05, t.CT_Statistique06,
|
||||||
|
t.CT_Statistique07, t.CT_Statistique08, t.CT_Statistique09,
|
||||||
|
t.CT_Statistique10,
|
||||||
|
|
||||||
|
-- COMMERCIAL TIERS (4)
|
||||||
|
t.CT_Encours, t.CT_Assurance, t.CT_Langue, t.CO_No,
|
||||||
|
|
||||||
|
-- FACTURATION TIERS (11)
|
||||||
|
t.CT_Lettrage, t.CT_Sommeil, t.CT_Facture, t.CT_Prospect,
|
||||||
|
t.CT_BLFact, t.CT_Saut, t.CT_ValidEch, t.CT_ControlEnc,
|
||||||
|
t.CT_NotRappel, t.CT_NotPenal, t.CT_BonAPayer,
|
||||||
|
|
||||||
|
-- LOGISTIQUE TIERS (4)
|
||||||
|
t.CT_PrioriteLivr, t.CT_LivrPartielle,
|
||||||
|
t.CT_DelaiTransport, t.CT_DelaiAppro,
|
||||||
|
|
||||||
|
-- COMMENTAIRE TIERS (1)
|
||||||
|
t.CT_Commentaire,
|
||||||
|
|
||||||
|
-- ANALYTIQUE TIERS (1)
|
||||||
|
t.CA_Num,
|
||||||
|
|
||||||
|
-- ORGANISATION / SURVEILLANCE TIERS (10)
|
||||||
|
t.MR_No, t.CT_Surveillance, t.CT_Coface,
|
||||||
|
t.CT_SvFormeJuri, t.CT_SvEffectif, t.CT_SvRegul,
|
||||||
|
t.CT_SvCotation, t.CT_SvObjetMaj, t.CT_SvCA, t.CT_SvResultat,
|
||||||
|
|
||||||
|
-- COMPTE GENERAL ET CATEGORIES TIERS (3)
|
||||||
|
t.CG_NumPrinc, t.N_CatTarif, t.N_CatCompta,
|
||||||
|
|
||||||
|
-- COLLABORATEUR (23 champs)
|
||||||
|
c.CO_No AS Collab_CO_No,
|
||||||
|
c.CO_Nom AS Collab_CO_Nom,
|
||||||
|
c.CO_Prenom AS Collab_CO_Prenom,
|
||||||
|
c.CO_Fonction AS Collab_CO_Fonction,
|
||||||
|
c.CO_Adresse AS Collab_CO_Adresse,
|
||||||
|
c.CO_Complement AS Collab_CO_Complement,
|
||||||
|
c.CO_CodePostal AS Collab_CO_CodePostal,
|
||||||
|
c.CO_Ville AS Collab_CO_Ville,
|
||||||
|
c.CO_CodeRegion AS Collab_CO_CodeRegion,
|
||||||
|
c.CO_Pays AS Collab_CO_Pays,
|
||||||
|
c.CO_Service AS Collab_CO_Service,
|
||||||
|
c.CO_Vendeur AS Collab_CO_Vendeur,
|
||||||
|
c.CO_Caissier AS Collab_CO_Caissier,
|
||||||
|
c.CO_Acheteur AS Collab_CO_Acheteur,
|
||||||
|
c.CO_Telephone AS Collab_CO_Telephone,
|
||||||
|
c.CO_Telecopie AS Collab_CO_Telecopie,
|
||||||
|
c.CO_EMail AS Collab_CO_EMail,
|
||||||
|
c.CO_TelPortable AS Collab_CO_TelPortable,
|
||||||
|
c.CO_Matricule AS Collab_CO_Matricule,
|
||||||
|
c.CO_Facebook AS Collab_CO_Facebook,
|
||||||
|
c.CO_LinkedIn AS Collab_CO_LinkedIn,
|
||||||
|
c.CO_Skype AS Collab_CO_Skype,
|
||||||
|
c.CO_Sommeil AS Collab_CO_Sommeil,
|
||||||
|
c.CO_ChefVentes AS Collab_CO_ChefVentes,
|
||||||
|
c.CO_NoChefVentes AS Collab_CO_NoChefVentes
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["_build_tiers_select_query"]
|
||||||
Loading…
Reference in a new issue