SIFEN — Facturación multi-moneda
Guía técnica para emitir DTEs en moneda extranjera (USD, EUR, BRL, ARS, CLP, etc.) en OnnixConnect. Escrita tras resolver los 4 errores que aparecen en cadena al enviar una FE multi-moneda a SIFEN TEST.
Estado: 5/5 monedas aprobadas (0260) con cotizaciones reales del BCP.
Contexto
Cuando la moneda de la operación no es PYG, SIFEN exige 4 piezas adicionales en el XML — y si falta cualquiera, rechaza. Las 4 tienen que estar simultáneamente correctas (sin tolerancia a errores parciales):
- Descripción oficial de la moneda (D016
dDesMoneOpe) coincidiendo con el catálogo ISO 4217 del XSDMonedas_v150.xsd. - Condición del tipo de cambio (D017
dCondTiCam) = 1 (global) o 2 (por ítem). - Tipo de cambio por forma de pago (E611
dTiCamTiPag) dentro de cada pago del grupogCamCond. - Total general en guaraníes (F023
dTotalGs) dentro degTotSub= dTotGralOpe × dTiCam.
Además, si dCondTiCam=1 (global) NO hay que emitir dTiCamItem (E725) ni dTotOpeGs (EA009) por ítem — ambos son exclusivos de D017=2.
Archivos involucrados
| Archivo | Rol |
|---|---|
app/Domains/Sifen/Enums/Moneda.php | Enum con códigos ISO 4217 y sus descripciones oficiales SIFEN (del XSD, en inglés). Cambiar una descripción rompe 1206. |
app/Domains/Sifen/Services/Xml/SifenXmlBuilder.php | prepareData() setea dCondTiCam, dTiCam, dTotalGs. prepareItem() maneja el flag emitirTotGs. |
resources/views/sifen/partials/g_dat_gral_ope.blade.php | Emite <cMoneOpe>, <dDesMoneOpe>, <dCondTiCam>, <dTiCam>. |
resources/views/sifen/partials/g_cam_cond.blade.php | Emite <dTiCamTiPag> dentro de <gPaConEIni> cuando moneda ≠ PYG. |
resources/views/sifen/partials/g_cam_item.blade.php | Emite <dTotOpeGs> solo si emitirTotGs=true (nunca con D017=1). |
resources/views/sifen/partials/g_tot_sub.blade.php | Emite <dTotalGs> al final de gTotSub cuando moneda ≠ PYG. Orden XSD crítico. |
docs/xsd/Monedas_v150.xsd | Fuente de verdad de las descripciones oficiales ISO 4217. |
Catálogo oficial SIFEN (más usadas en LATAM)
Estas son las descripciones textuales que acepta SIFEN — sensible a mayúsculas, acentos y espacios. No usar las descripciones del BCP ni traducirlas al español.
| Código | Descripción oficial | Nota |
|---|---|---|
| PYG | Guarani | Sin tilde |
| USD | US Dollar | No "Dólar Americano" |
| EUR | Euro | — |
| BRL | Brazilian Real | No "Real Brasileño" |
| ARS | Argentine Peso | No "Peso Argentino" |
| CLP | Chilean Peso | No "Peso Chileno" |
| COP | Colombian Peso | — |
| UYU | Peso Uruguayo | En español (así figura en el XSD) |
| BOB | Boliviano | — |
| PEN | Nuevo Sol | En español |
| GBP | Pound Sterling | — |
| JPY | Yen | No "Yen Japonés" |
| CNY | Yuan Renminbi | — |
| CAD | Canadian Dollar | — |
| AUD | Australian Dollar | — |
| CHF | Swiss Franc | — |
| MXN | Mexican Peso | — |
| HKD | Honk Kong Dollar | Con typo "Honk" — así está en el XSD oficial |
Si necesitás una moneda fuera de esta lista: abrir docs/xsd/Monedas_v150.xsd y buscar el <CodeName> del código ISO correspondiente.
Mapa de errores SIFEN y sus fixes
Secuencia típica que aparece cuando se activa multi-moneda por primera vez:
| # | Código | Campo | Causa | Fix |
|---|---|---|---|---|
| 1 | 1206 | D016 dDesMoneOpe | Descripción en español en vez de la del XSD | Enum Moneda::description() con nombres ISO 4217 (inglés) |
| 2 | 1556 | E611 dTiCamTiPag | Falta <dTiCamTiPag> dentro de <gPaConEIni> | Agregar en g_cam_cond.blade.php condicional a moneda ≠ PYG |
| 3 | 1858 | EA009 dTotOpeGs | Se emite por ítem aunque dCondTiCam=1 | Quitar: solo se emite cuando D017=2 (por ítem) |
| 4 | 2382 | F023 dTotalGs | Falta el total general en Gs en gTotSub | Agregar <dTotalGs> = dTotGralOpe × dTiCam al final de gTotSub |
Errores secundarios a tener en cuenta:
| Código | Campo | Observación |
|---|---|---|
| 0160 | — | "XML malformado [dXxx es invalido]" suele ser por orden incorrecto de elementos en gTotSub. Verificar orden XSD. |
| 1207 | D017 | Condición del tipo de cambio no informada cuando moneda ≠ PYG |
| 1208 | D017a | Condición del tipo de cambio informada cuando moneda = PYG |
| 1209 | D018 | dTiCam no informado cuando dCondTiCam=1 |
| 1555 | E610 | Descripción moneda por tipo de pago no coincide con código (mismo catálogo) |
| 1557 | E611a | dTiCamTiPag informado cuando moneda del pago = PYG |
Detalle técnico de cada fix
Fix 1 — Enum Moneda con descripciones del XSD
app/Domains/Sifen/Enums/Moneda.php:
public function description(): string
{
return match ($this) {
self::PYG => 'Guarani',
self::USD => 'US Dollar',
self::EUR => 'Euro',
self::BRL => 'Brazilian Real', // ← antes: 'Real Brasileño' (1206)
self::ARS => 'Argentine Peso', // ← antes: 'Peso Argentino' (1206)
self::CLP => 'Chilean Peso', // ← antes: 'Peso Chileno' (1206)
// ... resto del match
};
}
Fix 2 — dTiCamTiPag en gPaConEIni
resources/views/sifen/partials/g_cam_cond.blade.php:
<gPaConEIni>
<iTiPago>1</iTiPago>
<dDesTiPag>Efectivo</dDesTiPag>
<dMonTiPag>{{ $dMonTiPag }}</dMonTiPag>
<cMoneTiPag>{{ $cMoneOpe }}</cMoneTiPag>
<dDMoneTiPag>{{ $dDMoneTiPag }}</dDMoneTiPag>
@if(!empty($dTiCam) && $cMoneOpe !== 'PYG')
<dTiCamTiPag>{{ $dTiCam }}</dTiCamTiPag>
@endif
</gPaConEIni>
Fix 3 — No emitir dTotOpeGs por ítem cuando D017=1
Manual v150 línea 3803 (E725): "Obligatorio si D017 = 2. No informar si D017 = 1". Manual v150 línea 3887 (EA009): "Obligatorio si existe el campo E725".
Como OnnixConnect siempre usa dCondTiCam=1 (global), ni dTiCamItem ni dTotOpeGs deben ir en cada ítem. Solo dTotalGs al final.
app/Domains/Sifen/Services/Xml/SifenXmlBuilder.php en prepareItem():
// dTotOpeGs solo se emite si dCondTiCam=2 (por ítem). Como usamos
// global (=1), no debe emitirse — Manual v150 EA009 (No informar si D017=1).
'emitirTotGs' => false,
Fix 4 — dTotalGs en gTotSub con orden correcto
app/Domains/Sifen/Services/Xml/SifenXmlBuilder.php en prepareData():
'dTotalGs' => $moneda !== 'PYG'
? number_format(round($dTotGralOpe * $tipoCambio), 0, '.', '')
: null,
resources/views/sifen/partials/g_tot_sub.blade.php — después del bloque IVA:
<dTotGralOpe>{{ $dTotGralOpe }}</dTotGralOpe>
@if(!$esAFE)
<dIVA5>{{ $dIVA5 }}</dIVA5>
<dIVA10>{{ $dIVA10 }}</dIVA10>
... (resto campos IVA)
<dTBasGraIVA>{{ $dTBasGraIVA }}</dTBasGraIVA>
@endif
@if(!empty($dTotalGs))
<dTotalGs>{{ $dTotalGs }}</dTotalGs>
@endif
Orden XSD completo de gTotSub (respetar exactamente): dSubExe → dSubExo → dSub5 → dSub10 → dTotOpe → dTotDesc → dTotDescGlotem → dTotAntItem → dTotAnt → dPorcDescTotal → dDescTotal → dAnticipo → dRedon → dComi → dTotGralOpe → dIVA5 → dIVA10 → dLiqTotIVA5 → dLiqTotIVA10 → dIVAComi → dTotIVA → dBaseGrav5 → dBaseGrav10 → dTBasGraIVA → dTotalGs.
Poner dTotalGs fuera de ese orden produce 0160 — XML malformado [dXxx es invalido].
Resultado final
5 FE de prueba enviadas a SIFEN TEST el 2026-04-20 con cotizaciones del BCP:
| Moneda | TC Gs/ME | Total ME | Total Gs | DTE id | CDC |
|---|---|---|---|---|---|
| USD | 6.379,25 | 500,00 | 3.189.625 | 279 | 0260 |
| EUR | 7.230,00 | 400,00 | 2.892.000 | 280 | 0260 |
| BRL | 1.278,53 | 2.000,00 | 2.557.060 | 284 | 0260 |
| ARS | 4,70 | 50.000,00 | 235.000 | 282 | 0260 |
| CLP | 7,80 | 300.000,00 | 2.340.000 | 283 | 0260 |
Cómo probar multi-moneda end-to-end
Script de prueba
php artisan tinker --execute="require base_path('scripts/tinker_test_enviar_multimoneda.php');"
Envía 1 FE en cada una de las 5 monedas (USD/EUR/BRL/ARS/CLP) con cotizaciones del BCP y muestra resumen.
Verificación del XML generado
# Inspeccionar los campos relevantes del XML firmado
php artisan tinker --execute='
$dte = \App\Domains\Sifen\Models\SifenDte::where("moneda","!=","PYG")->latest()->first();
$xml = file_get_contents(\Storage::disk("local")->path($dte->xml_signed_path));
foreach (["cMoneOpe","dDesMoneOpe","dCondTiCam","dTiCam","dTiCamTiPag","dTotOpeGs","dTotalGs"] as $tag) {
if (preg_match("/<{$tag}>(.+?)<\/{$tag}>/", $xml, $m)) echo "$tag = {$m[1]}\n";
}
'
Salida esperada (ejemplo USD total 500):
cMoneOpe = USD
dDesMoneOpe = US Dollar
dCondTiCam = 1
dTiCam = 6379.2500
dTiCamTiPag = 6379.2500
dTotalGs = 3189625
Nota: dTotOpeGs (por ítem) NO debe aparecer — si aparece, revisar emitirTotGs en prepareItem().
Notas para debug
- No usar cotizaciones del BCP como referencia de descripciones. El BCP usa descripciones en español ("REAL BRASILEÑO", "PESO ARGENTINO") que no pasan la validación 1206. La fuente de verdad es
docs/xsd/Monedas_v150.xsd. - Preferir
dCondTiCam=1(global). Un solo tipo de cambio para todo el DTE. Más simple y menos campos que calcular. Usar D017=2 solo si hay razones de negocio (ej. facturas con ítems adquiridos en distintos momentos del día con tipos de cambio distintos). - Redondeo: el tipo de cambio usa 4 decimales fijos (
number_format($tc, 4, '.', '')). El total en guaraníes se redondea a entero (round($total * $tc)). - Orden XSD: validar contra
docs/xsd/DE_v150.xsdcuando aparece0160 — XML malformado.
Referencias
docs/xsd/Monedas_v150.xsd— catálogo oficial ISO 4217 con descripciones exactas.docs/manuales-md/Manual_SIFEN_v150_completo.md:- Líneas 2850-2880 — campos D015, D016, D017, D018 (grupo D1 moneda y tipo de cambio).
- Líneas 3800-3900 — campos E721, E725 (precio ítem, tipo de cambio ítem) y EA008, EA009 (total ítem, total ítem en Gs).
- Líneas 4685-4700 — campo F023
dTotalGs(total general en guaraníes). - Líneas 7100-7130 — validaciones 1204-1209 (IVA, moneda, tipo de cambio).
- Líneas 7510-7530 — validaciones 1554-1557 (moneda y tipo de cambio por forma de pago).
- Líneas 7660-7720 — validaciones 1853-1862 (cálculos de ítem).
- Líneas 8140-8170 — validaciones 2382-2386 (total general en Gs).