SDK y modelos: Python
Dataclasses sin dependencias externas. Cada modelo tiene to_dict() para armar el JSON del request y from_dict() para leer la respuesta. Descarga el zip completo y descomprímelo como un paquete (incluye __init__.py), por ejemplo como novafe_models.
Descargar
Sección titulada «Descargar»- ecf.py: emisión de e-CF, con sus tipos anidados.
- novafe-models-python.zip: todas las áreas de la API.
- documento OpenAPI: el contrato completo, para generar tus propios modelos.
Ejemplo: emitir un comprobante
Sección titulada «Ejemplo: emitir un comprobante»import osimport uuidfrom decimal import Decimal
import requests
from novafe_models.ecf import ( EcfBuyerPayload, EcfDto, EcfLinePayload, EcfPaymentMethodPayload, EcfPaymentPayload, IssueEcfCommand,)
payload = IssueEcfCommand( type=31, income_type="01", internal_number="FAC-2026-00042", buyer=EcfBuyerPayload(rnc="131880681", name="Mi Cliente SRL"), payment=EcfPaymentPayload( condition="credit", due_date="15-03-2026", methods=[EcfPaymentMethodPayload(type="check_transfer", amount=Decimal("2360"))], ), lines=[ EcfLinePayload( name="Servicio de consultoría", kind="service", quantity=Decimal("1"), unit_of_measure="43", unit_price=Decimal("2000"), itbis_rate=1, ) ],)
response = requests.post( "https://api.novafe.example/api/v1/ecf", json=payload.to_dict(), headers={ "X-API-Key": os.environ["NOVAFE_API_KEY"], "Idempotency-Key": str(uuid.uuid4()), },)response.raise_for_status()
ecf = EcfDto.from_dict(response.json())print(ecf.encf, ecf.status)Modelos de emisión
Sección titulada «Modelos de emisión»Todo el contenido de ecf.py, listo para copiar. Incluye IssueEcfCommand (el cuerpo de POST /ecf), EcfDto (la respuesta) y sus tipos anidados.
# Generado desde el documento OpenAPI publico de NovaFE. No lo edites a mano: regeneralo con `bun run models`.from __future__ import annotations
from dataclasses import dataclassfrom decimal import Decimalfrom typing import Any
from .common import _ser, _strip
@dataclassclass DgiiMessage: code: int value: str
def to_dict(self) -> dict[str, Any]: return _strip({ "code": _ser(self.code), "value": _ser(self.value), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "DgiiMessage": return cls( code=data["code"], value=data["value"], )
@dataclassclass DgiiTrackIdEntry: """Una entrada de `/{amb}/consultatrackids/api/trackids/consulta`. Puede haber más de una para el mismo e-NCF si se remitió varias veces."""
track_id: str estado: str fecha_recepcion: str | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "trackId": _ser(self.track_id), "estado": _ser(self.estado), "fechaRecepcion": _ser(self.fecha_recepcion), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "DgiiTrackIdEntry": return cls( track_id=data["trackId"], estado=data["estado"], fecha_recepcion=(data["fechaRecepcion"] if data.get("fechaRecepcion") is not None else None), )
@dataclassclass EcfAdditionalInfoPayload: """Texto libre para la Representación Impresa."""
issuer: str | None = None buyer: str | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "issuer": _ser(self.issuer), "buyer": _ser(self.buyer), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfAdditionalInfoPayload": return cls( issuer=(data["issuer"] if data.get("issuer") is not None else None), buyer=(data["buyer"] if data.get("buyer") is not None else None), )
@dataclassclass EcfAdditionalTaxPayload: """Desglose de impuestos adicionales por código de la Tabla I (001–039). Todos los montos los trae el cliente ya calculados. `iscEspecifico` (monto fijo por volumen: alcoholes 006-018, cigarrillos 019-022) integra la base imponible del ITBIS de esa línea; `iscAdvalorem` y `otros` (Propina, CDT, ISC de servicios, Primera Placa…) van "por encima", sin tocar la base."""
code: str rate: Decimal | None = None isc_especifico: Decimal | None = None isc_advalorem: Decimal | None = None otros: Decimal | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "code": _ser(self.code), "rate": _ser(self.rate), "iscEspecifico": _ser(self.isc_especifico), "iscAdvalorem": _ser(self.isc_advalorem), "otros": _ser(self.otros), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfAdditionalTaxPayload": return cls( code=data["code"], rate=(Decimal(str(data["rate"])) if data.get("rate") is not None else None), isc_especifico=(Decimal(str(data["iscEspecifico"])) if data.get("iscEspecifico") is not None else None), isc_advalorem=(Decimal(str(data["iscAdvalorem"])) if data.get("iscAdvalorem") is not None else None), otros=(Decimal(str(data["otros"])) if data.get("otros") is not None else None), )
@dataclassclass EcfAttemptDto: """Un renglón del historial de envíos de un comprobante: cómo terminó un ciclo contra la DGII. Lo que emite `GET /api/v1/ecf/{id}/attempts`."""
attempt: int """1, 2, 3… dentro del comprobante.""" outcome: str """En qué estado dejó el ciclo al comprobante: `accepted`, `accepted_conditional`, `rejected`, `review` o `failed`.""" messages: list[DgiiMessage] """Observaciones o motivo de rechazo de la DGII; o el motivo del fallo si no hubo veredicto.""" xml_sha256: str """SHA-256 (hex) del XML enviado en este ciclo.""" recorded_at: str """Cuándo se registró el resultado.""" track_id: str | None = None """`TrackId` de la DGII de este ciclo; ausente si no llegó a haber uno.""" status_code: int | None = None """Código de estado de la DGII (1 aceptado, 2 rechazado, 4 aceptado condicional); ausente si no hubo veredicto.""" status_text: str | None = None """El `estado` textual de la DGII.""" sequence_used: bool | None = None """`secuenciaUtilizada` de la DGII: `false` = el e-NCF no se consumió; `true` o ausente = consumido.""" retry_decision: str | None = None """Qué se decidió con un rechazo: `auto:<código>` (la DGII falló de forma transitoria y NovaFE reenvía el mismo XML solo, p. ej. `auto:4`) o `exhausted` (era reintentable pero se agotó el ladder y el rechazo quedó firme). Ausente en el resto de resultados."""
def to_dict(self) -> dict[str, Any]: return _strip({ "attempt": _ser(self.attempt), "outcome": _ser(self.outcome), "trackId": _ser(self.track_id), "statusCode": _ser(self.status_code), "statusText": _ser(self.status_text), "sequenceUsed": _ser(self.sequence_used), "messages": _ser(self.messages), "retryDecision": _ser(self.retry_decision), "xmlSha256": _ser(self.xml_sha256), "recordedAt": _ser(self.recorded_at), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfAttemptDto": return cls( attempt=data["attempt"], outcome=data["outcome"], track_id=(data["trackId"] if data.get("trackId") is not None else None), status_code=(data["statusCode"] if data.get("statusCode") is not None else None), status_text=(data["statusText"] if data.get("statusText") is not None else None), sequence_used=(data["sequenceUsed"] if data.get("sequenceUsed") is not None else None), messages=[DgiiMessage.from_dict(i) for i in data["messages"]], retry_decision=(data["retryDecision"] if data.get("retryDecision") is not None else None), xml_sha256=data["xmlSha256"], recorded_at=data["recordedAt"], )
@dataclassclass EcfBuyerPayload: """Bloque `<Comprador>`. `Rnc` y `ForeignId` son excluyentes."""
name: str | None = None rnc: str | None = None foreign_id: str | None = None email: str | None = None contact: str | None = None address: str | None = None municipality: str | None = None province: str | None = None additional_info: str | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "name": _ser(self.name), "rnc": _ser(self.rnc), "foreignId": _ser(self.foreign_id), "email": _ser(self.email), "contact": _ser(self.contact), "address": _ser(self.address), "municipality": _ser(self.municipality), "province": _ser(self.province), "additionalInfo": _ser(self.additional_info), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfBuyerPayload": return cls( name=(data["name"] if data.get("name") is not None else None), rnc=(data["rnc"] if data.get("rnc") is not None else None), foreign_id=(data["foreignId"] if data.get("foreignId") is not None else None), email=(data["email"] if data.get("email") is not None else None), contact=(data["contact"] if data.get("contact") is not None else None), address=(data["address"] if data.get("address") is not None else None), municipality=(data["municipality"] if data.get("municipality") is not None else None), province=(data["province"] if data.get("province") is not None else None), additional_info=(data["additionalInfo"] if data.get("additionalInfo") is not None else None), )
@dataclassclass EcfDeclaredTotalsPayload: """Totales que calculó el cliente (en DOP). Se comparan con el cálculo de NovaFE (tolerancia RF-06.6); el valor de NovaFE es el que va al XML. Todo opcional."""
monto_gravado_total: Decimal | None = None monto_exento: Decimal | None = None total_itbis: Decimal | None = None monto_impuesto_adicional: Decimal | None = None monto_total: Decimal | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "montoGravadoTotal": _ser(self.monto_gravado_total), "montoExento": _ser(self.monto_exento), "totalItbis": _ser(self.total_itbis), "montoImpuestoAdicional": _ser(self.monto_impuesto_adicional), "montoTotal": _ser(self.monto_total), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfDeclaredTotalsPayload": return cls( monto_gravado_total=(Decimal(str(data["montoGravadoTotal"])) if data.get("montoGravadoTotal") is not None else None), monto_exento=(Decimal(str(data["montoExento"])) if data.get("montoExento") is not None else None), total_itbis=(Decimal(str(data["totalItbis"])) if data.get("totalItbis") is not None else None), monto_impuesto_adicional=(Decimal(str(data["montoImpuestoAdicional"])) if data.get("montoImpuestoAdicional") is not None else None), monto_total=(Decimal(str(data["montoTotal"])) if data.get("montoTotal") is not None else None), )
@dataclassclass EcfDgiiExchange: """El intercambio con la DGII de un comprobante: lo que ella respondió más los instantes en que NovaFE registró cada paso."""
messages: list[DgiiMessage] """Observaciones o motivo de rechazo que devolvió la DGII.""" track_id: str | None = None """Identificador que la DGII asigna al recibir el comprobante.""" status: str | None = None """El `estado` textual de la DGII ("Aceptado", "Rechazado", "En Proceso"…); en el idioma en que lo manda ella.""" status_code: int | None = None """Código de estado de la DGII: 1 aceptado · 2 rechazado · 3 en proceso · 4 aceptado condicional.""" sequence_used: bool | None = None """`secuenciaUtilizada` de la DGII: `false` = el e-NCF no se consumió (firma/XML inválidos); `true` o ausente = consumido.""" submitted_at: str | None = None """Instante en que la DGII confirmó la recepción (hay string? EcfDgiiExchange.TrackId).""" received_at: str | None = None """La `fechaRecepcion` que informó la DGII; ausente si no la dio (p. ej. RFCE).""" processed_at: str | None = None """Instante en que la DGII dio un resultado definitivo."""
def to_dict(self) -> dict[str, Any]: return _strip({ "trackId": _ser(self.track_id), "status": _ser(self.status), "statusCode": _ser(self.status_code), "sequenceUsed": _ser(self.sequence_used), "messages": _ser(self.messages), "submittedAt": _ser(self.submitted_at), "receivedAt": _ser(self.received_at), "processedAt": _ser(self.processed_at), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfDgiiExchange": return cls( track_id=(data["trackId"] if data.get("trackId") is not None else None), status=(data["status"] if data.get("status") is not None else None), status_code=(data["statusCode"] if data.get("statusCode") is not None else None), sequence_used=(data["sequenceUsed"] if data.get("sequenceUsed") is not None else None), messages=[DgiiMessage.from_dict(i) for i in data["messages"]], submitted_at=(data["submittedAt"] if data.get("submittedAt") is not None else None), received_at=(data["receivedAt"] if data.get("receivedAt") is not None else None), processed_at=(data["processedAt"] if data.get("processedAt") is not None else None), )
@dataclassclass EcfDto: """Vista de un comprobante emitido (respuesta de `POST /ecf` y `GET /ecf/{id}`). Trae la identidad fiscal, el resumen comercial (comprador, monto total — igual que EcfSummaryDto) y el resultado del intercambio con la DGII (EcfDgiiExchange? EcfDto.Dgii). El desglose por línea y el resto del detalle comercial siguen viviendo solo en el XML firmado (`GET /ecf/{id}/xml`)."""
id: str status: str encf: str type: int environment: str issue_date: str issued_at: str signed_at: str security_code: str qr_url: str submits_rfce: bool signed_during_contingency: bool monto_total: Decimal document_hash: str sequence_expires_on: str | None = None internal_number: str | None = None tolerance_warning: str | None = None buyer_rnc: str | None = None buyer_name: str | None = None dgii: EcfDgiiExchange | None = None """Lo que pasó con la DGII: `trackId`, estado (texto y código), mensajes, `secuenciaUtilizada` y los instantes de envío, recepción y resolución. `null` mientras el comprobante no se haya enviado.""" retry: EcfRetry | None = None """El reintento automático en curso o ya hecho: cuántas veces NovaFE reenvió el mismo comprobante tras un rechazo transitorio de la DGII, el tope, y cuándo es el próximo intento. `null` mientras no haya habido ningún reintento.""" links: EcfLinks | None = None """Enlaces a los recursos relacionados del comprobante."""
def to_dict(self) -> dict[str, Any]: return _strip({ "id": _ser(self.id), "status": _ser(self.status), "encf": _ser(self.encf), "type": _ser(self.type), "environment": _ser(self.environment), "sequenceExpiresOn": _ser(self.sequence_expires_on), "issueDate": _ser(self.issue_date), "issuedAt": _ser(self.issued_at), "signedAt": _ser(self.signed_at), "securityCode": _ser(self.security_code), "qrUrl": _ser(self.qr_url), "submitsRfce": _ser(self.submits_rfce), "internalNumber": _ser(self.internal_number), "toleranceWarning": _ser(self.tolerance_warning), "signedDuringContingency": _ser(self.signed_during_contingency), "montoTotal": _ser(self.monto_total), "buyerRnc": _ser(self.buyer_rnc), "buyerName": _ser(self.buyer_name), "documentHash": _ser(self.document_hash), "dgii": _ser(self.dgii), "retry": _ser(self.retry), "links": _ser(self.links), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfDto": return cls( id=data["id"], status=data["status"], encf=data["encf"], type=data["type"], environment=data["environment"], sequence_expires_on=(data["sequenceExpiresOn"] if data.get("sequenceExpiresOn") is not None else None), issue_date=data["issueDate"], issued_at=data["issuedAt"], signed_at=data["signedAt"], security_code=data["securityCode"], qr_url=data["qrUrl"], submits_rfce=data["submitsRfce"], internal_number=(data["internalNumber"] if data.get("internalNumber") is not None else None), tolerance_warning=(data["toleranceWarning"] if data.get("toleranceWarning") is not None else None), signed_during_contingency=data["signedDuringContingency"], monto_total=Decimal(str(data["montoTotal"])), buyer_rnc=(data["buyerRnc"] if data.get("buyerRnc") is not None else None), buyer_name=(data["buyerName"] if data.get("buyerName") is not None else None), document_hash=data["documentHash"], dgii=(EcfDgiiExchange.from_dict(data["dgii"]) if data.get("dgii") is not None else None), retry=(EcfRetry.from_dict(data["retry"]) if data.get("retry") is not None else None), links=(EcfLinks.from_dict(data["links"]) if data.get("links") is not None else None), )
@dataclassclass EcfExportPayload: """Datos de exportación (solo tipo 46)."""
loading_port_name: str | None = None delivery_terms: str | None = None total_fob: Decimal | None = None insurance: Decimal | None = None freight: Decimal | None = None other_charges: Decimal | None = None total_cif: Decimal | None = None customs_regime: str | None = None departure_port_name: str | None = None unloading_port_name: str | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "loadingPortName": _ser(self.loading_port_name), "deliveryTerms": _ser(self.delivery_terms), "totalFob": _ser(self.total_fob), "insurance": _ser(self.insurance), "freight": _ser(self.freight), "otherCharges": _ser(self.other_charges), "totalCif": _ser(self.total_cif), "customsRegime": _ser(self.customs_regime), "departurePortName": _ser(self.departure_port_name), "unloadingPortName": _ser(self.unloading_port_name), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfExportPayload": return cls( loading_port_name=(data["loadingPortName"] if data.get("loadingPortName") is not None else None), delivery_terms=(data["deliveryTerms"] if data.get("deliveryTerms") is not None else None), total_fob=(Decimal(str(data["totalFob"])) if data.get("totalFob") is not None else None), insurance=(Decimal(str(data["insurance"])) if data.get("insurance") is not None else None), freight=(Decimal(str(data["freight"])) if data.get("freight") is not None else None), other_charges=(Decimal(str(data["otherCharges"])) if data.get("otherCharges") is not None else None), total_cif=(Decimal(str(data["totalCif"])) if data.get("totalCif") is not None else None), customs_regime=(data["customsRegime"] if data.get("customsRegime") is not None else None), departure_port_name=(data["departurePortName"] if data.get("departurePortName") is not None else None), unloading_port_name=(data["unloadingPortName"] if data.get("unloadingPortName") is not None else None), )
@dataclassclass EcfForeignCurrencyPayload: """Facturación en divisa (`<OtraMoneda>`). El cliente trae los montos ya convertidos."""
currency: str exchange_rate: Decimal totals: EcfForeignCurrencyTotalsPayload
def to_dict(self) -> dict[str, Any]: return _strip({ "currency": _ser(self.currency), "exchangeRate": _ser(self.exchange_rate), "totals": _ser(self.totals), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfForeignCurrencyPayload": return cls( currency=data["currency"], exchange_rate=Decimal(str(data["exchangeRate"])), totals=EcfForeignCurrencyTotalsPayload.from_dict(data["totals"]), )
@dataclassclass EcfForeignCurrencyTotalsPayload: monto_gravado_total: Decimal | None = None monto_gravado_i1: Decimal | None = None monto_gravado_i2: Decimal | None = None monto_gravado_i3: Decimal | None = None monto_exento: Decimal | None = None total_itbis: Decimal | None = None total_itbis1: Decimal | None = None total_itbis2: Decimal | None = None total_itbis3: Decimal | None = None monto_total: Decimal | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "montoGravadoTotal": _ser(self.monto_gravado_total), "montoGravadoI1": _ser(self.monto_gravado_i1), "montoGravadoI2": _ser(self.monto_gravado_i2), "montoGravadoI3": _ser(self.monto_gravado_i3), "montoExento": _ser(self.monto_exento), "totalItbis": _ser(self.total_itbis), "totalItbis1": _ser(self.total_itbis1), "totalItbis2": _ser(self.total_itbis2), "totalItbis3": _ser(self.total_itbis3), "montoTotal": _ser(self.monto_total), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfForeignCurrencyTotalsPayload": return cls( monto_gravado_total=(Decimal(str(data["montoGravadoTotal"])) if data.get("montoGravadoTotal") is not None else None), monto_gravado_i1=(Decimal(str(data["montoGravadoI1"])) if data.get("montoGravadoI1") is not None else None), monto_gravado_i2=(Decimal(str(data["montoGravadoI2"])) if data.get("montoGravadoI2") is not None else None), monto_gravado_i3=(Decimal(str(data["montoGravadoI3"])) if data.get("montoGravadoI3") is not None else None), monto_exento=(Decimal(str(data["montoExento"])) if data.get("montoExento") is not None else None), total_itbis=(Decimal(str(data["totalItbis"])) if data.get("totalItbis") is not None else None), total_itbis1=(Decimal(str(data["totalItbis1"])) if data.get("totalItbis1") is not None else None), total_itbis2=(Decimal(str(data["totalItbis2"])) if data.get("totalItbis2") is not None else None), total_itbis3=(Decimal(str(data["totalItbis3"])) if data.get("totalItbis3") is not None else None), monto_total=(Decimal(str(data["montoTotal"])) if data.get("montoTotal") is not None else None), )
@dataclassclass EcfGlobalAdjustmentPayload: """Un descuento o recargo global (Sección D)."""
line: int kind: str | None = None affects_itbis_rate: int | None = None amount: Decimal | None = None norm1007: bool | None = None description: str | None = None percentage: Decimal | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "line": _ser(self.line), "kind": _ser(self.kind), "affectsItbisRate": _ser(self.affects_itbis_rate), "amount": _ser(self.amount), "norm1007": _ser(self.norm1007), "description": _ser(self.description), "percentage": _ser(self.percentage), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfGlobalAdjustmentPayload": return cls( line=data["line"], kind=(data["kind"] if data.get("kind") is not None else None), affects_itbis_rate=(data["affectsItbisRate"] if data.get("affectsItbisRate") is not None else None), amount=(Decimal(str(data["amount"])) if data.get("amount") is not None else None), norm1007=(data["norm1007"] if data.get("norm1007") is not None else None), description=(data["description"] if data.get("description") is not None else None), percentage=(Decimal(str(data["percentage"])) if data.get("percentage") is not None else None), )
@dataclassclass EcfItemCodePayload: type: str value: str
def to_dict(self) -> dict[str, Any]: return _strip({ "type": _ser(self.type), "value": _ser(self.value), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfItemCodePayload": return cls( type=data["type"], value=data["value"], )
@dataclassclass EcfLineDetailsPayload: """Campos opcionales del `<Item>` — passthrough. La derivación del ISC desde `alcoholDegrees`/`referenceQuantity` es un slice posterior; hoy el cliente trae el monto final del ISC en `additionalTaxes` (el ISC específico ahí sí integra la base del ITBIS — ver EcfAdditionalTaxPayload)."""
reference_quantity: Decimal | None = None reference_unit: str | None = None subquantities: list[EcfSubquantityPayload] | None = None alcohol_degrees: Decimal | None = None reference_unit_price: Decimal | None = None manufacture_date: str | None = None expiry_date: str | None = None mining: EcfMiningPayload | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "referenceQuantity": _ser(self.reference_quantity), "referenceUnit": _ser(self.reference_unit), "subquantities": _ser(self.subquantities), "alcoholDegrees": _ser(self.alcohol_degrees), "referenceUnitPrice": _ser(self.reference_unit_price), "manufactureDate": _ser(self.manufacture_date), "expiryDate": _ser(self.expiry_date), "mining": _ser(self.mining), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfLineDetailsPayload": return cls( reference_quantity=(Decimal(str(data["referenceQuantity"])) if data.get("referenceQuantity") is not None else None), reference_unit=(data["referenceUnit"] if data.get("referenceUnit") is not None else None), subquantities=([EcfSubquantityPayload.from_dict(i) for i in data["subquantities"]] if data.get("subquantities") is not None else None), alcohol_degrees=(Decimal(str(data["alcoholDegrees"])) if data.get("alcoholDegrees") is not None else None), reference_unit_price=(Decimal(str(data["referenceUnitPrice"])) if data.get("referenceUnitPrice") is not None else None), manufacture_date=(data["manufactureDate"] if data.get("manufactureDate") is not None else None), expiry_date=(data["expiryDate"] if data.get("expiryDate") is not None else None), mining=(EcfMiningPayload.from_dict(data["mining"]) if data.get("mining") is not None else None), )
@dataclassclass EcfLineForeignCurrencyPayload: """`<OtraMonedaDetalle>` — precio y montos de la línea en divisa (passthrough)."""
unit_price: Decimal | None = None discount: Decimal | None = None surcharge: Decimal | None = None line_amount: Decimal | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "unitPrice": _ser(self.unit_price), "discount": _ser(self.discount), "surcharge": _ser(self.surcharge), "lineAmount": _ser(self.line_amount), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfLineForeignCurrencyPayload": return cls( unit_price=(Decimal(str(data["unitPrice"])) if data.get("unitPrice") is not None else None), discount=(Decimal(str(data["discount"])) if data.get("discount") is not None else None), surcharge=(Decimal(str(data["surcharge"])) if data.get("surcharge") is not None else None), line_amount=(Decimal(str(data["lineAmount"])) if data.get("lineAmount") is not None else None), )
@dataclassclass EcfLinePayload: """Una línea de `<DetallesItems>`."""
name: str | None = None kind: str | None = None quantity: Decimal | None = None unit_price: Decimal | None = None itbis_rate: int | None = None unit_of_measure: str | None = None description: str | None = None discount: Decimal | None = None surcharge: Decimal | None = None price_includes_tax: bool | None = None declared_amount: Decimal | None = None codes: list[EcfItemCodePayload] | None = None retention: EcfLineRetentionPayload | None = None additional_taxes: list[EcfAdditionalTaxPayload] | None = None foreign_currency: EcfLineForeignCurrencyPayload | None = None details: EcfLineDetailsPayload | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "name": _ser(self.name), "kind": _ser(self.kind), "quantity": _ser(self.quantity), "unitPrice": _ser(self.unit_price), "itbisRate": _ser(self.itbis_rate), "unitOfMeasure": _ser(self.unit_of_measure), "description": _ser(self.description), "discount": _ser(self.discount), "surcharge": _ser(self.surcharge), "priceIncludesTax": _ser(self.price_includes_tax), "declaredAmount": _ser(self.declared_amount), "codes": _ser(self.codes), "retention": _ser(self.retention), "additionalTaxes": _ser(self.additional_taxes), "foreignCurrency": _ser(self.foreign_currency), "details": _ser(self.details), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfLinePayload": return cls( name=(data["name"] if data.get("name") is not None else None), kind=(data["kind"] if data.get("kind") is not None else None), quantity=(Decimal(str(data["quantity"])) if data.get("quantity") is not None else None), unit_price=(Decimal(str(data["unitPrice"])) if data.get("unitPrice") is not None else None), itbis_rate=(data["itbisRate"] if data.get("itbisRate") is not None else None), unit_of_measure=(data["unitOfMeasure"] if data.get("unitOfMeasure") is not None else None), description=(data["description"] if data.get("description") is not None else None), discount=(Decimal(str(data["discount"])) if data.get("discount") is not None else None), surcharge=(Decimal(str(data["surcharge"])) if data.get("surcharge") is not None else None), price_includes_tax=(data["priceIncludesTax"] if data.get("priceIncludesTax") is not None else None), declared_amount=(Decimal(str(data["declaredAmount"])) if data.get("declaredAmount") is not None else None), codes=([EcfItemCodePayload.from_dict(i) for i in data["codes"]] if data.get("codes") is not None else None), retention=(EcfLineRetentionPayload.from_dict(data["retention"]) if data.get("retention") is not None else None), additional_taxes=([EcfAdditionalTaxPayload.from_dict(i) for i in data["additionalTaxes"]] if data.get("additionalTaxes") is not None else None), foreign_currency=(EcfLineForeignCurrencyPayload.from_dict(data["foreignCurrency"]) if data.get("foreignCurrency") is not None else None), details=(EcfLineDetailsPayload.from_dict(data["details"]) if data.get("details") is not None else None), )
@dataclassclass EcfLineRetentionPayload: """Área `<Retencion>` de la línea (tipos 41 y 47). Los montos los calcula el cliente."""
agent: str | None = None itbis_withheld: Decimal | None = None isr_withheld: Decimal | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "agent": _ser(self.agent), "itbisWithheld": _ser(self.itbis_withheld), "isrWithheld": _ser(self.isr_withheld), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfLineRetentionPayload": return cls( agent=(data["agent"] if data.get("agent") is not None else None), itbis_withheld=(Decimal(str(data["itbisWithheld"])) if data.get("itbisWithheld") is not None else None), isr_withheld=(Decimal(str(data["isrWithheld"])) if data.get("isrWithheld") is not None else None), )
@dataclassclass EcfLinks: """Enlaces (relativos) a los recursos del comprobante."""
self: str """El comprobante y su estado.""" xml: str """El XML firmado (`<ECF>`).""" representation: str """La Representación Impresa en PDF.""" rfce_xml: str | None = None """El resumen firmado (`<RFCE>`); solo cuando `submitsRfce`."""
def to_dict(self) -> dict[str, Any]: return _strip({ "self": _ser(self.self), "xml": _ser(self.xml), "rfceXml": _ser(self.rfce_xml), "representation": _ser(self.representation), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfLinks": return cls( self=data["self"], xml=data["xml"], rfce_xml=(data["rfceXml"] if data.get("rfceXml") is not None else None), representation=data["representation"], )
@dataclassclass EcfMiningPayload: """`<Mineria>` — datos de liquidación minera (solo tipos 32/33/34/46)."""
net_weight_kilogram: Decimal | None = None net_weight_mining: Decimal | None = None affiliation_type: int | None = None settlement: int | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "netWeightKilogram": _ser(self.net_weight_kilogram), "netWeightMining": _ser(self.net_weight_mining), "affiliationType": _ser(self.affiliation_type), "settlement": _ser(self.settlement), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfMiningPayload": return cls( net_weight_kilogram=(Decimal(str(data["netWeightKilogram"])) if data.get("netWeightKilogram") is not None else None), net_weight_mining=(Decimal(str(data["netWeightMining"])) if data.get("netWeightMining") is not None else None), affiliation_type=(data["affiliationType"] if data.get("affiliationType") is not None else None), settlement=(data["settlement"] if data.get("settlement") is not None else None), )
@dataclassclass EcfPagePayload: """Página de la RI (`<Paginacion>`)."""
number: int | None = None line_from: int | None = None line_to: int | None = None monto_gravado_total: Decimal | None = None monto_gravado_i1: Decimal | None = None monto_gravado_i2: Decimal | None = None monto_gravado_i3: Decimal | None = None monto_exento: Decimal | None = None total_itbis: Decimal | None = None itbis1: Decimal | None = None itbis2: Decimal | None = None itbis3: Decimal | None = None monto_impuesto_adicional: Decimal | None = None isc_especifico: Decimal | None = None otros_impuestos: Decimal | None = None amount: Decimal | None = None non_invoiceable_amount: Decimal | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "number": _ser(self.number), "lineFrom": _ser(self.line_from), "lineTo": _ser(self.line_to), "montoGravadoTotal": _ser(self.monto_gravado_total), "montoGravadoI1": _ser(self.monto_gravado_i1), "montoGravadoI2": _ser(self.monto_gravado_i2), "montoGravadoI3": _ser(self.monto_gravado_i3), "montoExento": _ser(self.monto_exento), "totalItbis": _ser(self.total_itbis), "itbis1": _ser(self.itbis1), "itbis2": _ser(self.itbis2), "itbis3": _ser(self.itbis3), "montoImpuestoAdicional": _ser(self.monto_impuesto_adicional), "iscEspecifico": _ser(self.isc_especifico), "otrosImpuestos": _ser(self.otros_impuestos), "amount": _ser(self.amount), "nonInvoiceableAmount": _ser(self.non_invoiceable_amount), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfPagePayload": return cls( number=(data["number"] if data.get("number") is not None else None), line_from=(data["lineFrom"] if data.get("lineFrom") is not None else None), line_to=(data["lineTo"] if data.get("lineTo") is not None else None), monto_gravado_total=(Decimal(str(data["montoGravadoTotal"])) if data.get("montoGravadoTotal") is not None else None), monto_gravado_i1=(Decimal(str(data["montoGravadoI1"])) if data.get("montoGravadoI1") is not None else None), monto_gravado_i2=(Decimal(str(data["montoGravadoI2"])) if data.get("montoGravadoI2") is not None else None), monto_gravado_i3=(Decimal(str(data["montoGravadoI3"])) if data.get("montoGravadoI3") is not None else None), monto_exento=(Decimal(str(data["montoExento"])) if data.get("montoExento") is not None else None), total_itbis=(Decimal(str(data["totalItbis"])) if data.get("totalItbis") is not None else None), itbis1=(Decimal(str(data["itbis1"])) if data.get("itbis1") is not None else None), itbis2=(Decimal(str(data["itbis2"])) if data.get("itbis2") is not None else None), itbis3=(Decimal(str(data["itbis3"])) if data.get("itbis3") is not None else None), monto_impuesto_adicional=(Decimal(str(data["montoImpuestoAdicional"])) if data.get("montoImpuestoAdicional") is not None else None), isc_especifico=(Decimal(str(data["iscEspecifico"])) if data.get("iscEspecifico") is not None else None), otros_impuestos=(Decimal(str(data["otrosImpuestos"])) if data.get("otrosImpuestos") is not None else None), amount=(Decimal(str(data["amount"])) if data.get("amount") is not None else None), non_invoiceable_amount=(Decimal(str(data["nonInvoiceableAmount"])) if data.get("nonInvoiceableAmount") is not None else None), )
@dataclassclass EcfPaymentMethodPayload: """Una forma de pago. `Type` y `Amount` son nullable para distinguir "no vino" de un valor: el validador exige ambos y la API nunca rellena uno por su cuenta."""
type: str | None = None amount: Decimal | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "type": _ser(self.type), "amount": _ser(self.amount), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfPaymentMethodPayload": return cls( type=(data["type"] if data.get("type") is not None else None), amount=(Decimal(str(data["amount"])) if data.get("amount") is not None else None), )
@dataclassclass EcfPaymentPayload: """Bloque de pago del encabezado."""
condition: str | None = None due_date: str | None = None methods: list[EcfPaymentMethodPayload] | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "condition": _ser(self.condition), "dueDate": _ser(self.due_date), "methods": _ser(self.methods), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfPaymentPayload": return cls( condition=(data["condition"] if data.get("condition") is not None else None), due_date=(data["dueDate"] if data.get("dueDate") is not None else None), methods=([EcfPaymentMethodPayload.from_dict(i) for i in data["methods"]] if data.get("methods") is not None else None), )
@dataclassclass EcfReferencePayload: """Sección `<InformacionReferencia>` — Notas de Crédito/Débito y reemplazos."""
modified_ncf: str modified_ncf_date: str modification_code: str | None = None other_issuer_rnc: str | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "modifiedNcf": _ser(self.modified_ncf), "modifiedNcfDate": _ser(self.modified_ncf_date), "modificationCode": _ser(self.modification_code), "otherIssuerRnc": _ser(self.other_issuer_rnc), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfReferencePayload": return cls( modified_ncf=data["modifiedNcf"], modified_ncf_date=data["modifiedNcfDate"], modification_code=(data["modificationCode"] if data.get("modificationCode") is not None else None), other_issuer_rnc=(data["otherIssuerRnc"] if data.get("otherIssuerRnc") is not None else None), )
@dataclassclass EcfRetry: """Reintento automático de un comprobante tras un rechazo transitorio de la DGII."""
count: int """Reenvíos automáticos que NovaFE ya hizo de este comprobante.""" max: int """Tope de reenvíos automáticos. Al agotarse, el rechazo queda firme.""" next_attempt_at: str | None = None """Cuándo es el próximo intento; `null` si no hay uno pendiente."""
def to_dict(self) -> dict[str, Any]: return _strip({ "count": _ser(self.count), "max": _ser(self.max), "nextAttemptAt": _ser(self.next_attempt_at), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfRetry": return cls( count=data["count"], max=data["max"], next_attempt_at=(data["nextAttemptAt"] if data.get("nextAttemptAt") is not None else None), )
@dataclassclass EcfShippingPayload: """Datos de embarque (`<InformacionesAdicionales>`). Passthrough."""
shipment_date: str | None = None shipment_number: str | None = None container_number: str | None = None reference_number: str | None = None gross_weight: Decimal | None = None net_weight: Decimal | None = None gross_weight_unit: str | None = None net_weight_unit: str | None = None package_count: Decimal | None = None package_unit: str | None = None volume: Decimal | None = None volume_unit: str | None = None export: EcfExportPayload | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "shipmentDate": _ser(self.shipment_date), "shipmentNumber": _ser(self.shipment_number), "containerNumber": _ser(self.container_number), "referenceNumber": _ser(self.reference_number), "grossWeight": _ser(self.gross_weight), "netWeight": _ser(self.net_weight), "grossWeightUnit": _ser(self.gross_weight_unit), "netWeightUnit": _ser(self.net_weight_unit), "packageCount": _ser(self.package_count), "packageUnit": _ser(self.package_unit), "volume": _ser(self.volume), "volumeUnit": _ser(self.volume_unit), "export": _ser(self.export), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfShippingPayload": return cls( shipment_date=(data["shipmentDate"] if data.get("shipmentDate") is not None else None), shipment_number=(data["shipmentNumber"] if data.get("shipmentNumber") is not None else None), container_number=(data["containerNumber"] if data.get("containerNumber") is not None else None), reference_number=(data["referenceNumber"] if data.get("referenceNumber") is not None else None), gross_weight=(Decimal(str(data["grossWeight"])) if data.get("grossWeight") is not None else None), net_weight=(Decimal(str(data["netWeight"])) if data.get("netWeight") is not None else None), gross_weight_unit=(data["grossWeightUnit"] if data.get("grossWeightUnit") is not None else None), net_weight_unit=(data["netWeightUnit"] if data.get("netWeightUnit") is not None else None), package_count=(Decimal(str(data["packageCount"])) if data.get("packageCount") is not None else None), package_unit=(data["packageUnit"] if data.get("packageUnit") is not None else None), volume=(Decimal(str(data["volume"])) if data.get("volume") is not None else None), volume_unit=(data["volumeUnit"] if data.get("volumeUnit") is not None else None), export=(EcfExportPayload.from_dict(data["export"]) if data.get("export") is not None else None), )
@dataclassclass EcfSubquantityPayload: quantity: Decimal unit_code: str
def to_dict(self) -> dict[str, Any]: return _strip({ "quantity": _ser(self.quantity), "unitCode": _ser(self.unit_code), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfSubquantityPayload": return cls( quantity=Decimal(str(data["quantity"])), unit_code=data["unitCode"], )
@dataclassclass EcfSubtotalPayload: """Subtotal informativo para la RI (Sección C). No afecta la base imponible."""
number: int | None = None description: str | None = None order: int | None = None monto_gravado_total: Decimal | None = None monto_gravado_i1: Decimal | None = None monto_gravado_i2: Decimal | None = None monto_gravado_i3: Decimal | None = None total_itbis: Decimal | None = None itbis1: Decimal | None = None itbis2: Decimal | None = None itbis3: Decimal | None = None monto_impuesto_adicional: Decimal | None = None monto_exento: Decimal | None = None amount: Decimal | None = None lines: int | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "number": _ser(self.number), "description": _ser(self.description), "order": _ser(self.order), "montoGravadoTotal": _ser(self.monto_gravado_total), "montoGravadoI1": _ser(self.monto_gravado_i1), "montoGravadoI2": _ser(self.monto_gravado_i2), "montoGravadoI3": _ser(self.monto_gravado_i3), "totalItbis": _ser(self.total_itbis), "itbis1": _ser(self.itbis1), "itbis2": _ser(self.itbis2), "itbis3": _ser(self.itbis3), "montoImpuestoAdicional": _ser(self.monto_impuesto_adicional), "montoExento": _ser(self.monto_exento), "amount": _ser(self.amount), "lines": _ser(self.lines), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfSubtotalPayload": return cls( number=(data["number"] if data.get("number") is not None else None), description=(data["description"] if data.get("description") is not None else None), order=(data["order"] if data.get("order") is not None else None), monto_gravado_total=(Decimal(str(data["montoGravadoTotal"])) if data.get("montoGravadoTotal") is not None else None), monto_gravado_i1=(Decimal(str(data["montoGravadoI1"])) if data.get("montoGravadoI1") is not None else None), monto_gravado_i2=(Decimal(str(data["montoGravadoI2"])) if data.get("montoGravadoI2") is not None else None), monto_gravado_i3=(Decimal(str(data["montoGravadoI3"])) if data.get("montoGravadoI3") is not None else None), total_itbis=(Decimal(str(data["totalItbis"])) if data.get("totalItbis") is not None else None), itbis1=(Decimal(str(data["itbis1"])) if data.get("itbis1") is not None else None), itbis2=(Decimal(str(data["itbis2"])) if data.get("itbis2") is not None else None), itbis3=(Decimal(str(data["itbis3"])) if data.get("itbis3") is not None else None), monto_impuesto_adicional=(Decimal(str(data["montoImpuestoAdicional"])) if data.get("montoImpuestoAdicional") is not None else None), monto_exento=(Decimal(str(data["montoExento"])) if data.get("montoExento") is not None else None), amount=(Decimal(str(data["amount"])) if data.get("amount") is not None else None), lines=(data["lines"] if data.get("lines") is not None else None), )
@dataclassclass EcfSummaryDto: """Fila del listado de comprobantes emitidos."""
id: str status: str encf: str type: int environment: str issue_date: str monto_total: Decimal created_at: str buyer_rnc: str | None = None buyer_name: str | None = None retry: EcfRetry | None = None """Reintento automático (ver EcfRetry? EcfDto.Retry); `null` si no hubo ninguno."""
def to_dict(self) -> dict[str, Any]: return _strip({ "id": _ser(self.id), "status": _ser(self.status), "encf": _ser(self.encf), "type": _ser(self.type), "environment": _ser(self.environment), "issueDate": _ser(self.issue_date), "montoTotal": _ser(self.monto_total), "buyerRnc": _ser(self.buyer_rnc), "buyerName": _ser(self.buyer_name), "createdAt": _ser(self.created_at), "retry": _ser(self.retry), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfSummaryDto": return cls( id=data["id"], status=data["status"], encf=data["encf"], type=data["type"], environment=data["environment"], issue_date=data["issueDate"], monto_total=Decimal(str(data["montoTotal"])), buyer_rnc=(data["buyerRnc"] if data.get("buyerRnc") is not None else None), buyer_name=(data["buyerName"] if data.get("buyerName") is not None else None), created_at=data["createdAt"], retry=(EcfRetry.from_dict(data["retry"]) if data.get("retry") is not None else None), )
@dataclassclass EcfTransportPayload: """Datos de transporte (`<Transporte>`). Passthrough."""
driver: str | None = None transport_document: str | None = None vehicle_id: str | None = None plate: str | None = None route: str | None = None zone: str | None = None delivery_note: str | None = None via: str | None = None origin_country: str | None = None destination_address: str | None = None destination_country: str | None = None carrier_rnc: str | None = None carrier_name: str | None = None voyage_number: str | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "driver": _ser(self.driver), "transportDocument": _ser(self.transport_document), "vehicleId": _ser(self.vehicle_id), "plate": _ser(self.plate), "route": _ser(self.route), "zone": _ser(self.zone), "deliveryNote": _ser(self.delivery_note), "via": _ser(self.via), "originCountry": _ser(self.origin_country), "destinationAddress": _ser(self.destination_address), "destinationCountry": _ser(self.destination_country), "carrierRnc": _ser(self.carrier_rnc), "carrierName": _ser(self.carrier_name), "voyageNumber": _ser(self.voyage_number), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "EcfTransportPayload": return cls( driver=(data["driver"] if data.get("driver") is not None else None), transport_document=(data["transportDocument"] if data.get("transportDocument") is not None else None), vehicle_id=(data["vehicleId"] if data.get("vehicleId") is not None else None), plate=(data["plate"] if data.get("plate") is not None else None), route=(data["route"] if data.get("route") is not None else None), zone=(data["zone"] if data.get("zone") is not None else None), delivery_note=(data["deliveryNote"] if data.get("deliveryNote") is not None else None), via=(data["via"] if data.get("via") is not None else None), origin_country=(data["originCountry"] if data.get("originCountry") is not None else None), destination_address=(data["destinationAddress"] if data.get("destinationAddress") is not None else None), destination_country=(data["destinationCountry"] if data.get("destinationCountry") is not None else None), carrier_rnc=(data["carrierRnc"] if data.get("carrierRnc") is not None else None), carrier_name=(data["carrierName"] if data.get("carrierName") is not None else None), voyage_number=(data["voyageNumber"] if data.get("voyageNumber") is not None else None), )
@dataclassclass IssueEcfCommand: """Payload de emisión de un e-CF (`POST /api/v1/ecf`). Un solo objeto discriminado por int IssueEcfCommand.Type; el servidor asigna la secuencia, arma el bloque Emisor desde el perfil del tenant, calcula los totales, firma y persiste."""
type: int """Código DGII del tipo de e-CF: 31, 32, 33, 34, 41, 43, 44, 45, 46, 47.""" lines: list[EcfLinePayload] encf: str | None = None """e-NCF de 13 caracteres. Solo con `sequences.mode = external` (y obligatorio ahí); con `managed` se rechaza, NovaFE asigna el suyo.""" sequence_expires_on: str | None = None """`<FechaVencimientoSecuencia>` del e-NCF. Con `sequences.mode = external` es obligatorio en los tipos que llevan vencimiento (todos menos 32 y 34) y no aplica en esos dos; en `managed` se rechaza.""" issue_date: str | None = None """Fecha de emisión (calendario dominicano). Default: hoy.""" income_type: str | None = None """`<TipoIngresos>` — "01"…"06". Obligatorio en 31/32/33/34/44/45/46.""" prices_include_tax: bool | None = None """`true` si los precios de las líneas ya traen el ITBIS incluido.""" deferred_delivery: bool | None = None """`<IndicadorEnvioDiferido>` — solo contribuyentes autorizados.""" non_invoiceable_amount: Decimal | None = None """`<MontoNoFacturable>` — reembolsos, propina voluntaria. Puede ser negativo.""" internal_number: str | None = None """`<NumeroFacturaInterna>` — clave de dedup de negocio.""" seller_code: str | None = None """`<CodigoVendedor>`.""" additional_info: EcfAdditionalInfoPayload | None = None buyer: EcfBuyerPayload | None = None payment: EcfPaymentPayload | None = None reference: EcfReferencePayload | None = None global_adjustments: list[EcfGlobalAdjustmentPayload] | None = None foreign_currency: EcfForeignCurrencyPayload | None = None shipping: EcfShippingPayload | None = None transport: EcfTransportPayload | None = None subtotals: list[EcfSubtotalPayload] | None = None pagination: list[EcfPagePayload] | None = None declared_totals: EcfDeclaredTotalsPayload | None = None """Totales declarados por el cliente (chequeo de tolerancia; nunca bloquea)."""
def to_dict(self) -> dict[str, Any]: return _strip({ "type": _ser(self.type), "encf": _ser(self.encf), "sequenceExpiresOn": _ser(self.sequence_expires_on), "issueDate": _ser(self.issue_date), "incomeType": _ser(self.income_type), "pricesIncludeTax": _ser(self.prices_include_tax), "deferredDelivery": _ser(self.deferred_delivery), "nonInvoiceableAmount": _ser(self.non_invoiceable_amount), "internalNumber": _ser(self.internal_number), "sellerCode": _ser(self.seller_code), "additionalInfo": _ser(self.additional_info), "buyer": _ser(self.buyer), "payment": _ser(self.payment), "lines": _ser(self.lines), "reference": _ser(self.reference), "globalAdjustments": _ser(self.global_adjustments), "foreignCurrency": _ser(self.foreign_currency), "shipping": _ser(self.shipping), "transport": _ser(self.transport), "subtotals": _ser(self.subtotals), "pagination": _ser(self.pagination), "declaredTotals": _ser(self.declared_totals), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "IssueEcfCommand": return cls( type=data["type"], encf=(data["encf"] if data.get("encf") is not None else None), sequence_expires_on=(data["sequenceExpiresOn"] if data.get("sequenceExpiresOn") is not None else None), issue_date=(data["issueDate"] if data.get("issueDate") is not None else None), income_type=(data["incomeType"] if data.get("incomeType") is not None else None), prices_include_tax=(data["pricesIncludeTax"] if data.get("pricesIncludeTax") is not None else None), deferred_delivery=(data["deferredDelivery"] if data.get("deferredDelivery") is not None else None), non_invoiceable_amount=(Decimal(str(data["nonInvoiceableAmount"])) if data.get("nonInvoiceableAmount") is not None else None), internal_number=(data["internalNumber"] if data.get("internalNumber") is not None else None), seller_code=(data["sellerCode"] if data.get("sellerCode") is not None else None), additional_info=(EcfAdditionalInfoPayload.from_dict(data["additionalInfo"]) if data.get("additionalInfo") is not None else None), buyer=(EcfBuyerPayload.from_dict(data["buyer"]) if data.get("buyer") is not None else None), payment=(EcfPaymentPayload.from_dict(data["payment"]) if data.get("payment") is not None else None), lines=[EcfLinePayload.from_dict(i) for i in data["lines"]], reference=(EcfReferencePayload.from_dict(data["reference"]) if data.get("reference") is not None else None), global_adjustments=([EcfGlobalAdjustmentPayload.from_dict(i) for i in data["globalAdjustments"]] if data.get("globalAdjustments") is not None else None), foreign_currency=(EcfForeignCurrencyPayload.from_dict(data["foreignCurrency"]) if data.get("foreignCurrency") is not None else None), shipping=(EcfShippingPayload.from_dict(data["shipping"]) if data.get("shipping") is not None else None), transport=(EcfTransportPayload.from_dict(data["transport"]) if data.get("transport") is not None else None), subtotals=([EcfSubtotalPayload.from_dict(i) for i in data["subtotals"]] if data.get("subtotals") is not None else None), pagination=([EcfPagePayload.from_dict(i) for i in data["pagination"]] if data.get("pagination") is not None else None), declared_totals=(EcfDeclaredTotalsPayload.from_dict(data["declaredTotals"]) if data.get("declaredTotals") is not None else None), )
@dataclassclass PagedResultOfEcfSummaryDto: items: list[EcfSummaryDto] total_count: int page: int page_size: int total_pages: int | None = None has_next_page: bool | None = None has_previous_page: bool | None = None
def to_dict(self) -> dict[str, Any]: return _strip({ "items": _ser(self.items), "totalCount": _ser(self.total_count), "page": _ser(self.page), "pageSize": _ser(self.page_size), "totalPages": _ser(self.total_pages), "hasNextPage": _ser(self.has_next_page), "hasPreviousPage": _ser(self.has_previous_page), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "PagedResultOfEcfSummaryDto": return cls( items=[EcfSummaryDto.from_dict(i) for i in data["items"]], total_count=data["totalCount"], page=data["page"], page_size=data["pageSize"], total_pages=(data["totalPages"] if data.get("totalPages") is not None else None), has_next_page=(data["hasNextPage"] if data.get("hasNextPage") is not None else None), has_previous_page=(data["hasPreviousPage"] if data.get("hasPreviousPage") is not None else None), )
@dataclassclass RepresentationLayout: pass
def to_dict(self) -> dict[str, Any]: return _strip({ })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "RepresentationLayout": return cls( )
@dataclassclass ValidateEcfIssueDto: """Un error o aviso de `POST /api/v1/ecf/validate`."""
code: str """Código estable, apto para lógica del cliente.""" message: str """Descripción lista para mostrar.""" field: str | None = None """Campo del payload al que se refiere (p. ej. `Lines[0].Quantity`); `null` si es de todo el comprobante."""
def to_dict(self) -> dict[str, Any]: return _strip({ "field": _ser(self.field), "code": _ser(self.code), "message": _ser(self.message), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "ValidateEcfIssueDto": return cls( field=(data["field"] if data.get("field") is not None else None), code=data["code"], message=data["message"], )
@dataclassclass ValidateEcfPreviewDto: """Vista previa de `POST /api/v1/ecf/validate` para un payload válido: pasó la matriz de validación por tipo (Módulo 2) y el cálculo de totales (Módulo 6) sin asignar una secuencia real, firmar ni persistir nada."""
type: int type_name: str sample_encf: str """Placeholder (`E{tipo}0000000000`) — nunca un e-NCF real, no se asignó ninguna secuencia para construir este documento.""" issue_date: str monto_gravado_total: Decimal monto_exento: Decimal total_itbis: Decimal monto_impuesto_adicional: Decimal monto_total: Decimal expect_conditional_acceptance: bool """Los totales quedarían fuera de la tolerancia de cuadratura (RF-06.6) — la DGII probablemente aceptaría el comprobante de forma condicional si se emite así.""" sequence_expires_on_estimate: str | None = None """Estimado (31-dic del año siguiente a DateOnly ValidateEcfPreviewDto.IssueDate) cuando el tipo lo lleva; no viene de una secuencia real. `null` en los tipos que no lo llevan (32, 34)."""
def to_dict(self) -> dict[str, Any]: return _strip({ "type": _ser(self.type), "typeName": _ser(self.type_name), "sampleEncf": _ser(self.sample_encf), "issueDate": _ser(self.issue_date), "sequenceExpiresOnEstimate": _ser(self.sequence_expires_on_estimate), "montoGravadoTotal": _ser(self.monto_gravado_total), "montoExento": _ser(self.monto_exento), "totalItbis": _ser(self.total_itbis), "montoImpuestoAdicional": _ser(self.monto_impuesto_adicional), "montoTotal": _ser(self.monto_total), "expectConditionalAcceptance": _ser(self.expect_conditional_acceptance), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "ValidateEcfPreviewDto": return cls( type=data["type"], type_name=data["typeName"], sample_encf=data["sampleEncf"], issue_date=data["issueDate"], sequence_expires_on_estimate=(data["sequenceExpiresOnEstimate"] if data.get("sequenceExpiresOnEstimate") is not None else None), monto_gravado_total=Decimal(str(data["montoGravadoTotal"])), monto_exento=Decimal(str(data["montoExento"])), total_itbis=Decimal(str(data["totalItbis"])), monto_impuesto_adicional=Decimal(str(data["montoImpuestoAdicional"])), monto_total=Decimal(str(data["montoTotal"])), expect_conditional_acceptance=data["expectConditionalAcceptance"], )
@dataclassclass ValidateEcfResultDto: """Resultado de `POST /api/v1/ecf/validate`. Siempre llega con `200` cuando el cuerpo se pudo leer y el contribuyente puede validar: bool ValidateEcfResultDto.Valid dice si el comprobante pasaría la validación de forma y la matriz estructural y fiscal por tipo (Módulo 2 + 6) que corre `POST /ecf`. No asigna una secuencia real, no firma y no persiste nada. Lo que impide validar (sin autenticar, perfil del emisor sin configurar) sigue siendo un error HTTP."""
valid: bool """`true` si el comprobante se podría emitir tal cual en cuanto a su contenido.""" message: str """Resumen en una frase, listo para mostrar.""" errors: list[ValidateEcfIssueDto] """Todos los problemas encontrados, no solo el primero. Vacío si bool ValidateEcfResultDto.Valid.""" warnings: list[ValidateEcfIssueDto] """Avisos que no impiden emitir (p. ej. aceptación condicional probable).""" preview: ValidateEcfPreviewDto | None = None """La vista previa de lo que se emitiría; `null` si el comprobante no es válido."""
def to_dict(self) -> dict[str, Any]: return _strip({ "valid": _ser(self.valid), "message": _ser(self.message), "errors": _ser(self.errors), "warnings": _ser(self.warnings), "preview": _ser(self.preview), })
@classmethod def from_dict(cls, data: dict[str, Any]) -> "ValidateEcfResultDto": return cls( valid=data["valid"], message=data["message"], errors=[ValidateEcfIssueDto.from_dict(i) for i in data["errors"]], warnings=[ValidateEcfIssueDto.from_dict(i) for i in data["warnings"]], preview=(ValidateEcfPreviewDto.from_dict(data["preview"]) if data.get("preview") is not None else None), )