desarrollo instalado en el cliente globe italia
This commit is contained in:
commit
d7c4d532b5
17 files changed
+1946
No files matched your search
Executable
+313
@@ -0,0 +1,313 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
from lxml import etree
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from logger_manager import get_logger
|
||||
|
||||
# ============================================================================
|
||||
_BASE = Path(__file__).resolve().parent
|
||||
# load_dotenv(_BASE / ".env")
|
||||
# load_dotenv(_BASE.parent / ".env")
|
||||
|
||||
load_dotenv(dotenv_path=".env")
|
||||
|
||||
INPUT_DIR = Path(os.getenv("INPUT_DIR", "data/input"))
|
||||
PROCESSED_DIR = Path("data/processed")
|
||||
OUTPUT_DIR = Path("data/output")
|
||||
HTML_DIR = Path("data/html")
|
||||
SAP_B1_DIR = Path("data/sap_b1")
|
||||
TEMPLATE_FILE = _BASE / "template.html"
|
||||
|
||||
POST_TO_SAP_B1 = True
|
||||
|
||||
TEXT_MONTHS = {
|
||||
1: "enero", 2: "febrero", 3: "marzo", 4: "abril",
|
||||
5: "mayo", 6: "junio", 7: "julio", 8: "agosto",
|
||||
9: "septiembre", 10: "octubre", 11: "noviembre", 12: "diciembre",
|
||||
}
|
||||
# ============================================================================
|
||||
log = get_logger()
|
||||
|
||||
# ============================================================================
|
||||
def _format_date(d: str) -> str:
|
||||
if not d:
|
||||
return ""
|
||||
p = d.split("-")
|
||||
if len(p) == 3:
|
||||
return f"{p[2][:2]} de {TEXT_MONTHS.get(int(p[1]), p[1])} de {p[0]}"
|
||||
return d
|
||||
|
||||
# ============================================================================
|
||||
def _format_datetime(dt: str) -> str:
|
||||
if not dt:
|
||||
return ""
|
||||
dp = dt.split("T")[0]
|
||||
tp = dt.split("T")[1][:5] if "T" in dt else ""
|
||||
fd = _format_date(dp)
|
||||
return f"{fd} {tp} hrs." if tp else fd
|
||||
|
||||
# ============================================================================
|
||||
def _event_text(ev: str) -> str:
|
||||
return {"DATE_OF_INVOICE": "fecha de factura", "RECEIPT_OF_GOODS": "recepción de mercadería"}.get(ev, ev or "")
|
||||
|
||||
# ============================================================================
|
||||
def apply_allowances(net_price: int, qty: int, allowances: list) -> tuple:
|
||||
total = net_price * qty
|
||||
applied = []
|
||||
for alw in allowances:
|
||||
pct = alw.get("percentage", 0)
|
||||
if pct > 0:
|
||||
dsc = round(total * pct / 100)
|
||||
else:
|
||||
dsc = round(alw.get("amount", 0))
|
||||
applied.append({"type": alw.get("type"), "percentage": pct, "amount": dsc})
|
||||
total -= dsc
|
||||
return total, applied
|
||||
|
||||
# ============================================================================
|
||||
def _strip_ns(root):
|
||||
for el in root.iter():
|
||||
if "}" in el.tag:
|
||||
el.tag = el.tag.split("}", 1)[1]
|
||||
etree.cleanup_namespaces(root)
|
||||
return root
|
||||
|
||||
# ============================================================================
|
||||
def _x(node, path, default=None):
|
||||
v = node.xpath(path)
|
||||
return v[0] if v else default
|
||||
|
||||
# ============================================================================
|
||||
def _t(node, path, default=""):
|
||||
v = _x(node, path)
|
||||
return v.text.strip() if v is not None and v.text else default
|
||||
|
||||
# ============================================================================
|
||||
def _a(node, path, attr, default=""):
|
||||
v = _x(node, path)
|
||||
return v.get(attr, default) if v is not None else default
|
||||
|
||||
# ============================================================================
|
||||
def _l(node, path):
|
||||
return node.xpath(path)
|
||||
|
||||
# ============================================================================
|
||||
def substring_desde_fin(texto: str, cantidad: int = 10, excluir_ultimos: int = 1) -> str:
|
||||
inicio = -(cantidad + excluir_ultimos)
|
||||
fin = -excluir_ultimos if excluir_ultimos > 0 else None
|
||||
return texto[inicio:fin]
|
||||
|
||||
# ============================================================================
|
||||
def parse_order(xml_bytes: bytes, filename: str = "") -> dict:
|
||||
root = _strip_ns(etree.fromstring(xml_bytes))
|
||||
order = _x(root, "//order")
|
||||
if order is None:
|
||||
raise ValueError("Elemento <order> no encontrado")
|
||||
|
||||
msg = _x(root, "//messageHeader")
|
||||
oc_id = _x(
|
||||
order,
|
||||
'typedEntityIdentification[@entityType="ORDER"]/entityIdentification/uniqueCreatorIdentification',
|
||||
)
|
||||
oc_num = oc_id.text.strip() if oc_id is not None and oc_id.text else ""
|
||||
|
||||
cd = order.get("creationDate", "")
|
||||
pt = _x(order, "paymentTerms")
|
||||
event = pt.get("eventType", "") if pt is not None else ""
|
||||
net_days = _t(order, "paymentTerms/netPayment/timePeriodDue")
|
||||
|
||||
cab = {
|
||||
"emisor": _t(msg, "from/gln") if msg is not None else "",
|
||||
"receptor": _t(msg, "to/gln") if msg is not None else "",
|
||||
"numero_oc": substring_desde_fin(oc_num) if (oc_num is not None and len(oc_num) > 10) else "",
|
||||
"fecha_generacion": cd,
|
||||
"fecha_generacion_format": _format_datetime(cd),
|
||||
"fecha_embarque": _t(order, "movementDate"),
|
||||
"fecha_embarque_format": _format_date(_t(order, "movementDate")),
|
||||
"fecha_cancelacion": "",
|
||||
"fecha_cancelacion_format": "",
|
||||
"condiciones_pago": f"NET {net_days} días desde {_event_text(event)}" if net_days else "",
|
||||
"condiciones_pago_texto": f"NET {net_days} días desde {_event_text(event)}" if net_days else "",
|
||||
"lugar_entrega": _t(order, 'shipParty[@identificationType="SHIP_TO"]/gln'),
|
||||
"dpto_ventas": _t(order, "salesDepartamentNumber"),
|
||||
"tipo_oc": _t(order, "orderType"),
|
||||
"promocion": _t(order, "promotionDealNumber"),
|
||||
"numero_proveedor": _t(order, "internalVendorNumber"),
|
||||
"observaciones": _t(order, "specialHandling"),
|
||||
}
|
||||
|
||||
carrier = _x(root, "//mainCarrierTransport")
|
||||
if carrier is not None:
|
||||
cab["etapa_transporte"] = _t(carrier, "mode")
|
||||
cab["transportista"] = _t(carrier, "carrierName")
|
||||
cab["divisa"] = _t(carrier, "../../currency") or _t(carrier, "../../../currency") or _t(carrier, "../currency")
|
||||
|
||||
detalle = []
|
||||
for item in _l(order, "lineItem"):
|
||||
np = int(_t(item, "netPrice/amount", "0"))
|
||||
rq = int(_t(item, "requestedQuantity", "0"))
|
||||
cu = int(_t(item, "containedUnits", "0"))
|
||||
ta = int(_t(item, "totalAmount/amount", "0"))
|
||||
currency = _a(item, "netPrice/amount", "currencyISOcode", "CLP")
|
||||
|
||||
alw_list = []
|
||||
for alw in _l(item, "allowanceCharge"):
|
||||
alw_list.append({
|
||||
"type": alw.get("allowanceOrChargeType", ""),
|
||||
"percentage": int(_t(alw, "monetaryAmountOrPercentage/percentage", "0")),
|
||||
"amount": int(_t(alw, "monetaryAmountOrPercentage/amount", "0")),
|
||||
})
|
||||
|
||||
calc_total, applied_alw = apply_allowances(np, rq, alw_list)
|
||||
color = _t(item, "itemColor")
|
||||
size = _t(item, "itemSize")
|
||||
|
||||
detalle.append({
|
||||
"linea": item.get("number", ""),
|
||||
"cod_upc": _t(item, "itemIdentification/gtin"),
|
||||
"item": _t(item, "itemIdentification/buyerItemNumber"),
|
||||
"cod_prov": _t(item, "itemIdentification/vendorItemNumber"),
|
||||
"color_talla_um": "/ ".join(filter(None, [color, size])),
|
||||
"descripcion": _t(item, 'itemDescription[@language="SP"]/text'),
|
||||
"precio_original": np,
|
||||
"moneda": currency,
|
||||
"allowances": applied_alw,
|
||||
"precio_efectivo": round(calc_total / rq) if rq > 0 else 0,
|
||||
"empaques": rq,
|
||||
"unidad_empaque": _a(item, "requestedQuantity", "UnitType", ""),
|
||||
"unid_empaque": cu,
|
||||
"cantidad_unid": rq * cu,
|
||||
"importe": ta,
|
||||
})
|
||||
|
||||
return {
|
||||
"metadata": {"archivo": filename, "procesado": datetime.now().isoformat()},
|
||||
"cabecera": cab,
|
||||
"detalle": detalle,
|
||||
"totales": {
|
||||
"importe_total": sum(d["importe"] for d in detalle),
|
||||
"moneda": detalle[0]["moneda"] if detalle else "CLP",
|
||||
},
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
def write_json(data: dict, path: Path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# ============================================================================
|
||||
def write_html(data: dict, template_path: Path, output_path: Path):
|
||||
env = Environment(loader=FileSystemLoader(str(template_path.parent)))
|
||||
template = env.get_template(template_path.name)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(template.render(data=data))
|
||||
|
||||
# ============================================================================
|
||||
def process_file(xml_path: Path):
|
||||
with log.task(f"📦 {xml_path.name}"):
|
||||
log.info("📄 XML parseado")
|
||||
with open(xml_path, "rb") as f:
|
||||
data = parse_order(f.read(), filename=xml_path.name)
|
||||
|
||||
base = xml_path.stem
|
||||
write_json(data, OUTPUT_DIR / f"{base}.json")
|
||||
log.info("💾 JSON guardado")
|
||||
|
||||
write_html(data, TEMPLATE_FILE, HTML_DIR / f"{base}.html")
|
||||
log.info("💾 HTML generado")
|
||||
|
||||
if POST_TO_SAP_B1:
|
||||
with log.task("🔐 SAP B1"):
|
||||
try:
|
||||
from sapb1_mapper import to_draft_payload
|
||||
from sapb1_manager import create_orders
|
||||
from sql_manager import get_client_by_gln, get_item_catalog, get_price_list, get_prices, get_card_name
|
||||
from email_reporter import generate_email_html
|
||||
|
||||
cab = data.get("cabecera", {})
|
||||
emisor = cab.get("emisor", "")
|
||||
lugar_entrega = cab.get("lugar_entrega", "")
|
||||
|
||||
result = get_client_by_gln(emisor, lugar_entrega)
|
||||
|
||||
if result:
|
||||
cardcode = result["CardCode"]
|
||||
city = result["City"]
|
||||
county = result["County"]
|
||||
log.info("CardCode={} (desde CRD1)".format(cardcode))
|
||||
else:
|
||||
cardcode = emisor
|
||||
city = ""
|
||||
county = ""
|
||||
log.info("CardCode={} (fallback emisor)".format(cardcode))
|
||||
|
||||
item_catalog = get_item_catalog(cardcode)
|
||||
pricelist = get_price_list(cardcode)
|
||||
prices = get_prices(pricelist) if pricelist else {}
|
||||
log.info("Catálogo: {} items, precios: {} registros".format(len(item_catalog), len(prices)))
|
||||
|
||||
cardname = get_card_name(cardcode)
|
||||
email_html = generate_email_html(data, cardcode, cardname, item_catalog, prices)
|
||||
(HTML_DIR / f"{base}_email.html").write_text(email_html, encoding="utf-8")
|
||||
log.info("Email HTML generado")
|
||||
|
||||
payload = to_draft_payload(data, cardcode=cardcode, city=city, county=county, item_catalog=item_catalog, prices=prices)
|
||||
write_json(payload, SAP_B1_DIR / f"{base}_sap.json")
|
||||
log.info("Payload guardado en data/sap_b1/")
|
||||
|
||||
det = data.get("detalle", [])
|
||||
price_ok = True
|
||||
for d in det:
|
||||
upc = d.get("cod_upc", "") or ""
|
||||
sap_item = item_catalog.get(upc, "")
|
||||
sap_price = prices.get(sap_item) if sap_item else None
|
||||
importe = d.get("importe", 0) or 0
|
||||
qty = d.get("cantidad_unid", 1) or 1
|
||||
precio_xml = round(importe / qty) if qty > 0 else 0
|
||||
if not sap_item or sap_price is None or sap_price != precio_xml:
|
||||
price_ok = False
|
||||
log.warning("⚠️ {}: difiere (XML={} vs SAP={})".format(upc, precio_xml, sap_price))
|
||||
|
||||
if not price_ok:
|
||||
email_path = HTML_DIR / f"{base}_email.html"
|
||||
num_oc = cab.get("numero_oc", "") or base
|
||||
try:
|
||||
from mail_sender import send_validation_email
|
||||
send_validation_email(
|
||||
email_path,
|
||||
subject=f"Validación OC {num_oc} — diferencias precio/catálogo",
|
||||
)
|
||||
log.info("Notificación enviada a MAIL_TO")
|
||||
except Exception as e:
|
||||
log.warning("Error enviando correo: {}".format(e))
|
||||
|
||||
create_orders(payload)
|
||||
except ImportError as e:
|
||||
log.warning("⚠️ SAP saltado (dependencia faltante: {})".format(e))
|
||||
except Exception as e:
|
||||
log.warning("❌ SAP error: {}".format(e))
|
||||
|
||||
PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(xml_path), str(PROCESSED_DIR / xml_path.name))
|
||||
log.info("📁 XML movido a processed/")
|
||||
|
||||
# ============================================================================
|
||||
def process_all():
|
||||
INPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
files = sorted(f for f in INPUT_DIR.iterdir() if f.is_file())
|
||||
if not files:
|
||||
return
|
||||
log.info("📦 Archivos pendientes: {}".format(len(files)))
|
||||
for f in files:
|
||||
try:
|
||||
process_file(f)
|
||||
except Exception as e:
|
||||
log.error("❌ Error con {}: {}".format(f.name, e))
|
||||
|
||||
# ============================================================================
|
||||
Reference in new issue
Block a user