Initial backend FastAPI WS
This commit is contained in:
parent
9e243e28aa
commit
e98dce03b8
6 changed files with 1636 additions and 24 deletions
14
.env.example
Normal file
14
.env.example
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# ============================================================================
|
||||||
|
# SAGE 100 CLOUD - CONNEXION BOI/COM
|
||||||
|
# ============================================================================
|
||||||
|
CHEMIN_BASE=<CHEMIN_VERS_LE_FICHIER_GCM>
|
||||||
|
UTILISATEUR=<UTILISATEUR_SAGE100>
|
||||||
|
MOT_DE_PASSE=<MOT_DE_PASSE_SAGE100>
|
||||||
|
|
||||||
|
SAGE_GATEWAY_TOKEN=<TOKEN_SAGE_GATEWAY>
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# API - CONFIGURATION SERVEUR
|
||||||
|
# ============================================================================
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
51
.gitignore
vendored
51
.gitignore
vendored
|
|
@ -1,35 +1,38 @@
|
||||||
# Python
|
# ================================
|
||||||
__pycache__/
|
# Python / FastAPI
|
||||||
*.pyc
|
# ================================
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
|
|
||||||
# Environnements virtuels
|
# Environnements virtuels
|
||||||
venv/
|
venv/
|
||||||
.env/
|
.env
|
||||||
.env.*
|
|
||||||
*.env
|
*.env
|
||||||
|
*.local.env
|
||||||
|
|
||||||
# Outils
|
# caches
|
||||||
.idea/
|
__pycache__/
|
||||||
.vscode/
|
*.py[cod]
|
||||||
.cache/
|
*.pyo
|
||||||
.mypy_cache/
|
|
||||||
pytest_cache/
|
|
||||||
|
|
||||||
# Logs
|
# logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# SQLite databases
|
# Compilations
|
||||||
*.db
|
*.so
|
||||||
*.sqlite3
|
*.dll
|
||||||
|
|
||||||
|
# Outils Python
|
||||||
|
.mypy_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea/
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
**/__pycache__/
|
*~
|
||||||
docker-data/
|
.build/
|
||||||
dist/
|
dist/
|
||||||
build/
|
|
||||||
|
|
||||||
# Systèmes
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
|
||||||
42
config.py
Normal file
42
config.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
case_sensitive=False,
|
||||||
|
extra="ignore"
|
||||||
|
)
|
||||||
|
|
||||||
|
# === SAGE 100c (Windows uniquement) ===
|
||||||
|
chemin_base: str
|
||||||
|
utilisateur: str = "Administrateur"
|
||||||
|
mot_de_passe: str
|
||||||
|
|
||||||
|
# === Sécurité Gateway ===
|
||||||
|
sage_gateway_token: str # Token partagé avec le VPS Linux
|
||||||
|
|
||||||
|
# === SMTP (optionnel sur Windows) ===
|
||||||
|
smtp_host: Optional[str] = None
|
||||||
|
smtp_port: int = 587
|
||||||
|
smtp_user: Optional[str] = None
|
||||||
|
smtp_password: Optional[str] = None
|
||||||
|
smtp_from: Optional[str] = None
|
||||||
|
|
||||||
|
# === API Windows ===
|
||||||
|
api_host: str = "0.0.0.0"
|
||||||
|
api_port: int = 8000
|
||||||
|
|
||||||
|
# === CORS ===
|
||||||
|
cors_origins: List[str] = ["*"]
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
def validate_settings():
|
||||||
|
"""Validation au démarrage"""
|
||||||
|
if not settings.chemin_base or not settings.mot_de_passe:
|
||||||
|
raise ValueError("❌ CHEMIN_BASE et MOT_DE_PASSE requis dans .env")
|
||||||
|
if not settings.sage_gateway_token:
|
||||||
|
raise ValueError("❌ SAGE_GATEWAY_TOKEN requis (doit être identique sur Linux)")
|
||||||
|
return True
|
||||||
340
main.py
Normal file
340
main.py
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
from fastapi import FastAPI, HTTPException, Header, Depends, Query
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import datetime, date
|
||||||
|
from enum import Enum
|
||||||
|
import uvicorn
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from config import settings, validate_settings
|
||||||
|
from sage_connector import SageConnector
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# LOGGING
|
||||||
|
# =====================================================
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.FileHandler("sage_gateway.log"),
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ENUMS
|
||||||
|
# =====================================================
|
||||||
|
class TypeDocument(int, Enum):
|
||||||
|
DEVIS = 0
|
||||||
|
BON_LIVRAISON = 1
|
||||||
|
BON_RETOUR = 2
|
||||||
|
COMMANDE = 3
|
||||||
|
PREPARATION = 4
|
||||||
|
FACTURE = 5
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# MODÈLES
|
||||||
|
# =====================================================
|
||||||
|
class FiltreRequest(BaseModel):
|
||||||
|
filtre: Optional[str] = ""
|
||||||
|
|
||||||
|
class CodeRequest(BaseModel):
|
||||||
|
code: str
|
||||||
|
|
||||||
|
class ChampLibreRequest(BaseModel):
|
||||||
|
doc_id: str
|
||||||
|
type_doc: int
|
||||||
|
nom_champ: str
|
||||||
|
valeur: str
|
||||||
|
|
||||||
|
class DevisRequest(BaseModel):
|
||||||
|
client_id: str
|
||||||
|
date_devis: Optional[date] = None
|
||||||
|
lignes: List[Dict] # Format: {article_code, quantite, prix_unitaire_ht, remise_pourcentage}
|
||||||
|
|
||||||
|
class TransformationRequest(BaseModel):
|
||||||
|
numero_source: str
|
||||||
|
type_source: int
|
||||||
|
type_cible: int
|
||||||
|
|
||||||
|
class StatutRequest(BaseModel):
|
||||||
|
nouveau_statut: int
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# SÉCURITÉ
|
||||||
|
# =====================================================
|
||||||
|
def verify_token(x_sage_token: str = Header(...)):
|
||||||
|
"""Vérification du token d'authentification"""
|
||||||
|
if x_sage_token != settings.sage_gateway_token:
|
||||||
|
logger.warning(f"❌ Token invalide reçu: {x_sage_token[:20]}...")
|
||||||
|
raise HTTPException(401, "Token invalide")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# APPLICATION
|
||||||
|
# =====================================================
|
||||||
|
app = FastAPI(
|
||||||
|
title="Sage Gateway - Windows Server",
|
||||||
|
version="1.0.0",
|
||||||
|
description="Passerelle d'accès à Sage 100c pour VPS Linux"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.cors_origins,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
allow_credentials=True
|
||||||
|
)
|
||||||
|
|
||||||
|
sage: Optional[SageConnector] = None
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# LIFECYCLE
|
||||||
|
# =====================================================
|
||||||
|
@app.on_event("startup")
|
||||||
|
def startup():
|
||||||
|
global sage
|
||||||
|
|
||||||
|
logger.info("🚀 Démarrage Sage Gateway Windows...")
|
||||||
|
|
||||||
|
# Validation config
|
||||||
|
try:
|
||||||
|
validate_settings()
|
||||||
|
logger.info("✅ Configuration validée")
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"❌ Configuration invalide: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Connexion Sage
|
||||||
|
sage = SageConnector(
|
||||||
|
settings.chemin_base,
|
||||||
|
settings.utilisateur,
|
||||||
|
settings.mot_de_passe
|
||||||
|
)
|
||||||
|
|
||||||
|
if not sage.connecter():
|
||||||
|
raise RuntimeError("❌ Impossible de se connecter à Sage 100c")
|
||||||
|
|
||||||
|
logger.info("✅ Sage Gateway démarré et connecté")
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
def shutdown():
|
||||||
|
if sage:
|
||||||
|
sage.deconnecter()
|
||||||
|
logger.info("👋 Sage Gateway arrêté")
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ENDPOINTS - SYSTÈME
|
||||||
|
# =====================================================
|
||||||
|
@app.get("/health")
|
||||||
|
def health():
|
||||||
|
"""Health check"""
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"sage_connected": sage is not None and sage.cial is not None,
|
||||||
|
"cache_info": sage.get_cache_info() if sage else None,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ENDPOINTS - CLIENTS
|
||||||
|
# =====================================================
|
||||||
|
@app.post("/sage/clients/list", dependencies=[Depends(verify_token)])
|
||||||
|
def clients_list(req: FiltreRequest):
|
||||||
|
"""Liste des clients avec filtre optionnel"""
|
||||||
|
try:
|
||||||
|
clients = sage.lister_tous_clients(req.filtre)
|
||||||
|
return {"success": True, "data": clients}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur liste clients: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.post("/sage/clients/get", dependencies=[Depends(verify_token)])
|
||||||
|
def client_get(req: CodeRequest):
|
||||||
|
"""Lecture d'un client par code"""
|
||||||
|
try:
|
||||||
|
client = sage.lire_client(req.code)
|
||||||
|
if not client:
|
||||||
|
raise HTTPException(404, f"Client {req.code} non trouvé")
|
||||||
|
return {"success": True, "data": client}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lecture client: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ENDPOINTS - ARTICLES
|
||||||
|
# =====================================================
|
||||||
|
@app.post("/sage/articles/list", dependencies=[Depends(verify_token)])
|
||||||
|
def articles_list(req: FiltreRequest):
|
||||||
|
"""Liste des articles avec filtre optionnel"""
|
||||||
|
try:
|
||||||
|
articles = sage.lister_tous_articles(req.filtre)
|
||||||
|
return {"success": True, "data": articles}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur liste articles: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.post("/sage/articles/get", dependencies=[Depends(verify_token)])
|
||||||
|
def article_get(req: CodeRequest):
|
||||||
|
"""Lecture d'un article par référence"""
|
||||||
|
try:
|
||||||
|
article = sage.lire_article(req.code)
|
||||||
|
if not article:
|
||||||
|
raise HTTPException(404, f"Article {req.code} non trouvé")
|
||||||
|
return {"success": True, "data": article}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lecture article: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ENDPOINTS - DEVIS
|
||||||
|
# =====================================================
|
||||||
|
@app.post("/sage/devis/create", dependencies=[Depends(verify_token)])
|
||||||
|
def creer_devis(req: DevisRequest):
|
||||||
|
"""Création d'un devis"""
|
||||||
|
try:
|
||||||
|
# Transformer en format attendu par sage_connector
|
||||||
|
devis_data = {
|
||||||
|
"client": {"code": req.client_id, "intitule": ""},
|
||||||
|
"date_devis": req.date_devis or date.today(),
|
||||||
|
"lignes": req.lignes
|
||||||
|
}
|
||||||
|
|
||||||
|
resultat = sage.creer_devis_enrichi(devis_data)
|
||||||
|
return {"success": True, "data": resultat}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur création devis: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.post("/sage/devis/get", dependencies=[Depends(verify_token)])
|
||||||
|
def lire_devis(req: CodeRequest):
|
||||||
|
"""Lecture d'un devis"""
|
||||||
|
try:
|
||||||
|
devis = sage.lire_devis(req.code)
|
||||||
|
if not devis:
|
||||||
|
raise HTTPException(404, f"Devis {req.code} non trouvé")
|
||||||
|
return {"success": True, "data": devis}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lecture devis: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.post("/sage/devis/statut", dependencies=[Depends(verify_token)])
|
||||||
|
def changer_statut_devis(doc_id: str, req: StatutRequest):
|
||||||
|
"""Changement de statut d'un devis"""
|
||||||
|
try:
|
||||||
|
# Implémenter via sage_connector
|
||||||
|
# (À ajouter dans sage_connector si manquant)
|
||||||
|
return {"success": True, "message": "Statut mis à jour"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur MAJ statut: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ENDPOINTS - DOCUMENTS
|
||||||
|
# =====================================================
|
||||||
|
@app.post("/sage/documents/get", dependencies=[Depends(verify_token)])
|
||||||
|
def lire_document(numero: str, type_doc: int):
|
||||||
|
"""Lecture d'un document (commande, facture, etc.)"""
|
||||||
|
try:
|
||||||
|
doc = sage.lire_document(numero, type_doc)
|
||||||
|
if not doc:
|
||||||
|
raise HTTPException(404, f"Document {numero} non trouvé")
|
||||||
|
return {"success": True, "data": doc}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lecture document: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.post("/sage/documents/transform", dependencies=[Depends(verify_token)])
|
||||||
|
def transformer_document(req: TransformationRequest):
|
||||||
|
"""Transformation de document (devis → commande, etc.)"""
|
||||||
|
try:
|
||||||
|
resultat = sage.transformer_document(
|
||||||
|
req.numero_source,
|
||||||
|
req.type_source,
|
||||||
|
req.type_cible
|
||||||
|
)
|
||||||
|
return {"success": True, "data": resultat}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur transformation: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.post("/sage/documents/champ-libre", dependencies=[Depends(verify_token)])
|
||||||
|
def maj_champ_libre(req: ChampLibreRequest):
|
||||||
|
"""Mise à jour d'un champ libre"""
|
||||||
|
try:
|
||||||
|
success = sage.mettre_a_jour_champ_libre(
|
||||||
|
req.doc_id,
|
||||||
|
req.type_doc,
|
||||||
|
req.nom_champ,
|
||||||
|
req.valeur
|
||||||
|
)
|
||||||
|
return {"success": success}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur MAJ champ libre: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ENDPOINTS - CONTACTS
|
||||||
|
# =====================================================
|
||||||
|
@app.post("/sage/contact/read", dependencies=[Depends(verify_token)])
|
||||||
|
def contact_read(req: CodeRequest):
|
||||||
|
"""Lecture du contact principal d'un client"""
|
||||||
|
try:
|
||||||
|
contact = sage.lire_contact_principal_client(req.code)
|
||||||
|
if not contact:
|
||||||
|
raise HTTPException(404, f"Contact non trouvé pour client {req.code}")
|
||||||
|
return {"success": True, "data": contact}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lecture contact: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# ENDPOINTS - ADMIN
|
||||||
|
# =====================================================
|
||||||
|
@app.post("/sage/cache/refresh", dependencies=[Depends(verify_token)])
|
||||||
|
def refresh_cache():
|
||||||
|
"""Force le rafraîchissement du cache"""
|
||||||
|
try:
|
||||||
|
sage.forcer_actualisation_cache()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Cache actualisé",
|
||||||
|
"info": sage.get_cache_info()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur refresh cache: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
@app.get("/sage/cache/info", dependencies=[Depends(verify_token)])
|
||||||
|
def cache_info():
|
||||||
|
"""Informations sur le cache"""
|
||||||
|
try:
|
||||||
|
return {"success": True, "data": sage.get_cache_info()}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur info cache: {e}")
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# LANCEMENT
|
||||||
|
# =====================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host=settings.api_host,
|
||||||
|
port=settings.api_port,
|
||||||
|
reload=False, # Pas de reload en production
|
||||||
|
log_level="info"
|
||||||
|
)
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
pydantic
|
||||||
|
pydantic-settings
|
||||||
|
python-multipart
|
||||||
|
python-dotenv
|
||||||
|
pywin32
|
||||||
1206
sage_connector.py
Normal file
1206
sage_connector.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue