diff --git a/main.py b/main.py index 5a6b9ba..cc49111 100644 --- a/main.py +++ b/main.py @@ -204,8 +204,8 @@ def creer_devis(req: DevisRequest): try: devis_data = { "client": {"code": req.client_id, "intitule": ""}, - "date_devis": req.date_devis or date.today(), - "date_livraison": req.date_livraison or date.today(), + "date_devis": req.date_devis or datetime.now(), + "date_livraison": req.date_livraison or datetime.now(), "reference": req.reference, "lignes": req.lignes, } @@ -687,8 +687,8 @@ def creer_commande_endpoint(req: CommandeCreate): try: commande_data = { "client": {"code": req.client_id, "intitule": ""}, - "date_commande": req.date_commande or date.today(), - "date_livraison": req.date_livraison or date.today(), + "date_commande": req.date_commande or datetime.now(), + "date_livraison": req.date_livraison or datetime.now(), "reference": req.reference, "lignes": req.lignes, } @@ -727,8 +727,8 @@ def creer_livraison_endpoint(req: LivraisonCreate): livraison_data = { "client": {"code": req.client_id, "intitule": ""}, - "date_livraison": req.date_livraison or date.today(), - "date_livraison_prevue": req.date_livraison or date.today(), + "date_livraison": req.date_livraison or datetime.now(), + "date_livraison_prevue": req.date_livraison or datetime.now(), "reference": req.reference, "lignes": req.lignes, } @@ -767,8 +767,8 @@ def creer_avoir_endpoint(req: AvoirCreate): avoir_data = { "client": {"code": req.client_id, "intitule": ""}, - "date_avoir": req.date_avoir or date.today(), - "date_livraison": req.date_livraison or date.today(), + "date_avoir": req.date_avoir or datetime.now(), + "date_livraison": req.date_livraison or datetime.now(), "reference": req.reference, "lignes": req.lignes, } @@ -810,8 +810,8 @@ def creer_facture_endpoint(req: FactureCreate): facture_data = { "client": {"code": req.client_id, "intitule": ""}, - "date_facture": req.date_facture or date.today(), - "date_livraison": req.date_livraison or date.today(), + "date_facture": req.date_facture or datetime.now(), + "date_livraison": req.date_livraison or datetime.now(), "reference": req.reference, "lignes": req.lignes, } @@ -1177,7 +1177,7 @@ def creer_entree_stock(req: EntreeStock): ) entree_data = { - "date_mouvement": req.date_entree or date.today(), + "date_mouvement": req.date_entree or datetime.now(), "reference": req.reference, "depot_code": req.depot_code, "lignes": [ligne.dict() for ligne in req.lignes], @@ -1207,7 +1207,7 @@ def creer_sortie_stock(req: SortieStock): ) sortie_data = { - "date_mouvement": req.date_sortie or date.today(), + "date_mouvement": req.date_sortie or datetime.now(), "reference": req.reference, "depot_code": req.depot_code, "lignes": [ligne.dict() for ligne in req.lignes], diff --git a/schemas/documents/avoirs.py b/schemas/documents/avoirs.py index b66c2fd..81fb7c6 100644 --- a/schemas/documents/avoirs.py +++ b/schemas/documents/avoirs.py @@ -1,14 +1,14 @@ from pydantic import BaseModel from typing import Optional, List, Dict -from datetime import date +from datetime import datetime class AvoirCreate(BaseModel): """Création d'un avoir côté gateway""" client_id: str - date_avoir: Optional[date] = None - date_livraison: Optional[date] = None + date_avoir: Optional[datetime] = None + date_livraison: Optional[datetime] = None lignes: List[Dict] reference: Optional[str] = None diff --git a/schemas/documents/commandes.py b/schemas/documents/commandes.py index 5315b79..c952ce6 100644 --- a/schemas/documents/commandes.py +++ b/schemas/documents/commandes.py @@ -1,14 +1,14 @@ from pydantic import BaseModel from typing import Optional, List, Dict -from datetime import date +from datetime import datetime class CommandeCreate(BaseModel): """Création d'une commande""" client_id: str - date_commande: Optional[date] = None - date_livraison: Optional[date] = None + date_commande: Optional[datetime] = None + date_livraison: Optional[datetime] = None reference: Optional[str] = None lignes: List[Dict] diff --git a/schemas/documents/devis.py b/schemas/documents/devis.py index d91ac02..eb7cf46 100644 --- a/schemas/documents/devis.py +++ b/schemas/documents/devis.py @@ -1,12 +1,12 @@ from pydantic import BaseModel from typing import Optional, List, Dict -from datetime import date +from datetime import datetime class DevisRequest(BaseModel): client_id: str - date_devis: Optional[date] = None - date_livraison: Optional[date] = None + date_devis: Optional[datetime] = None + date_livraison: Optional[datetime] = None reference: Optional[str] = None lignes: List[Dict] diff --git a/schemas/documents/factures.py b/schemas/documents/factures.py index 8c9c208..68fc36e 100644 --- a/schemas/documents/factures.py +++ b/schemas/documents/factures.py @@ -1,14 +1,14 @@ from pydantic import BaseModel from typing import Optional, List, Dict -from datetime import date +from datetime import datetime class FactureCreate(BaseModel): """Création d'une facture côté gateway""" client_id: str - date_facture: Optional[date] = None - date_livraison: Optional[date] = None + date_facture: Optional[datetime] = None + date_livraison: Optional[datetime] = None lignes: List[Dict] reference: Optional[str] = None diff --git a/schemas/documents/livraisons.py b/schemas/documents/livraisons.py index 734955d..10e89f7 100644 --- a/schemas/documents/livraisons.py +++ b/schemas/documents/livraisons.py @@ -1,14 +1,14 @@ from pydantic import BaseModel from typing import Optional, List, Dict -from datetime import date +from datetime import datetime class LivraisonCreate(BaseModel): """Création d'une livraison côté gateway""" client_id: str - date_livraison: Optional[date] = None - date_livraison_prevue: Optional[date] = None + date_livraison: Optional[datetime] = None + date_livraison_prevue: Optional[datetime] = None lignes: List[Dict] reference: Optional[str] = None diff --git a/utils/documents/documents_data_sql.py b/utils/documents/documents_data_sql.py index cf8f608..08c451e 100644 --- a/utils/documents/documents_data_sql.py +++ b/utils/documents/documents_data_sql.py @@ -5,6 +5,7 @@ from utils.functions.functions import ( _convertir_type_depuis_sql, _convertir_type_pour_sql, _safe_strip, + _combiner_date_heure, ) @@ -106,7 +107,8 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): 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 + 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 = ? @@ -119,63 +121,59 @@ def _lire_document_sql(cursor, numero: str, type_doc: int): if not row: logger.warning( - f"[SQL READ] Document {numero} (type={type_doc}) INTROUVABLE dans F_DOCENTETE" + 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}") + logger.info(f"[SQL READ] Document trouvé: {numero_piece}") doc = { "numero": numero_piece, - "reference": _safe_strip(row[2]), - "date": str(row[1]) if row[1] else "", - "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, + "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( @@ -386,7 +384,8 @@ def _lister_documents_avec_lignes_sql( 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 + 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 = ? @@ -400,7 +399,7 @@ def _lister_documents_avec_lignes_sql( ) params.extend([f"%{filtre}%", f"%{filtre}%", f"%{filtre}%"]) - query += " ORDER BY d.DO_Date DESC" + query += " ORDER BY d.DO_Date DESC, d.DO_Heure DESC" if limit: query = f"SELECT TOP ({limit}) * FROM ({query}) AS subquery" @@ -455,15 +454,13 @@ def _lister_documents_avec_lignes_sql( "numero": numero, "type": type_doc_depuis_sql, "reference": _safe_strip(entete.DO_Ref), - "date": str(entete.DO_Date) if entete.DO_Date else "", - "date_livraison": ( - str(entete.DO_DateLivr) if entete.DO_DateLivr else "" - ), - "date_expedition": ( - str(entete.DO_DateExpedition) - if entete.DO_DateExpedition - else "" - ), + "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), diff --git a/utils/functions/data/create_doc.py b/utils/functions/data/create_doc.py index e8f4d6f..e76072d 100644 --- a/utils/functions/data/create_doc.py +++ b/utils/functions/data/create_doc.py @@ -47,9 +47,19 @@ def creer_document_vente( logger.info(f"✓ Document {config.nom_document} créé") # ===== DATES ===== - doc.DO_Date = pywintypes.Time( - normaliser_date(doc_data.get(config.champ_date_principale)) + date_principale = normaliser_date( + doc_data.get(config.champ_date_principale) ) + doc.DO_Date = pywintypes.Time(date_principale) + + # Heure - même datetime, Sage extrait la composante horaire + 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}") # Date secondaire (livraison, etc.) if config.champ_date_secondaire and doc_data.get( @@ -407,10 +417,6 @@ def _relire_document_final( def modifier_document_vente( self, numero: str, doc_data: dict, type_document: TypeDocumentVente ) -> Dict: - """ - Méthode unifiée de modification de documents de vente - RÉUTILISE les mêmes sous-méthodes que la création - """ if not self.cial: raise RuntimeError("Connexion Sage non établie") @@ -546,9 +552,14 @@ def modifier_document_vente( logger.info("📝 Modifications simples...") if modif_date: - doc.DO_Date = pywintypes.Time( - normaliser_date(doc_data_temp.get(config.champ_date_principale)) + 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: diff --git a/utils/functions/functions.py b/utils/functions/functions.py index 4cc5e12..1aecf5b 100644 --- a/utils/functions/functions.py +++ b/utils/functions/functions.py @@ -110,20 +110,96 @@ def _normaliser_type_document(type_doc: int) -> int: def normaliser_date(valeur): - if isinstance(valeur, str): - try: - return datetime.fromisoformat(valeur) - except ValueError: - return datetime.now() + """Parse flexible des dates - supporte ISO, datetime, YYYY-MM-DD HH:MM:SS""" + if valeur is None: + return datetime.now() - elif isinstance(valeur, date): - return datetime.combine(valeur, datetime.min.time()) - - elif isinstance(valeur, datetime): + if isinstance(valeur, datetime): return valeur - else: - return datetime.now() + 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: + # Convertir en entier pour éliminer les zéros de padding SQL + heure_int = int(str(do_heure).strip()) + + # Formatter en string 6 caractères (HHMMSS) + 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__ = [ @@ -136,4 +212,5 @@ __all__ = [ "_convertir_type_depuis_sql", "_convertir_type_pour_sql", "normaliser_date", + "_combiner_date_heure", ]