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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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", {})
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -757,6 +757,41 @@
|
||||
<span class="form-hint">Régimen del emisor según el SAT</span>
|
||||
</div>
|
||||
|
||||
<div class="form-field form-field--span2">
|
||||
<label class="form-label" for="direccion-fiscal">Calle</label>
|
||||
<input class="form-input" id="direccion-fiscal" type="text" value="" placeholder="Ej: Av. Insurgentes Sur" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="numero-exterior">Número Exterior</label>
|
||||
<input class="form-input" id="numero-exterior" type="text" value="" placeholder="Ej: 123" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="numero-interior">Número Interior</label>
|
||||
<input class="form-input" id="numero-interior" type="text" value="" placeholder="Ej: 4B" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="colonia-fiscal">Colonia</label>
|
||||
<input class="form-input" id="colonia-fiscal" type="text" value="" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="ciudad-fiscal">Ciudad</label>
|
||||
<input class="form-input" id="ciudad-fiscal" type="text" value="" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="municipio-fiscal">Municipio / Alcaldía</label>
|
||||
<input class="form-input" id="municipio-fiscal" type="text" value="" />
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-label" for="estado-fiscal">Estado</label>
|
||||
<input class="form-input" id="estado-fiscal" type="text" value="" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="margin-top:var(--space-4);display:flex;justify-content:flex-end;gap:var(--space-3);align-items:center;">
|
||||
@@ -867,6 +902,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CARTA MANIFIESTO -->
|
||||
<div class="config-section" style="grid-column: span 2;">
|
||||
<div class="config-section__header">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
<span class="config-section__title">Carta Manifiesto (SAT)</span>
|
||||
</div>
|
||||
<div class="config-section__body" id="manifiesto-panel">
|
||||
<p style="color:var(--color-text-muted); margin-bottom:var(--space-3);">
|
||||
Firma la carta manifiesto con tu <strong>FIEL</strong> (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.
|
||||
</p>
|
||||
<div style="border:1px solid var(--color-border); border-radius:var(--radius-md); overflow:hidden; background:var(--color-bg-base);">
|
||||
<iframe
|
||||
id="manifiesto-iframe"
|
||||
src="https://www.facturapi.io/embedded/manifiesto"
|
||||
title="Firma de Carta Manifiesto"
|
||||
style="width:100%; height:720px; border:0; display:block;"
|
||||
loading="lazy"
|
||||
allow="fullscreen"
|
||||
></iframe>
|
||||
</div>
|
||||
<div style="margin-top:var(--space-3); display:flex; gap:var(--space-3); justify-content:flex-end;">
|
||||
<a class="btn btn--ghost btn--sm" href="https://www.facturapi.io/manifiesto" target="_blank" rel="noopener">
|
||||
Abrir portal de firma
|
||||
</a>
|
||||
<button type="button" class="btn btn--secondary btn--sm" onclick="Invoicing.reloadManifiesto()">
|
||||
Recargar firma
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONFIGURACIÓN DE SERIES — full width -->
|
||||
<div class="config-section" style="grid-column: span 2;">
|
||||
<div class="config-section__header">
|
||||
|
||||
Reference in New Issue
Block a user