primer commit
This commit is contained in:
commit
cf8d9fbbb2
9 files changed
+764
No files matched your search
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
venv/
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
downloads/
|
||||||
|
logs/
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"hana": {
|
||||||
|
"host": "192.168.1.175",
|
||||||
|
"port": 30015,
|
||||||
|
"user": "SYSTEM",
|
||||||
|
"password": "SAPB1Admin.25",
|
||||||
|
"schema": "SBO_TAGUMEDICA_21"
|
||||||
|
},
|
||||||
|
"files":{
|
||||||
|
"path":"downloads"
|
||||||
|
},
|
||||||
|
"outlook": {
|
||||||
|
"client_id": "957c324b-3da3-43ed-b912-ca52fb5d3128",
|
||||||
|
"client_secret": "Guc8Q~u6FH3xBKpxJ-hQ7QSpRWbnvmX.LnWr-dw7",
|
||||||
|
"tenant_id": "76f77b2f-6fc3-42a0-ae83-df05508405d6",
|
||||||
|
"download_folder": "./adjuntos",
|
||||||
|
"verify_ssl": false,
|
||||||
|
"ca_bundle": null
|
||||||
|
},
|
||||||
|
"mail":{
|
||||||
|
"sender":"cpe@tagumedica.com",
|
||||||
|
"to":["cpe@tagumedica.com"]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from config_model import Config, HanaConfig, FilesConfig, OutlookConfig, MailConfig
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
CONFIG_PATH = Path(__file__).resolve().parent / "config.json"
|
||||||
|
|
||||||
|
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Instantiate dataclasses
|
||||||
|
hana = HanaConfig(**data.get('hana', {}))
|
||||||
|
files = FilesConfig(**data.get('files', {}))
|
||||||
|
outlook = OutlookConfig(
|
||||||
|
client_id=data['outlook']['client_id'],
|
||||||
|
client_secret=data['outlook']['client_secret'],
|
||||||
|
tenant_id=data['outlook']['tenant_id'],
|
||||||
|
download_folder=data['outlook']['download_folder'],
|
||||||
|
verify_ssl=data['outlook'].get('verify_ssl', True),
|
||||||
|
ca_bundle=data['outlook'].get('ca_bundle')
|
||||||
|
)
|
||||||
|
|
||||||
|
mail = MailConfig(
|
||||||
|
sender=data['mail']['sender'],
|
||||||
|
to=data['mail']['to']
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG = Config(
|
||||||
|
hana=hana,
|
||||||
|
files=files,
|
||||||
|
outlook=outlook,
|
||||||
|
mail=mail
|
||||||
|
)
|
||||||
|
|
||||||
|
########################################################
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HanaConfig:
|
||||||
|
host: str = ""
|
||||||
|
port: int = 30015
|
||||||
|
user: str = ""
|
||||||
|
password: str = ""
|
||||||
|
schema: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server(self) -> str:
|
||||||
|
return self.host
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FilesConfig:
|
||||||
|
path: str = ""
|
||||||
|
|
||||||
|
# Nueva configuración para Outlook
|
||||||
|
@dataclass
|
||||||
|
class OutlookConfig:
|
||||||
|
client_id: str = ""
|
||||||
|
client_secret: str = ""
|
||||||
|
tenant_id: str = ""
|
||||||
|
download_folder: str = ""
|
||||||
|
verify_ssl: bool = True
|
||||||
|
ca_bundle: Optional[str] = None
|
||||||
|
|
||||||
|
# Configuración de correo (mail)
|
||||||
|
@dataclass
|
||||||
|
class MailConfig:
|
||||||
|
sender: str = ""
|
||||||
|
to: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
hana: HanaConfig
|
||||||
|
files: FilesConfig
|
||||||
|
outlook: OutlookConfig
|
||||||
|
mail: MailConfig
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
[
|
||||||
|
{"Fecha": "2026-03-09", "Tipo": "01", "Folio": "FB03-2140049", "Total": 255.60, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-09", "Tipo": "01", "Folio": "FB03-2140074", "Total": 187.87, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-31", "Tipo": "01", "Folio": "FB03-2175811", "Total": 139.44, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-27", "Tipo": "01", "Folio": "FB03-2174046", "Total": 198.12, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-27", "Tipo": "01", "Folio": "FB03-2174021", "Total": 263.42, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-26", "Tipo": "01", "Folio": "FB03-2171060", "Total": 325.99, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-25", "Tipo": "01", "Folio": "FB03-2167938", "Total": 138.72, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-24", "Tipo": "01", "Folio": "FB03-2167436", "Total": 247.16, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FB03-2164070", "Total": 187.76, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FB03-2164043", "Total": 187.76, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FB03-2164016", "Total": 146.03, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FB03-2163989", "Total": 264.25, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FB03-2163962", "Total": 159.13, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-12", "Tipo": "01", "Folio": "FB03-2148003", "Total": 260.45, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-06", "Tipo": "01", "Folio": "FB01-2754860", "Total": 343.20, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-07", "Tipo": "01", "Folio": "FI01-18763215", "Total": 591.29, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-07", "Tipo": "01", "Folio": "FN01-43415089", "Total": 5.32, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-04-05", "Tipo": "01", "Folio": "FC01-655990", "Total": 24884.08, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652057", "Total": 20.40, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651957", "Total": 34.97, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651857", "Total": 48.32, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651757", "Total": 40.65, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652957", "Total": 67.73, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651607", "Total": 18.56, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651407", "Total": 33.22, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651507", "Total": 38.82, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651657", "Total": 5.08, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666008", "Total": 9.54, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666058", "Total": 18.83, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666108", "Total": 73.18, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666158", "Total": 23.38, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666208", "Total": 1.75, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666258", "Total": 122.27, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666308", "Total": 39.63, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666358", "Total": 105.17, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666408", "Total": 11.98, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666458", "Total": 13.16, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666508", "Total": 37.88, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652857", "Total": 1500.93, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652707", "Total": 119.35, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652657", "Total": 27.02, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652557", "Total": 44.44, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652457", "Total": 24.64, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652357", "Total": 7.90, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652257", "Total": 26.20, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3652157", "Total": 14.40, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-13", "Tipo": "01", "Folio": "FF01-7858371", "Total": 20.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-16", "Tipo": "01", "Folio": "FC01-591576", "Total": 153.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-16", "Tipo": "01", "Folio": "FC01-591578", "Total": 299.94, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-05", "Tipo": "01", "Folio": "FC03-6519623", "Total": 90.70, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-05", "Tipo": "01", "Folio": "FC01-530088", "Total": 74.56, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FF01-7834626", "Total": 26.38, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-12", "Tipo": "01", "Folio": "FC03-6547872", "Total": 43056.77, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-11", "Tipo": "01", "Folio": "FC03-6543416", "Total": 99.57, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FO01-7834725", "Total": 124.30, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FN01-7804676", "Total": 29.60, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FN01-53361710", "Total": 109.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FO01-7834675", "Total": 29.60, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FO01-7804626", "Total": 28.63, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FC03-6591174", "Total": 122.98, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-24", "Tipo": "01", "Folio": "FF01-7905388", "Total": 20.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-05", "Tipo": "01", "Folio": "FO01-7905087", "Total": 20.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-24", "Tipo": "01", "Folio": "FO01-79005487", "Total": 29.94, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FC03-5565306", "Total": 304.53, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FC01-592173", "Total": 153.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-17", "Tipo": "01", "Folio": "FN01-63422478", "Total": 10.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-16", "Tipo": "01", "Folio": "FC01-7861432", "Total": 52.16, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-16", "Tipo": "01", "Folio": "FF01-7861392", "Total": 104.38, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-16", "Tipo": "01", "Folio": "FN01-43592288", "Total": 11.30, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-16", "Tipo": "01", "Folio": "FC03-6559540", "Total": 82.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-16", "Tipo": "01", "Folio": "FC03-6559492", "Total": 85.01, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-24", "Tipo": "01", "Folio": "FN01-44177547", "Total": 1.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-24", "Tipo": "01", "Folio": "FN01-44144049", "Total": 1.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-24", "Tipo": "01", "Folio": "FC03-6596247", "Total": 50.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FF01-7895690", "Total": 20.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FC03-6591214", "Total": 50.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-23", "Tipo": "01", "Folio": "FC03-6591248", "Total": 50.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-24", "Tipo": "01", "Folio": "FN01-53361710", "Total": 22993.89, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-31", "Tipo": "01", "Folio": "FO01-7939416", "Total": 32.93, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-31", "Tipo": "01", "Folio": "FN01-143493572", "Total": 1.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-12", "Tipo": "01", "Folio": "FO01-7850651", "Total": 28.63, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-12", "Tipo": "01", "Folio": "FO01-7850701", "Total": 33.36, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-12", "Tipo": "01", "Folio": "FO01-7850751", "Total": 20.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FC01-590267", "Total": 208.25, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-05", "Tipo": "01", "Folio": "FO01-7817287", "Total": 26.38, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-05", "Tipo": "01", "Folio": "FC01-590270", "Total": 273.61, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FC01-590276", "Total": 710.32, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FC01-590276", "Total": 544.64, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FC03-6539674", "Total": 196.42, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FC03-6539719", "Total": 50.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666558", "Total": 13.16, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666608", "Total": 143.16, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666658", "Total": 8.53, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666708", "Total": 34.70, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666758", "Total": 32.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666808", "Total": 13.16, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666858", "Total": 13.16, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3666908", "Total": 24.80, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3667108", "Total": 13.97, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-06", "Tipo": "01", "Folio": "FC03-6522400", "Total": 1360.40, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-06", "Tipo": "01", "Folio": "FC03-6522450", "Total": 120.04, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651107", "Total": 43.92, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651207", "Total": 250.35, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651307", "Total": 15.24, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-04", "Tipo": "01", "Folio": "FF01-3651507", "Total": 56.57, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-24", "Tipo": "01", "Folio": "FO01-79005586", "Total": 68.02, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-20", "Tipo": "01", "Folio": "FO01-7890803", "Total": 77.35, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-19", "Tipo": "01", "Folio": "FC03-6581706", "Total": 145.10, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-19", "Tipo": "01", "Folio": "FO01-7886392", "Total": 20.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-25", "Tipo": "01", "Folio": "FC03-6604683", "Total": 121.71, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-19", "Tipo": "01", "Folio": "FC01-596051", "Total": 198.04, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FC01-7886328", "Total": 232.05, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FO01-7876989", "Total": 94.40, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FC03-6591706", "Total": 20.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-27", "Tipo": "01", "Folio": "FO01-7923629", "Total": 20.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FO01-7876939", "Total": 29.60, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FN01-43408889", "Total": 10.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-08", "Tipo": "01", "Folio": "FD0-6433581", "Total": 16.50, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-07", "Tipo": "01", "Folio": "FD0-6433608", "Total": 13.43, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-09", "Tipo": "01", "Folio": "FN01-88644088", "Total": 186.88, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-09", "Tipo": "01", "Folio": "FN01-43415139", "Total": 7.30, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-06", "Tipo": "01", "Folio": "FO01-7823739", "Total": 28.63, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-06", "Tipo": "01", "Folio": "FC03-7832739", "Total": 563.77, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FC01-590264", "Total": 29.10, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-10", "Tipo": "01", "Folio": "FC01-590264", "Total": 157.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-27", "Tipo": "01", "Folio": "FO01-7923978", "Total": 41.00, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-27", "Tipo": "01", "Folio": "FC03-6624790", "Total": 244.70, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FC03-6565394", "Total": 81.52, "RUC": "20100047218"},
|
||||||
|
{"Fecha": "2026-03-18", "Tipo": "01", "Folio": "FC03-6565350", "Total": 87.15, "RUC": "20100047218"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime, date
|
||||||
|
from hdbcli import dbapi
|
||||||
|
from config_loader import CONFIG
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
def get_connection():
|
||||||
|
"""Crea y retorna una conexión a SAP HANA."""
|
||||||
|
try:
|
||||||
|
conn = dbapi.connect(
|
||||||
|
address=CONFIG.hana.server,
|
||||||
|
port=CONFIG.hana.port,
|
||||||
|
user=CONFIG.hana.user,
|
||||||
|
password=CONFIG.hana.password
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error conectando a HANA: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
def obtener_registros(days=30):
|
||||||
|
"""
|
||||||
|
Se conecta a la base de datos SAP HANA, realiza una consulta a la tabla OPCH,
|
||||||
|
y formatea el resultado para que coincida con el esquema de documentos.json.
|
||||||
|
"""
|
||||||
|
|
||||||
|
schema = CONFIG.hana.schema
|
||||||
|
query = f"""
|
||||||
|
SELECT TOP 50 T0."DocNum", T0."DocEntry", TO_VARCHAR(T0."DocDate",'YYYY-MM-DD') "DocDate"
|
||||||
|
, TO_VARCHAR(T0."TaxDate",'YYYY-MM-DD') "TaxDate", T0."CardName", T0."FolioPref", T0."FolioNum", T0."Indicator", T0."DocCur"
|
||||||
|
, CASE T0."DocCur" WHEN 'SOL' THEN T0."DocTotal" ELSE T0."DocTotalFC" END "DocTotal"
|
||||||
|
, T0."U_PGX_PDF" "PDF", T0."U_PGX_XML" "XML", T0."LicTradNum"
|
||||||
|
FROM "{schema}".OPCH T0
|
||||||
|
WHERE T0."CANCELED" = 'N'
|
||||||
|
AND T0."LicTradNum" = '20100047218'
|
||||||
|
AND (T0."U_PGX_PDF" = 'N' OR T0."U_PGX_XML" = 'N')
|
||||||
|
AND T0."CreateDate" >= ADD_DAYS(CURRENT_DATE, -{days});
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Conectando al servidor SAP HANA ({CONFIG.hana.server}:{CONFIG.hana.port})...")
|
||||||
|
connection = get_connection()
|
||||||
|
if not connection:
|
||||||
|
raise ConnectionError("No se pudo establecer la conexión a SAP HANA.")
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(query)
|
||||||
|
|
||||||
|
# Obtener nombres de columnas
|
||||||
|
columns = [column[0] for column in cursor.description]
|
||||||
|
|
||||||
|
# Convertir resultados a lista de diccionarios
|
||||||
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
results.append(dict(zip(columns, row)))
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Ocurrió un error en la conexión o consulta a HANA: {e}")
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
if cursor:
|
||||||
|
cursor.close()
|
||||||
|
if connection:
|
||||||
|
connection.close()
|
||||||
|
print("Conexión con SAP HANA cerrada.")
|
||||||
|
|
||||||
|
########################################################
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
class MultiStream:
|
||||||
|
"""
|
||||||
|
Clase para redirigir la salida a múltiples flujos (consola y archivo).
|
||||||
|
"""
|
||||||
|
def __init__(self, *streams):
|
||||||
|
self.streams = streams
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
for stream in self.streams:
|
||||||
|
stream.write(data)
|
||||||
|
stream.flush()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
for stream in self.streams:
|
||||||
|
stream.flush()
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
def setup_logger():
|
||||||
|
"""
|
||||||
|
Configura la redirección de stdout y stderr a un archivo de log y a la consola.
|
||||||
|
Retorna la ruta del archivo de log creado.
|
||||||
|
"""
|
||||||
|
log_dir = "logs"
|
||||||
|
if not os.path.exists(log_dir):
|
||||||
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
|
# Nombre del archivo: YYYY-MM-DD_HH-MM-SS.log
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
log_filename = f"{timestamp}.log"
|
||||||
|
log_path = os.path.join(log_dir, log_filename)
|
||||||
|
|
||||||
|
# Abrir archivo en modo append con encoding utf-8 para soportar iconos
|
||||||
|
log_file = open(log_path, "a", encoding="utf-8")
|
||||||
|
|
||||||
|
# Redirigir stdout y stderr
|
||||||
|
sys.stdout = MultiStream(sys.stdout, log_file)
|
||||||
|
sys.stderr = MultiStream(sys.stderr, log_file)
|
||||||
|
|
||||||
|
return log_path
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
def limpiar_logs_antiguos(dias=7):
|
||||||
|
"""
|
||||||
|
Elimina archivos de log con más de 'n' días de antigüedad.
|
||||||
|
"""
|
||||||
|
log_dir = "logs"
|
||||||
|
if not os.path.exists(log_dir):
|
||||||
|
return
|
||||||
|
|
||||||
|
limite = datetime.now() - timedelta(days=dias)
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for archivo in os.listdir(log_dir):
|
||||||
|
ruta_archivo = os.path.join(log_dir, archivo)
|
||||||
|
if os.path.isfile(ruta_archivo) and archivo.endswith(".log"):
|
||||||
|
fecha_archivo = datetime.fromtimestamp(os.path.getmtime(ruta_archivo))
|
||||||
|
if fecha_archivo < limite:
|
||||||
|
try:
|
||||||
|
os.remove(ruta_archivo)
|
||||||
|
count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Error al eliminar log {archivo}: {e}")
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
print(f"🧹 Se eliminaron {count} archivos de log antiguos (más de {dias} días).")
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import logger_utilities
|
||||||
|
import hana_client
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from config_loader import CONFIG
|
||||||
|
import outlook_utilities
|
||||||
|
import argparse
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
def descargar_comprobante(driver, wait, ruc, tipo, correlativo, monto, fecha, download_dir):
|
||||||
|
url = "https://public-einvoicing.sovos.com/pe"
|
||||||
|
driver.get(url)
|
||||||
|
|
||||||
|
# Esperar a que la aplicación cargue el input del RUC
|
||||||
|
wait.until(EC.presence_of_element_located((By.NAME, "senderTaxId")))
|
||||||
|
|
||||||
|
# 1. Completar RUC
|
||||||
|
ruc_input = driver.find_element(By.NAME, "senderTaxId")
|
||||||
|
ruc_input.clear()
|
||||||
|
ruc_input.send_keys(ruc)
|
||||||
|
|
||||||
|
# 2. Seleccionar Tipo Comprobante
|
||||||
|
select_trigger = driver.find_element(By.ID, "mui-component-select-invoiceTypeCode")
|
||||||
|
select_trigger.click()
|
||||||
|
|
||||||
|
# Esperar a que la opción esté disponible y seleccionarla
|
||||||
|
option_xpath = f"//li[@role='option' and @data-value='{tipo}']"
|
||||||
|
option_element = wait.until(EC.element_to_be_clickable((By.XPATH, option_xpath)))
|
||||||
|
option_element.click()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 3. Completar Folio / Correlativo
|
||||||
|
correlative_input = driver.find_element(By.NAME, "correlative")
|
||||||
|
correlative_input.clear()
|
||||||
|
correlative_input.send_keys(correlativo)
|
||||||
|
|
||||||
|
# 4. Completar Monto Total
|
||||||
|
amount_input = driver.find_element(By.NAME, "payableAmount")
|
||||||
|
amount_input.clear()
|
||||||
|
amount_input.send_keys(str(monto))
|
||||||
|
|
||||||
|
# 5. Completar Fecha de emisión
|
||||||
|
date_input = driver.find_element(By.XPATH, "//input[contains(@placeholder, 'Ej: 202')]")
|
||||||
|
driver.execute_script("arguments[0].click();", date_input)
|
||||||
|
|
||||||
|
# Seleccionar todo y reemplazar con la fecha
|
||||||
|
date_input.send_keys(Keys.COMMAND, 'a')
|
||||||
|
date_input.send_keys(Keys.BACKSPACE)
|
||||||
|
date_input.send_keys(fecha)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# 6. Enviar Formulario
|
||||||
|
submit_btn = driver.find_element(By.XPATH, "//button[contains(text(), 'Ver documento')]")
|
||||||
|
if not submit_btn.is_enabled():
|
||||||
|
raise ValueError("El botón 'Ver documento' está deshabilitado. Verifique que los campos sean válidos.")
|
||||||
|
|
||||||
|
driver.execute_script("arguments[0].click();", submit_btn)
|
||||||
|
|
||||||
|
# Esperar a que la página de resultados cargue o muestre error de "Document not found"
|
||||||
|
download_btn_xpath = "//*[@data-testid='btn-download-document']"
|
||||||
|
error_xpath = "//*[contains(@class, 'sovosSnackbar--error') or contains(text(), 'Document not found')]"
|
||||||
|
|
||||||
|
download_btn_present = False
|
||||||
|
error_present = False
|
||||||
|
|
||||||
|
for _ in range(30): # Esperar hasta 15 segundos máximo (30 * 0.5s)
|
||||||
|
btn_elements = driver.find_elements(By.XPATH, download_btn_xpath)
|
||||||
|
if btn_elements and btn_elements[0].is_displayed():
|
||||||
|
download_btn_present = True
|
||||||
|
break
|
||||||
|
|
||||||
|
err_elements = driver.find_elements(By.XPATH, error_xpath)
|
||||||
|
if err_elements and err_elements[0].is_displayed():
|
||||||
|
error_present = True
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if error_present:
|
||||||
|
raise FileNotFoundError("El comprobante no existe en el portal de Sovos (Document not found).")
|
||||||
|
|
||||||
|
if not download_btn_present:
|
||||||
|
raise TimeoutError("Se superó el tiempo de espera sin encontrar el comprobante ni un mensaje de error.")
|
||||||
|
|
||||||
|
# 7. Descargar PDF (Activo por defecto)
|
||||||
|
download_btn = driver.find_element(By.XPATH, download_btn_xpath)
|
||||||
|
driver.execute_script("arguments[0].click();", download_btn)
|
||||||
|
|
||||||
|
# 8. Cambiar a formato XML y Descargar
|
||||||
|
doc_sel_btn = driver.find_element(By.XPATH, "//button[contains(., '.pdf') or contains(., '.xml')]")
|
||||||
|
driver.execute_script("arguments[0].click();", doc_sel_btn)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
xml_option = wait.until(EC.element_to_be_clickable((By.XPATH, "//li[@role='menuitem' and contains(., '.xml')]")))
|
||||||
|
driver.execute_script("arguments[0].click();", xml_option)
|
||||||
|
|
||||||
|
# Esperar a que el selector se actualice y muestre XML
|
||||||
|
wait.until(EC.text_to_be_present_in_element((By.XPATH, "//button[contains(@class, 'sovosButton')]"), ".xml"))
|
||||||
|
|
||||||
|
download_btn = driver.find_element(By.XPATH, download_btn_xpath)
|
||||||
|
driver.execute_script("arguments[0].click();", download_btn)
|
||||||
|
|
||||||
|
# Generar patrón esperado para verificar (ej. serie = FB03, número con padding a 8 dígitos)
|
||||||
|
serie_num_pattern = correlativo.replace("-", "")
|
||||||
|
if "-" in correlativo:
|
||||||
|
parts = correlativo.split("-")
|
||||||
|
if len(parts) == 2:
|
||||||
|
serie_num_pattern = f"{parts[0]}{parts[1].zfill(8)}".lower()
|
||||||
|
|
||||||
|
# Esperar a que las descargas finalicen en el directorio
|
||||||
|
pdf_found = False
|
||||||
|
xml_found = False
|
||||||
|
for attempt in range(15):
|
||||||
|
files = os.listdir(download_dir)
|
||||||
|
pdf_found = any(f.endswith(".pdf") and serie_num_pattern in f.replace("-", "").lower() for f in files)
|
||||||
|
xml_found = any(f.endswith(".xml") and serie_num_pattern in f.replace("-", "").lower() for f in files)
|
||||||
|
if pdf_found and xml_found:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Listar descargas completadas
|
||||||
|
files = os.listdir(download_dir)
|
||||||
|
downloaded_files = [f for f in files if (f.endswith(".pdf") or f.endswith(".xml")) and serie_num_pattern in f.replace("-", "").lower()]
|
||||||
|
|
||||||
|
if not pdf_found or not xml_found:
|
||||||
|
raise FileNotFoundError(f"No se pudieron descargar los archivos correspondientes al correlativo: {correlative}")
|
||||||
|
|
||||||
|
return downloaded_files
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
def main():
|
||||||
|
registros = hana_client.obtener_registros(days=150)
|
||||||
|
|
||||||
|
download_dir = CONFIG.files.path
|
||||||
|
os.makedirs(download_dir, exist_ok=True)
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("⚙️ CONFIGURACIÓN DE DESCARGA SOVOS")
|
||||||
|
print(f"📁 Directorio Destino: {download_dir}")
|
||||||
|
print(f"📊 Total comprobantes a procesar: {len(registros)}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Configurar opciones de Chrome
|
||||||
|
chrome_options = Options()
|
||||||
|
chrome_options.add_argument("--headless")
|
||||||
|
chrome_options.add_argument("--no-sandbox")
|
||||||
|
chrome_options.add_argument("--disable-dev-shm-usage")
|
||||||
|
chrome_options.add_argument("--disable-gpu")
|
||||||
|
chrome_options.add_argument("--blink-settings=imagesEnabled=false")
|
||||||
|
chrome_options.add_argument("--disable-features=InsecureDownloadWarnings")
|
||||||
|
|
||||||
|
prefs = {
|
||||||
|
"download.default_directory": download_dir,
|
||||||
|
"download.prompt_for_download": False,
|
||||||
|
"download.directory_upgrade": True,
|
||||||
|
"safebrowsing.enabled": False,
|
||||||
|
"profile.default_content_setting_values.automatic_downloads": 1
|
||||||
|
}
|
||||||
|
chrome_options.add_experimental_option("prefs", prefs)
|
||||||
|
|
||||||
|
print("\n🌐 [1/2] Iniciando navegador Chrome en modo headless...")
|
||||||
|
driver = webdriver.Chrome(options=chrome_options)
|
||||||
|
|
||||||
|
# Configurar descargas en modo Headless vía CDP
|
||||||
|
driver.execute_cdp_cmd("Page.setDownloadBehavior", {
|
||||||
|
"behavior": "allow",
|
||||||
|
"downloadPath": download_dir
|
||||||
|
})
|
||||||
|
|
||||||
|
wait = WebDriverWait(driver, 15)
|
||||||
|
|
||||||
|
exitosos = 0
|
||||||
|
fallidos = 0
|
||||||
|
archivos_descargados = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for index, registro in enumerate(registros, start=1):
|
||||||
|
doc_num = registro.get('DocNum')
|
||||||
|
ind = registro.get('Indicator')
|
||||||
|
f_pref = registro.get('FolioPref')
|
||||||
|
f_num = registro.get('FolioNum')
|
||||||
|
print(f"\n🔍 PROCESANDO REGISTRO EN HANA: {doc_num} | Tipo: {ind} | Folio: {f_pref}-{f_num}")
|
||||||
|
|
||||||
|
ruc = registro.get("LicTradNum")
|
||||||
|
tipo = registro.get("Indicator")
|
||||||
|
folio = f_pref + "-" + str(f_num)
|
||||||
|
total = registro.get("DocTotal")
|
||||||
|
fecha = registro.get("TaxDate")
|
||||||
|
|
||||||
|
print(f"⚡ [{index}/{len(registros)}] Descargando Comprobante: {folio} (RUC: {ruc})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
archivos = descargar_comprobante(driver, wait, ruc, tipo, folio, total, fecha, download_dir)
|
||||||
|
exitosos += 1
|
||||||
|
archivos_descargados.extend(archivos)
|
||||||
|
print(f" ✅ Comprobante {folio} descargado correctamente.")
|
||||||
|
# Pequeño delay entre descargas
|
||||||
|
time.sleep(1.5)
|
||||||
|
except Exception as e:
|
||||||
|
fallidos += 1
|
||||||
|
print(f" ❌ Error al descargar {folio}: {e}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
print("\n🔌 Cerrando navegador...")
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("📊 RESUMEN DE PROCESAMIENTO")
|
||||||
|
print(f" ✅ Procesados con éxito: {exitosos}")
|
||||||
|
print(f" ❌ Fallidos: {fallidos}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Enviar correo con archivos descargados
|
||||||
|
if archivos_descargados:
|
||||||
|
try:
|
||||||
|
# Convertir nombres de archivo a rutas completas
|
||||||
|
attachment_paths = [os.path.join(download_dir, f) for f in archivos_descargados]
|
||||||
|
token = outlook_utilities.get_access_token()
|
||||||
|
subject = "Comprobantes descargados automáticamente"
|
||||||
|
html_body = f"<p>Se adjuntan {len(attachment_paths)} comprobantes descargados.</p>"
|
||||||
|
outlook_utilities.send_email(token, CONFIG.mail.sender, CONFIG.mail.to, subject, html_body, attachment_paths=attachment_paths)
|
||||||
|
except Exception as email_err:
|
||||||
|
print(f"⚠️ Error al enviar correo: {email_err}")
|
||||||
|
|
||||||
|
#if fallidos > 0:
|
||||||
|
# sys.exit(1)
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
# Configurar Logger (Redirige stdout a archivo y consola)
|
||||||
|
log_path = logger_utilities.setup_logger()
|
||||||
|
logger_utilities.limpiar_logs_antiguos(dias=30)
|
||||||
|
|
||||||
|
inicio_ejecucion = datetime.now()
|
||||||
|
print("="*60)
|
||||||
|
print(f"🚀 INICIO DE EJECUCIÓN: {inicio_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f"📄 Log guardado en: {log_path}")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
fin_ejecucion = datetime.now()
|
||||||
|
duracion = fin_ejecucion - inicio_ejecucion
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(f"🏁 FIN DE EJECUCIÓN: {fin_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f"⏱️ Duración total: {str(duracion).split('.')[0]}") # Sin microsegundos
|
||||||
|
print("="*60)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import msal
|
||||||
|
import requests
|
||||||
|
import warnings
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from config_loader import CONFIG
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
def get_access_token():
|
||||||
|
"""
|
||||||
|
Obtiene un token de acceso de Microsoft Graph usando las credenciales
|
||||||
|
configuradas en config.json (Client ID, Client Secret y Tenant ID).
|
||||||
|
"""
|
||||||
|
if not CONFIG or not CONFIG.outlook:
|
||||||
|
raise ValueError("La configuración de Outlook no está cargada correctamente.")
|
||||||
|
|
||||||
|
authority = f"https://login.microsoftonline.com/{CONFIG.outlook.tenant_id}"
|
||||||
|
|
||||||
|
app = msal.ConfidentialClientApplication(
|
||||||
|
CONFIG.outlook.client_id,
|
||||||
|
authority=authority,
|
||||||
|
client_credential=CONFIG.outlook.client_secret,
|
||||||
|
verify=CONFIG.outlook.verify_ssl
|
||||||
|
)
|
||||||
|
|
||||||
|
scopes = ["https://graph.microsoft.com/.default"]
|
||||||
|
result = app.acquire_token_for_client(scopes=scopes)
|
||||||
|
|
||||||
|
if "access_token" in result:
|
||||||
|
return result["access_token"]
|
||||||
|
else:
|
||||||
|
error = result.get("error")
|
||||||
|
error_description = result.get("error_description")
|
||||||
|
correlation_id = result.get("correlation_id")
|
||||||
|
raise Exception(f"Error al obtener token: {error} - {error_description} (ID: {correlation_id})")
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
def send_email(access_token, from_email, to_recipients, subject, html_body, attachment_paths=None):
|
||||||
|
"""
|
||||||
|
Envía un correo electrónico con cuerpo HTML y múltiples adjuntos PDF.
|
||||||
|
|
||||||
|
:param access_token: Token de acceso de Microsoft Graph API.
|
||||||
|
:param from_email: Correo electrónico del remitente (cuenta de Office 365).
|
||||||
|
:param to_recipients: Lista de correos electrónicos de los destinatarios.
|
||||||
|
:param subject: Asunto del correo.
|
||||||
|
:param html_body: Contenido del correo en formato HTML.
|
||||||
|
:param attachment_paths: Lista de rutas a los archivos PDF que se adjuntarán.
|
||||||
|
:return: True si se envió correctamente, False en caso contrario.
|
||||||
|
"""
|
||||||
|
url = f"https://graph.microsoft.com/v1.0/users/{from_email}/sendMail"
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {access_token}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configuración de SSL desde CONFIG
|
||||||
|
outlook_config = CONFIG.outlook
|
||||||
|
verify = outlook_config.verify_ssl if outlook_config.ca_bundle is None else outlook_config.ca_bundle
|
||||||
|
|
||||||
|
# Construir lista de destinatarios para Graph API
|
||||||
|
recipients_payload = []
|
||||||
|
for email in to_recipients:
|
||||||
|
recipients_payload.append({
|
||||||
|
"emailAddress": {
|
||||||
|
"address": email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Construir lista de adjuntos
|
||||||
|
attachments_payload = []
|
||||||
|
if attachment_paths:
|
||||||
|
for path in attachment_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
content_bytes = f.read()
|
||||||
|
|
||||||
|
# Codificar el contenido en Base64
|
||||||
|
encoded_content = base64.b64encode(content_bytes).decode('utf-8')
|
||||||
|
filename = os.path.basename(path)
|
||||||
|
|
||||||
|
attachments_payload.append({
|
||||||
|
"@odata.type": "#microsoft.graph.fileAttachment",
|
||||||
|
"name": filename,
|
||||||
|
"contentType": "application/pdf",
|
||||||
|
"contentBytes": encoded_content
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
print(f"Advertencia: El archivo no existe y no se adjuntará: {path}")
|
||||||
|
|
||||||
|
# Estructura del mensaje para la API
|
||||||
|
email_data = {
|
||||||
|
"message": {
|
||||||
|
"subject": subject,
|
||||||
|
"body": {
|
||||||
|
"contentType": "HTML",
|
||||||
|
"content": html_body
|
||||||
|
},
|
||||||
|
"toRecipients": recipients_payload,
|
||||||
|
"attachments": attachments_payload
|
||||||
|
},
|
||||||
|
"saveToSentItems": "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, headers=headers, json=email_data, verify=verify)
|
||||||
|
if response.status_code == 202:
|
||||||
|
print(f"SUCCESS: Correo enviado exitosamente a: {', '.join(to_recipients)}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"ERROR: Al enviar correo: {response.status_code} - {response.text}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Excepcion al enviar correo: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
########################################################
|
||||||
Reference in new issue
Block a user