From 1cce81b75f5a2ae0e5bcaaf78be5eb13a99ccbf1 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Mon, 13 Jul 2026 06:35:36 +0000 Subject: [PATCH] Facturapi: fix legal payload (name/legal_name/tax_system, address, no tax_id), reuse local org, status via user key; add fiscal address fields; embed Carta Manifiesto iframe --- pos/blueprints/config_bp.py | 12 ++++ pos/blueprints/invoicing_bp.py | 7 ++ pos/services/facturapi_service.py | 105 +++++++++++++++++++++++------- pos/static/js/invoicing.js | 36 +++++++++- pos/static/pwa/sw.js | 2 +- pos/templates/invoicing.html | 73 +++++++++++++++++++++ 6 files changed, 209 insertions(+), 26 deletions(-) diff --git a/pos/blueprints/config_bp.py b/pos/blueprints/config_bp.py index 90257c7..0af84d7 100644 --- a/pos/blueprints/config_bp.py +++ b/pos/blueprints/config_bp.py @@ -761,6 +761,12 @@ def get_business(): 'regimen_fiscal': cfg.get('tenant_regimen_fiscal', ''), 'cp': cfg.get('tenant_cp', ''), 'direccion': cfg.get('tenant_direccion', ''), + 'numero_exterior': cfg.get('tenant_numero_exterior', ''), + 'numero_interior': cfg.get('tenant_numero_interior', ''), + 'colonia': cfg.get('tenant_colonia', ''), + 'ciudad': cfg.get('tenant_ciudad', ''), + 'municipio': cfg.get('tenant_municipio', ''), + 'estado': cfg.get('tenant_estado', ''), 'telefono': cfg.get('tenant_telefono', ''), 'email': cfg.get('tenant_email', ''), }) @@ -778,6 +784,12 @@ def update_business(): 'regimen_fiscal': 'tenant_regimen_fiscal', 'cp': 'tenant_cp', 'direccion': 'tenant_direccion', + 'numero_exterior': 'tenant_numero_exterior', + 'numero_interior': 'tenant_numero_interior', + 'colonia': 'tenant_colonia', + 'ciudad': 'tenant_ciudad', + 'municipio': 'tenant_municipio', + 'estado': 'tenant_estado', 'telefono': 'tenant_telefono', 'email': 'tenant_email', # Tax params (also keep cfdi_* aliases in sync) diff --git a/pos/blueprints/invoicing_bp.py b/pos/blueprints/invoicing_bp.py index 40a02d5..6f83eef 100644 --- a/pos/blueprints/invoicing_bp.py +++ b/pos/blueprints/invoicing_bp.py @@ -46,6 +46,13 @@ def _get_issuer_config(cur, branch_id=None): "razon_social": config.get("tenant_razon_social", ""), "regimen_fiscal": config.get("cfdi_regimen_fiscal") or config.get("tenant_regimen_fiscal", "601"), "cp": config.get("tenant_cp", "00000"), + "direccion": config.get("tenant_direccion", ""), + "exterior": config.get("tenant_numero_exterior", ""), + "interior": config.get("tenant_numero_interior", ""), + "colonia": config.get("tenant_colonia", ""), + "ciudad": config.get("tenant_ciudad", ""), + "municipio": config.get("tenant_municipio", ""), + "estado": config.get("tenant_estado", ""), "serie": config.get("cfdi_serie") or config.get("invoice_serie", "A"), "facturapi_key": config.get("cfdi_facturapi_key", ""), "facturapi_org_id": config.get("cfdi_facturapi_org_id", ""), diff --git a/pos/services/facturapi_service.py b/pos/services/facturapi_service.py index 0b4abd4..8d29181 100644 --- a/pos/services/facturapi_service.py +++ b/pos/services/facturapi_service.py @@ -216,11 +216,27 @@ def create_organization(tenant_config: dict) -> dict: rfc = (tenant_config.get("rfc") or "").upper().strip() name = tenant_config.get("razon_social") or tenant_config.get("name") or rfc or "Nexus" - # First try to find existing org by RFC - existing = find_organization_by_rfc(tenant_config) if rfc else None - if existing: - org_id = existing["id"] - else: + # 1) Reuse the organization already stored locally, if it still exists in Facturapi. + local_org_id = _get_org_id(tenant_config) + if local_org_id: + try: + get_organization(local_org_id, user_key) + org_id = local_org_id + except FacturapiError: + local_org_id = None + + # 2) Try to find an existing organization by RFC (legacy / Horux-style lookup). + if not local_org_id and rfc: + existing = find_organization_by_rfc(tenant_config) + if existing: + org_id = existing["id"] + else: + payload = {"name": name} + org = _request("POST", "/organizations", user_key, json_payload=payload, timeout=60) + org_id = org.get("id") + if not org_id: + raise FacturapiError("Could not create organization: no id returned") + elif not local_org_id: payload = {"name": name} org = _request("POST", "/organizations", user_key, json_payload=payload, timeout=60) org_id = org.get("id") @@ -245,27 +261,49 @@ def create_organization(tenant_config: dict) -> dict: def _build_legal_payload(tenant_config: dict) -> dict: - """Build Facturapi /organizations/{id}/legal payload from tenant config.""" + """Build Facturapi /organizations/{id}/legal payload from tenant config. + + Facturapi expects `name`, `legal_name`, `tax_system` and an `address` object + with at least `street` and `exterior` non-empty. `tax_id` is not accepted + by this endpoint (it is set from the CSD or from the organization profile). + """ rfc = (tenant_config.get("rfc") or "").upper().strip() legal_name = (tenant_config.get("razon_social") or "").strip() tax_system = (tenant_config.get("regimen_fiscal") or "").strip() or "601" - zip_code = (tenant_config.get("cp") or tenant_config.get("tenant_cp") or "").strip() or "00000" + zip_code = (tenant_config.get("cp") or "").strip() or "00000" if not rfc or not legal_name: raise FacturapiError("RFC y Razón Social son obligatorios para configurar la organización en Facturapi") + + name = legal_name or rfc or "Nexus" + street = (tenant_config.get("direccion") or "").strip() or "No especificada" + exterior = ( + (tenant_config.get("exterior") or "").strip() + or (tenant_config.get("numero_exterior") or "").strip() + or "S/N" + ) + + address = { + "zip": zip_code, + "street": street, + "exterior": exterior, + } + + optional_address_fields = { + "interior": (tenant_config.get("interior") or tenant_config.get("numero_interior") or "").strip(), + "neighborhood": (tenant_config.get("colonia") or "").strip(), + "city": (tenant_config.get("ciudad") or "").strip(), + "municipality": (tenant_config.get("municipio") or "").strip(), + "state": (tenant_config.get("estado") or "").strip(), + } + for key, value in optional_address_fields.items(): + if value: + address[key] = value + return { - "tax_id": rfc, + "name": name, "legal_name": legal_name, "tax_system": tax_system, - "address": { - "zip": zip_code, - "street": (tenant_config.get("direccion") or "").strip(), - "exterior": "", - "interior": "", - "neighborhood": "", - "city": "", - "municipality": "", - "state": "", - }, + "address": address, } @@ -279,6 +317,22 @@ def update_organization_legal(tenant_config: dict, org_id: str) -> dict: return _request("PUT", f"/organizations/{org_id}/legal", user_key, json_payload=payload, timeout=60) +def _get_status_key(tenant_config: dict) -> str | None: + """Return the best key for read-only organization status checks. + + Prefer the Facturapi user key because the live secret key cannot read + organization metadata until the org is production-ready. + """ + user = _get_user_key() + if user: + return user + for key in ("facturapi_key", "cfdi_facturapi_key"): + tenant_key = (tenant_config.get(key) or "").strip() + if tenant_key.startswith("sk_user_"): + return tenant_key + return _get_secret_key(tenant_config) + + def get_org_status(tenant_config: dict) -> dict: result = { "configured": False, @@ -292,12 +346,12 @@ def get_org_status(tenant_config: dict) -> dict: "error": None, } - try: - api_key = get_api_key(tenant_config) - result["has_key"] = True - except FacturapiError as e: - result["error"] = str(e) + has_secret = bool(_get_secret_key(tenant_config)) + has_user = bool(_get_user_key_for_tenant(tenant_config)) + if not has_secret and not has_user: + result["error"] = "Facturapi not configured. Set FACTURAPI_USER_KEY env or tenant_config.facturapi_secret_key" return result + result["has_key"] = True org_id = _get_org_id(tenant_config) if not org_id: @@ -307,6 +361,11 @@ def get_org_status(tenant_config: dict) -> dict: result["has_org_id"] = True result["org_id"] = org_id + api_key = _get_status_key(tenant_config) + if not api_key: + result["error"] = "No Facturapi key available" + return result + def _fetch(): org = get_organization(org_id, api_key) legal = org.get("legal", {}) diff --git a/pos/static/js/invoicing.js b/pos/static/js/invoicing.js index 056b442..f69a5d6 100644 --- a/pos/static/js/invoicing.js +++ b/pos/static/js/invoicing.js @@ -397,6 +397,13 @@ const Invoicing = (() => { const cpEl = document.getElementById('cp-fiscal'); const razonEl = document.getElementById('razon-social'); const regimenEl = document.getElementById('regimen-fiscal'); + const direccionEl = document.getElementById('direccion-fiscal'); + const exteriorEl = document.getElementById('numero-exterior'); + const interiorEl = document.getElementById('numero-interior'); + const coloniaEl = document.getElementById('colonia-fiscal'); + const ciudadEl = document.getElementById('ciudad-fiscal'); + const municipioEl = document.getElementById('municipio-fiscal'); + const estadoEl = document.getElementById('estado-fiscal'); if (!rfcEl || !cpEl || !razonEl || !regimenEl) return; try { @@ -406,6 +413,13 @@ const Invoicing = (() => { rfcEl.value = data.rfc || ''; cpEl.value = data.cp || ''; razonEl.value = data.razon_social || ''; + if (direccionEl) direccionEl.value = data.direccion || ''; + if (exteriorEl) exteriorEl.value = data.numero_exterior || ''; + if (interiorEl) interiorEl.value = data.numero_interior || ''; + if (coloniaEl) coloniaEl.value = data.colonia || ''; + if (ciudadEl) ciudadEl.value = data.ciudad || ''; + if (municipioEl) municipioEl.value = data.municipio || ''; + if (estadoEl) estadoEl.value = data.estado || ''; const regimen = data.regimen_fiscal || '601'; regimenEl.value = regimen + ' — ' + (regimenEl.querySelector('option[value^="' + regimen + '"')?.textContent.split('—')[1]?.trim() || ''); // If exact value not matched, leave first option selected by SAT code prefix @@ -425,6 +439,13 @@ const Invoicing = (() => { const razon_social = (document.getElementById('razon-social')?.value || '').trim(); const regimenValue = document.getElementById('regimen-fiscal')?.value || '601'; const regimen_fiscal = regimenValue.split('—')[0].trim(); + const direccion = (document.getElementById('direccion-fiscal')?.value || '').trim(); + const numero_exterior = (document.getElementById('numero-exterior')?.value || '').trim(); + const numero_interior = (document.getElementById('numero-interior')?.value || '').trim(); + const colonia = (document.getElementById('colonia-fiscal')?.value || '').trim(); + const ciudad = (document.getElementById('ciudad-fiscal')?.value || '').trim(); + const municipio = (document.getElementById('municipio-fiscal')?.value || '').trim(); + const estado = (document.getElementById('estado-fiscal')?.value || '').trim(); const statusEl = document.getElementById('emisor-save-status'); if (!rfc || !razon_social || !cp) { @@ -436,7 +457,11 @@ const Invoicing = (() => { const res = await fetch('/pos/api/config/business', { method: 'PUT', headers: headers(), - body: JSON.stringify({ rfc, razon_social, regimen_fiscal, cp, cfdi_regimen_fiscal: regimen_fiscal, cfdi_serie: 'A' }) + body: JSON.stringify({ + rfc, razon_social, regimen_fiscal, cp, + direccion, numero_exterior, numero_interior, colonia, ciudad, municipio, estado, + cfdi_regimen_fiscal: regimen_fiscal, cfdi_serie: 'A' + }) }); if (!res.ok) { const err = await res.json().catch(() => ({ error: res.statusText })); @@ -483,6 +508,13 @@ const Invoicing = (() => { document.getElementById('csd-key-label').textContent = 'Subir llave privada .key'; } + function reloadManifiesto() { + const iframe = document.getElementById('manifiesto-iframe'); + if (iframe) { + iframe.src = iframe.src; + } + } + function updateFileLabels() { const cer = document.getElementById('csd-cer'); const key = document.getElementById('csd-key'); @@ -836,7 +868,7 @@ const Invoicing = (() => { showDetail, showCancelModal, confirmCancel, processQueue, showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder, openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice, setupFacturapi, - uploadCsd, resetCsdForm, + uploadCsd, resetCsdForm, reloadManifiesto, filterFacturas, exportFacturasCSV, exportNotasCSV, newCreditNote, newPaymentComplement, }; diff --git a/pos/static/pwa/sw.js b/pos/static/pwa/sw.js index e13f259..0d5bb88 100644 --- a/pos/static/pwa/sw.js +++ b/pos/static/pwa/sw.js @@ -6,7 +6,7 @@ // The fetch handler normalizes static asset URLs (strips ?v= query strings) // so templates can use cache-busting query params freely. -const VERSION = 41; +const VERSION = 43; const CACHE_NAME = 'nexus-pos-v' + VERSION; const APP_SHELL = [ diff --git a/pos/templates/invoicing.html b/pos/templates/invoicing.html index 66f0bc2..17cfa13 100644 --- a/pos/templates/invoicing.html +++ b/pos/templates/invoicing.html @@ -757,6 +757,41 @@ Régimen del emisor según el SAT +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
@@ -867,6 +902,44 @@
+ +
+
+ + + + + + + + Carta Manifiesto (SAT) +
+
+

+ Firma la carta manifiesto con tu FIEL (e.firma) para autorizar a Facturapi a timbrar CFDI ante el SAT. + Si no la ves, usa el botón para abrirla en una pestaña nueva. +

+
+ +
+
+ + Abrir portal de firma + + +
+
+
+