From 8769dc08fca17584f99dc8737ea22c5fa13a0220 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Thu, 21 Aug 2025 20:26:23 -0600 Subject: [PATCH] version 4.0.270 --- examples.py | 118 +++++- fiscalapi/__init__.py | 37 +- fiscalapi/models/fiscalapi_models.py | 345 ++++++++++++++++++ .../services/download_catalog_service.py | 32 ++ .../services/download_request_service.py | 213 +++++++++++ fiscalapi/services/download_rule_service.py | 37 ++ fiscalapi/services/fiscalapi_client.py | 6 + setup.py | 13 +- 8 files changed, 789 insertions(+), 12 deletions(-) create mode 100644 fiscalapi/services/download_catalog_service.py create mode 100644 fiscalapi/services/download_request_service.py create mode 100644 fiscalapi/services/download_rule_service.py diff --git a/examples.py b/examples.py index 688b3a0..a872a23 100644 --- a/examples.py +++ b/examples.py @@ -1,7 +1,7 @@ -from datetime import datetime +from datetime import datetime, timedelta from decimal import Decimal from fiscalapi.models.common_models import FiscalApiSettings -from fiscalapi.models.fiscalapi_models import Invoice, InvoiceIssuer, InvoiceItem, InvoiceRecipient, ItemTax, Product, ProductTax, Person, RelatedInvoice, TaxCredential, TaxFile +from fiscalapi.models.fiscalapi_models import ApiKey, CancelInvoiceRequest, CreatePdfRequest, DownloadRequest, DownloadRule, GlobalInformation, Invoice, InvoiceIssuer, InvoiceItem, InvoicePayment, InvoiceRecipient, InvoiceStatusRequest, ItemTax, PaidInvoice, PaidInvoiceTax, Product, ProductTax, Person, RelatedInvoice, SendInvoiceRequest, TaxCredential, TaxFile from fiscalapi.services.fiscalapi_client import FiscalApiClient def main (): @@ -19,7 +19,7 @@ def main (): - # listar api-keys + # listar api-keys # api_response = client.api_keys.get_list(1, 10) # print(api_response) @@ -1359,6 +1359,118 @@ def main (): # print(api_response) + # ======================================== + # EJEMPLOS DE DESCARGA MASIVA + # ======================================== + + # ======================================== + # CATÁLOGOS + # ======================================== + + #Obtener todos los catálogos de descarga masiva disponibles + # api_response = client.download_catalogs.get_list() + # print(api_response) + + #Listar los registros del catálogo 'SatInvoiceStatuses' de descarga masiva. + # api_response = client.download_catalogs.list_catalog("SatInvoiceStatuses") + # print(api_response) + + + # ======================================== + # Reglas de descarga + # ======================================== + + # Obtener lista paginada de reglas de descarga masiva + # api_response = client.download_rules.get_list(1, 2) + # print(api_response) + + + # Obtener regla de descarga masiva por id + # api_response = client.download_rules.get_by_id("dc342a5e-f5f2-48d7-a5c5-1d87df46ef26") + # print(api_response) + + + # Crear Regla para descargar CFDI recibidos y vigentes. + # request = DownloadRule( + # person_id="b0c1cf6c-153a-464e-99df-5741f45d6695", + # description="Regla descarga demo ...", + # sat_query_type_id="CFDI", + # download_type_id="Recibidos", + # sat_invoice_status_id="Vigente", + # ) + # api_response = client.download_rules.create(request) + # print(api_response) + + # Crear Regla de prueba para descargar CFDI recibidos y vigentes. + # api_response = client.download_rules.create_test_rule() + # print(api_response) + + # Actualizar regla de descarga masiva + # request = DownloadRule( + # id = "e480dcb6-529a-464b-b3e5-854e2f65e63a", + # description = "Regla descarga actualizada", + # ) + # api_response = client.download_rules.update(request); + # print(api_response) + + # Eliminar regla de descarga masiva + # api_response = client.download_rules.delete("e480dcb6-529a-464b-b3e5-854e2f65e63a") + # print(api_response) + + # ======================================== + # Solicitudes de descarga + # ======================================== + + # Obtener lista paginada de solicitudes de descarga masiva + # api_response = client.download_requests.get_list(1, 2) + # print(api_response) + + + # Obtener solicitud por ID + # api_response = client.download_requests.get_by_id("5f71344a-3b5a-4e36-9228-57170d18c64a") + # print(api_response) + + # Obtener lista paginada de xmls descargados asociados a una solicitud de descarga. + # api_response = client.download_requests.get_xmls("5f71344a-3b5a-4e36-9228-57170d18c64a") + # print(api_response) + + # Obtener lista paginada de metadatos descargados asociados a una solicitud de descarga. + # api_response = client.download_requests.get_metadata_items("5f71344a-3b5a-4e36-9228-57170d18c64a") + # print(api_response) + + # Descargar paquete (.zip file) de una solicitud de descarga masiva. + # api_response = client.download_requests.download_package("5f71344a-3b5a-4e36-9228-57170d18c64a") + # print(api_response) + + # Descargar SAT request (.xml file) de una solicitud de descarga masiva. (debug/testing) + # api_response = client.download_requests.download_sat_request("5f71344a-3b5a-4e36-9228-57170d18c64a") + # print(api_response) + + # Descargar SAT response (.xml file) de una solicitud de descarga masiva. (debug/testing) + # api_response = client.download_requests.download_sat_response("5f71344a-3b5a-4e36-9228-57170d18c64a") + # print(api_response) + + # Crear solicitud para descargar facturas de los últimos 5 días. + # request = DownloadRequest( + # download_rule_id="5351fb27-c85d-4593-a030-c1799a1cddd5", + # download_request_type_id="Manual", + # start_date=datetime.now() - timedelta(days=5), + # end_date=datetime.now(), + # ) + # api_response = client.download_requests.create(request) + # print(api_response) + + + # Eliminar solicitud de descarga masiva. + # api_response = client.download_requests.delete("5f71344a-3b5a-4e36-9228-57170d18c64a") + # print(api_response) + + # Buscar solicitud de descarga masiva por fecha de creación. + # api_response = client.download_requests.search(datetime.now()) + # print(api_response) + + + if __name__ == "__main__": main() diff --git a/fiscalapi/__init__.py b/fiscalapi/__init__.py index 1873235..402dd6d 100644 --- a/fiscalapi/__init__.py +++ b/fiscalapi/__init__.py @@ -36,6 +36,20 @@ InvoiceStatusRequest, InvoiceStatusResponse, ApiKey, + DownloadRule, + DownloadRequest, + MetadataItem, + XmlGlobalInformation, + XmlIssuer, + XmlRecipient, + XmlRelated, + XmlTax, + XmlItemCustomsInformation, + XmlItemPropertyAccount, + XmlItemTax, + XmlItem, + XmlComplement, + Xml, ) # Re-exportar servicios @@ -45,6 +59,9 @@ from .services.product_service import ProductService from .services.tax_file_servive import TaxFileService from .services.api_key_service import ApiKeyService +from .services.download_catalog_service import DownloadCatalogService +from .services.download_rule_service import DownloadRuleService +from .services.download_request_service import DownloadRequestService # Re-exportar la clase FiscalApiClient # (asumiendo que la definición está en fiscalapi/services/fiscalapi_client.py) @@ -82,7 +99,20 @@ "InvoiceStatusRequest", "InvoiceStatusResponse", "ApiKey", - + "DownloadRule", + "DownloadRequest", + "MetadataItem", + "XmlGlobalInformation", + "XmlIssuer", + "XmlRecipient", + "XmlRelated", + "XmlTax", + "XmlItemCustomsInformation", + "XmlItemPropertyAccount", + "XmlItemTax", + "XmlItem", + "XmlComplement", + "Xml", # Servicios "CatalogService", @@ -90,7 +120,10 @@ "PeopleService", "ProductService", "TaxFileService", - "ApiKeyService" + "ApiKeyService", + "DownloadCatalogService", + "DownloadRuleService", + "DownloadRequestService", # Cliente principal "FiscalApiClient", diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index b0183f9..cc4a472 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -372,4 +372,349 @@ class ApiKey(BaseDto): model_config = ConfigDict( populate_by_name=True, json_encoders={Decimal: str} + ) + + +#Download Models + +class DownloadRule(BaseDto): + """Representa una plantilla para crear solicitudes de descarga de CFDI o metadatos.""" + + person_id: Optional[str] = Field(default=None, alias="personId", description="ID de la persona asociada.") + person: Optional[Person] = Field(default=None, description="Información de la persona.") + tin: Optional[str] = Field(default=None, description="RFC de la regla de descarga.") + description: Optional[str] = Field(default=None, description="Descripción de la regla.") + + # 1 Pendiente, 2 Aprobada, 3 Rechazada, 4 Abandonada + download_rule_status_id: Optional[str] = Field(default=None, alias="downloadRuleStatusId", description="Estado de la regla de descarga (1 Pendiente, 2 Aprobada, 3 Rechazada, 4 Abandonada).") + download_rule_status: Optional[CatalogDto] = Field(default=None, alias="downloadRuleStatus", description="Estado de la regla de descarga.") + + # CFDI, Metadata + sat_query_type_id: Optional[str] = Field(default=None, alias="satQueryTypeId", description="Tipo de consulta SAT (CFDI, Metadata).") + sat_query_type: Optional[CatalogDto] = Field(default=None, alias="satQueryType", description="Tipo de consulta SAT.") + + # Emitidos, Recibidos + download_type_id: Optional[str] = Field(default=None, alias="downloadTypeId", description="Tipo de descarga (Emitidos, Recibidos).") + download_type: Optional[CatalogDto] = Field(default=None, alias="downloadType", description="Tipo de descarga.") + + # Vigente, Cancelado + sat_invoice_status_id: Optional[str] = Field(default=None, alias="satInvoiceStatusId", description="Estado del comprobante SAT (Vigente, Cancelado).") + sat_invoice_status: Optional[CatalogDto] = Field(default=None, alias="satInvoiceStatus", description="Estado del comprobante SAT.") + + model_config = ConfigDict( + populate_by_name=True + ) + + +class DownloadRequest(BaseDto): + """Representa una solicitud de descarga de CFDI o metadatos del SAT.""" + + consecutive: Optional[int] = Field(default=None, description="Consecutivo de la solicitud.") + sat_request_id: Optional[str] = Field(default=None, alias="satRequestId", description="ID de solicitud SAT utilizado para rastrear la solicitud en el sistema SAT.") + download_rule_id: Optional[str] = Field(default=None, alias="downloadRuleId", description="ID de la regla asociada con la solicitud.") + + download_type_id: Optional[str] = Field(default=None, alias="downloadTypeId", description="ID del tipo de descarga.") + download_type: Optional[CatalogDto] = Field(default=None, alias="downloadType", description="Tipo de descarga.") + + download_request_type_id: Optional[str] = Field(default=None, alias="downloadRequestTypeId", description="ID del tipo de solicitud de descarga.") + download_request_type: Optional[CatalogDto] = Field(default=None, alias="downloadRequestType", description="Tipo de solicitud de descarga.") + + recipient_tin: Optional[str] = Field(default=None, alias="recipientTin", description="RFC del receptor. CFDIs específicos o metadatos del RFC receptor dado.") + issuer_tin: Optional[str] = Field(default=None, alias="issuerTin", description="RFC del emisor. CFDIs específicos o metadatos del RFC emisor dado.") + requester_tin: Optional[str] = Field(default=None, alias="requesterTin", description="RFC quien está solicitando la consulta.") + + start_date: Optional[datetime] = Field(default=None, alias="startDate", description="Fecha inicial para la solicitud asociada.") + end_date: Optional[datetime] = Field(default=None, alias="endDate", description="Fecha final para la solicitud asociada.") + + sat_query_type_id: Optional[str] = Field(default=None, alias="satQueryTypeId", description="Tipo de solicitud para la petición. CFDI o Metadata.") + sat_query_type: Optional[CatalogDto] = Field(default=None, alias="satQueryType", description="Tipo de consulta SAT.") + + sat_invoice_type_id: Optional[str] = Field(default=None, alias="satInvoiceTypeId", description="Tipo de factura específico a solicitar. Ingreso, Egreso, Traslado, Nómina, Pago, Todos.") + sat_invoice_type: Optional[CatalogDto] = Field(default=None, alias="satInvoiceType", description="Tipo de comprobante SAT.") + + sat_invoice_status_id: Optional[str] = Field(default=None, alias="satInvoiceStatusId", description="Estado de los CFDIs a solicitar.") + sat_invoice_status: Optional[CatalogDto] = Field(default=None, alias="satInvoiceStatus", description="Estado del comprobante SAT.") + + sat_invoice_complement_id: Optional[str] = Field(default=None, alias="satInvoiceComplementId", description="Complementos de CFDIs para la solicitud.") + sat_invoice_complement: Optional[CatalogDto] = Field(default=None, alias="satInvoiceComplement", description="Complemento del comprobante SAT.") + + sat_request_status_id: Optional[str] = Field(default=None, alias="satRequestStatusId", description="Estado actual de la solicitud. DESCONOCIDO, ACEPTADA, EN PROCESO, TERMINADA, ERROR, RECHAZADA, VENCIDA.") + sat_request_status: Optional[CatalogDto] = Field(default=None, alias="satRequestStatus", description="Estado de la solicitud SAT.") + + download_request_status_id: Optional[str] = Field(default=None, alias="downloadRequestStatusId", description="ID del estado de la solicitud de Fiscalapi.") + download_request_status: Optional[CatalogDto] = Field(default=None, alias="downloadRequestStatus", description="Estado de la solicitud de descarga Fiscalapi.") + + last_attempt_date: Optional[datetime] = Field(default=None, alias="lastAttemptDate", description="Fecha del último intento para la solicitud asociada.") + next_attempt_date: Optional[datetime] = Field(default=None, alias="nextAttemptDate", description="Fecha del siguiente intento para la solicitud asociada.") + + invoice_count: Optional[int] = Field(default=None, alias="invoiceCount", description="Número de CFDIs encontrados para la solicitud cuando la solicitud ha terminado.") + package_ids: Optional[List[str]] = Field(default_factory=list, alias="packageIds", description="Lista de IDs de paquetes disponibles para descarga cuando la solicitud ha terminado.") + is_ready_to_download: Optional[bool] = Field(default=None, alias="isReadyToDownload", description="Indica si la solicitud está lista para descarga, se vuelve verdadero cuando la solicitud ha terminado y los paquetes están disponibles.") + retries_count: Optional[int] = Field(default=None, alias="retriesCount", description="Número total de reintentos realizados para esta solicitud a través de todas las re-presentaciones.") + + model_config = ConfigDict( + populate_by_name=True, + json_encoders={datetime: lambda v: v.isoformat()} + ) + + +class MetadataItem(BaseDto): + """Representa un elemento de metadatos de CFDI.""" + + invoice_uuid: Optional[str] = Field(default=None, alias="invoiceUuid", description="Folio de la factura - UUID.") + issuer_tin: Optional[str] = Field(default=None, alias="issuerTin", description="RFC del emisor del comprobante.") + issuer_name: Optional[str] = Field(default=None, alias="issuerName", description="Nombre o razón social del emisor.") + recipient_tin: Optional[str] = Field(default=None, alias="recipientTin", description="RFC del receptor del comprobante.") + recipient_name: Optional[str] = Field(default=None, alias="recipientName", description="Nombre o razón social del receptor.") + pac_tin: Optional[str] = Field(default=None, alias="pacTin", description="RFC del Proveedor Autorizado de Certificación (PAC).") + invoice_date: Optional[datetime] = Field(default=None, alias="invoiceDate", description="Fecha y hora de emisión del comprobante.") + sat_certification_date: Optional[datetime] = Field(default=None, alias="satCertificationDate", description="Fecha y hora de certificación por el SAT.") + amount: Optional[Decimal] = Field(default=None, description="Monto total del comprobante.") + invoice_type: Optional[str] = Field(default=None, alias="invoiceType", description="Tipo de comprobante (I = Ingreso, E = Egreso, T = Traslado, N = Nómina, P = Pago).") + status: Optional[int] = Field(default=None, description="Estatus del comprobante (1 = Vigente, 0 = Cancelado).") + cancellation_date: Optional[datetime] = Field(default=None, alias="cancellationDate", description="Fecha de cancelación del comprobante (si aplica).") + download_package_id: Optional[str] = Field(default=None, alias="downloadPackageId", description="ID del paquete de descarga.") + download_request_id: Optional[str] = Field(default=None, alias="downloadRequestId", description="ID de la solicitud de descarga.") + + model_config = ConfigDict( + populate_by_name=True, + json_encoders={ + datetime: lambda v: v.isoformat(), + Decimal: str + } + ) + + +class XmlGlobalInformation(BaseDto): + """Información global del CFDI (para CFDI globales).""" + + periodicity: Optional[str] = Field(default=None, description="Periodicidad del CFDI global.") + month: Optional[str] = Field(default=None, description="Mes del CFDI global.") + year: Optional[int] = Field(default=None, description="Año del CFDI global.") + + model_config = ConfigDict(populate_by_name=True) + + +class XmlIssuer(BaseDto): + """Información del emisor del CFDI.""" + + tin: Optional[str] = Field(default=None, description="RFC del emisor.") + legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del emisor.") + tax_regime: Optional[str] = Field(default=None, alias="taxRegime", description="Régimen fiscal del emisor.") + + model_config = ConfigDict(populate_by_name=True) + + +class XmlRecipient(BaseDto): + """Información del receptor del CFDI.""" + + tin: Optional[str] = Field(default=None, description="RFC del receptor.") + legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del receptor.") + zip_code: Optional[str] = Field(default=None, alias="zipCode", description="Código postal del receptor.") + tax_regime: Optional[str] = Field(default=None, alias="taxRegime", description="Régimen fiscal del receptor.") + cfdi_use: Optional[str] = Field(default=None, alias="cfdiUse", description="Uso del CFDI.") + foreign_tax_id: Optional[str] = Field(default=None, alias="foreignTaxId", description="ID fiscal extranjero.") + fiscal_residence: Optional[str] = Field(default=None, alias="fiscalResidence", description="Residencia fiscal.") + + model_config = ConfigDict(populate_by_name=True) + + +class XmlRelated(BaseDto): + """Información sobre facturas relacionadas del CFDI (CFDI relacionados).""" + + xml_id: Optional[str] = Field(default=None, alias="xmlId", description="ID del XML.") + relationship_type: Optional[str] = Field(default=None, alias="relationshipType", description="Tipo de relación.") + cfdi_uuid: Optional[str] = Field(default=None, alias="cfdiUuid", description="UUID del CFDI relacionado.") + + model_config = ConfigDict(populate_by_name=True) + + +class XmlTax(BaseDto): + """Información de los impuestos del CFDI.""" + + base: Optional[Decimal] = Field(default=None, description="Base del impuesto.") + tax: Optional[str] = Field(default=None, description="Impuesto.") + tax_type: Optional[str] = Field(default=None, alias="taxType", description="Tipo de impuesto.") + rate: Optional[Decimal] = Field(default=None, description="Tasa del impuesto.") + amount: Optional[Decimal] = Field(default=None, description="Monto del impuesto.") + tax_flag: Optional[str] = Field(default=None, alias="taxFlag", description="Bandera del impuesto.") + xml_id: Optional[str] = Field(default=None, alias="xmlId", description="ID del XML.") + + model_config = ConfigDict( + populate_by_name=True, + json_encoders={Decimal: str} + ) + + +class XmlItemCustomsInformation(BaseDto): + """Información aduanera de los conceptos del CFDI.""" + + xml_item_id: Optional[str] = Field(default=None, alias="xmlItemId", description="ID del concepto XML.") + customs_document_number: Optional[str] = Field(default=None, alias="customsDocumentNumber", description="Número de documento aduanero.") + + model_config = ConfigDict(populate_by_name=True) + + +class XmlItemPropertyAccount(BaseDto): + """Cuenta predial de los conceptos del CFDI.""" + + xml_item_id: Optional[str] = Field(default=None, alias="xmlItemId", description="ID del concepto XML.") + property_account_number: Optional[str] = Field(default=None, alias="propertyAccountNumber", description="Número de cuenta predial.") + + model_config = ConfigDict(populate_by_name=True) + + +class XmlItemTax(BaseDto): + """Impuestos de los conceptos del CFDI.""" + + base: Optional[Decimal] = Field(default=None, description="Base del impuesto.") + tax: Optional[str] = Field(default=None, description="Impuesto.") + tax_type: Optional[str] = Field(default=None, alias="taxType", description="Tipo de impuesto.") + rate: Optional[Decimal] = Field(default=None, description="Tasa del impuesto.") + amount: Optional[Decimal] = Field(default=None, description="Monto del impuesto.") + tax_flag: Optional[str] = Field(default=None, alias="taxFlag", description="Bandera del impuesto.") + xml_item_id: Optional[str] = Field(default=None, alias="xmlItemId", description="ID del concepto XML.") + + model_config = ConfigDict( + populate_by_name=True, + json_encoders={Decimal: str} + ) + + +class XmlItem(BaseDto): + """Información de los conceptos del CFDI.""" + + xml_id: Optional[str] = Field(default=None, alias="xmlId", description="ID del XML.") + item_code: Optional[str] = Field(default=None, alias="itemCode", description="Código del producto/servicio.") + sku: Optional[str] = Field(default=None, description="SKU del producto.") + quantity: Optional[Decimal] = Field(default=None, description="Cantidad.") + unit_measurement: Optional[str] = Field(default=None, alias="unitMeasurement", description="Unidad de medida.") + description: Optional[str] = Field(default=None, description="Descripción del concepto.") + unit_price: Optional[Decimal] = Field(default=None, alias="unitPrice", description="Precio unitario.") + amount: Optional[Decimal] = Field(default=None, description="Importe.") + discount: Optional[Decimal] = Field(default=None, description="Descuento.") + tax_object: Optional[str] = Field(default=None, alias="taxObject", description="Objeto de impuesto.") + third_party_account: Optional[str] = Field(default=None, alias="thirdPartyAccount", description="Cuenta de terceros.") + + xml_item_customs_information: Optional[List[XmlItemCustomsInformation]] = Field( + default_factory=list, + alias="xmlItemCustomsInformation", + description="Información aduanera del concepto." + ) + xml_item_property_accounts: Optional[List[XmlItemPropertyAccount]] = Field( + default_factory=list, + alias="xmlItemPropertyAccounts", + description="Cuentas prediales del concepto." + ) + taxes: Optional[List[XmlItemTax]] = Field(default_factory=list, description="Impuestos del concepto.") + + model_config = ConfigDict( + populate_by_name=True, + json_encoders={Decimal: str} + ) + + +class XmlComplement(BaseDto): + """Información de los complementos del CFDI.""" + + complement_name: Optional[str] = Field(default=None, alias="complementName", description="Nombre del complemento.") + base64_complement_value: Optional[str] = Field(default=None, alias="base64ComplementValue", description="Valor del complemento en base64.") + xml_id: Optional[str] = Field(default=None, alias="xmlId", description="ID del XML.") + + model_config = ConfigDict(populate_by_name=True) + + +class Xml(BaseDto): + """Representa el XML de un CFDI (Comprobante Fiscal Digital por Internet) descargado desde el SAT.""" + + # Regla de descarga + download_request_id: Optional[str] = Field(default=None, alias="downloadRequestId", description="ID de la solicitud de descarga.") + + # Version del CFDI + version: Optional[str] = Field(default=None, description="Versión del CFDI.") + + # Serie + series: Optional[str] = Field(default=None, description="Serie del CFDI.") + + # Folio + number: Optional[str] = Field(default=None, description="Folio del CFDI.") + + # Fecha de emisión del CFDI + date: Optional[datetime] = Field(default=None, description="Fecha de emisión del CFDI.") + + # Codigo de la forma de pago + payment_form: Optional[str] = Field(default=None, alias="paymentForm", description="Código de la forma de pago.") + + # Codigo del método de pago + payment_method: Optional[str] = Field(default=None, alias="paymentMethod", description="Código del método de pago.") + + # Numero de certificado del emisor + certificate_number: Optional[str] = Field(default=None, alias="certificateNumber", description="Número de certificado del emisor.") + + # Condiciones de pago + payment_conditions: Optional[str] = Field(default=None, alias="paymentConditions", description="Condiciones de pago.") + + # Subtotal del CFDI + sub_total: Optional[Decimal] = Field(default=None, alias="subTotal", description="Subtotal del CFDI.") + + # Descuento aplicado al CFDI + discount: Optional[Decimal] = Field(default=None, description="Descuento aplicado al CFDI.") + + # Codigo de la moneda del CFDI + currency: Optional[str] = Field(default=None, description="Código de la moneda del CFDI.") + + # Tipo de cambio del CFDI (si aplica) + exchange_rate: Optional[Decimal] = Field(default=None, alias="exchangeRate", description="Tipo de cambio del CFDI (si aplica).") + + # Total del CFDI + total: Optional[Decimal] = Field(default=None, description="Total del CFDI.") + + # Tipo de comprobante (I = Ingreso, E = Egreso, T = Traslado, N = Nómina, P = Pago) + invoice_type: Optional[str] = Field(default=None, alias="invoiceType", description="Tipo de comprobante (I = Ingreso, E = Egreso, T = Traslado, N = Nómina, P = Pago).") + + # Codigo de exportación (si aplica) + export: Optional[str] = Field(default=None, description="Código de exportación (si aplica).") + + # Lugar de expedición del CFDI + expedition_zip_code: Optional[str] = Field(default=None, alias="expeditionZipCode", description="Lugar de expedición del CFDI.") + + # Confirmacion si aplica + confirmation: Optional[str] = Field(default=None, description="Confirmación si aplica.") + + # Total impuestos retenidos + total_withheld_taxes: Optional[Decimal] = Field(default=None, alias="totalWithheldTaxes", description="Total impuestos retenidos.") + + # Total impuestos trasladados + total_transferred_taxes: Optional[Decimal] = Field(default=None, alias="totalTransferredTaxes", description="Total impuestos trasladados.") + + # Información global del CFDI (para CFDI globales) + xml_global_information: Optional[XmlGlobalInformation] = Field(default=None, alias="xmlGlobalInformation", description="Información global del CFDI (para CFDI globales).") + + # Información de impuestos del CFDI + taxes: Optional[List[XmlTax]] = Field(default_factory=list, description="Información de impuestos del CFDI.") + + # Información sobre facturas relacionada del CFDI (CFDI relacionados) + xml_related: Optional[List[XmlRelated]] = Field(default_factory=list, alias="xmlRelated", description="Información sobre facturas relacionadas del CFDI (CFDI relacionados).") + + # Información del emisor del CFDI + xml_issuer: Optional[XmlIssuer] = Field(default=None, alias="xmlIssuer", description="Información del emisor del CFDI.") + + # Información del receptor del CFDI + xml_recipient: Optional[XmlRecipient] = Field(default=None, alias="xmlRecipient", description="Información del receptor del CFDI.") + + # Información de los conceptos del CFDI + xml_items: Optional[List[XmlItem]] = Field(default_factory=list, alias="xmlItems", description="Información de los conceptos del CFDI.") + + # Información de los complementos del CFDI + xml_complements: Optional[List[XmlComplement]] = Field(default_factory=list, alias="xmlComplements", description="Información de los complementos del CFDI.") + + # Xml crudo en base64 + base64_content: Optional[str] = Field(default=None, alias="base64Content", description="XML crudo en base64.") + + model_config = ConfigDict( + populate_by_name=True, + json_encoders={ + datetime: lambda v: v.isoformat(), + Decimal: str + } ) \ No newline at end of file diff --git a/fiscalapi/services/download_catalog_service.py b/fiscalapi/services/download_catalog_service.py new file mode 100644 index 0000000..9b94d6a --- /dev/null +++ b/fiscalapi/services/download_catalog_service.py @@ -0,0 +1,32 @@ +from typing import List +from fiscalapi.models.common_models import ApiResponse, CatalogDto, PagedList +from fiscalapi.services.base_service import BaseService + + + +class DownloadCatalogService(BaseService): + """Servicio para gestionar catálogos de descarga masiva.""" + + def get_list(self) -> ApiResponse[List[str]]: + """ + Obtiene una lista de catálogos disponibles. + + Returns: + ApiResponse[List[str]]: Lista de catálogos disponibles + """ + endpoint = "download-catalogs" + return self.send_request("GET", endpoint, List[str]) + + def list_catalog (self, catalog_name: str) -> ApiResponse[List[CatalogDto]]: + """ + Obtiene una lista de registros de un catálogo. + + Args: + catalog_name (str): Nombre del catálogo + Returns: + ApiResponse[List[CatalogDto]]: Lista de registros de un catálogo + """ + endpoint = f"download-catalogs/{catalog_name}" + return self.send_request("GET", endpoint, List[CatalogDto]) + + \ No newline at end of file diff --git a/fiscalapi/services/download_request_service.py b/fiscalapi/services/download_request_service.py new file mode 100644 index 0000000..7fee886 --- /dev/null +++ b/fiscalapi/services/download_request_service.py @@ -0,0 +1,213 @@ +from datetime import datetime +from typing import List +from fiscalapi.models.common_models import ApiResponse, PagedList +from fiscalapi.models.fiscalapi_models import DownloadRequest, MetadataItem, Xml, FileResponse +from fiscalapi.services.base_service import BaseService + + +class DownloadRequestService(BaseService): + """Servicio para gestionar solicitudes de descarga de CFDI.""" + + def get_list(self, page_number: int, page_size: int) -> ApiResponse[PagedList[DownloadRequest]]: + """ + Obtiene una lista paginada de solicitudes de descarga. + + Args: + page_number (int): Número de página + page_size (int): Tamaño de página + + Returns: + ApiResponse[PagedList[DownloadRequest]]: Lista paginada de solicitudes de descarga + """ + endpoint = f"download-requests?pageNumber={page_number}&pageSize={page_size}" + return self.send_request("GET", endpoint, PagedList[DownloadRequest]) + + def get_by_id(self, request_id: str, details: bool = False) -> ApiResponse[DownloadRequest]: + """ + Obtiene una solicitud de descarga por su ID. + + Args: + request_id (str): ID de la solicitud de descarga + details (bool): Si incluir detalles adicionales + + Returns: + ApiResponse[DownloadRequest]: Solicitud de descarga encontrada + """ + endpoint = f"download-requests/{request_id}" + return self.send_request("GET", endpoint, DownloadRequest, details=details) + + def create(self, download_request: DownloadRequest) -> ApiResponse[DownloadRequest]: + """ + Crea una nueva solicitud de descarga. + + Args: + download_request (DownloadRequest): Solicitud de descarga a crear + + Returns: + ApiResponse[DownloadRequest]: Solicitud de descarga creada + + Raises: + ValueError: Si download_request es None + """ + if download_request is None: + raise ValueError("download_request no puede ser nulo") + + endpoint = "download-requests" + return self.send_request("POST", endpoint, DownloadRequest, payload=download_request) + + def update(self, request_id: str, download_request: DownloadRequest) -> ApiResponse[DownloadRequest]: + """ + Actualiza una solicitud de descarga existente. + + Args: + request_id (str): ID de la solicitud de descarga + download_request (DownloadRequest): Datos actualizados de la solicitud + + Returns: + ApiResponse[DownloadRequest]: Solicitud de descarga actualizada + + Raises: + ValueError: Si request_id o download_request son None + """ + if not request_id: + raise ValueError("request_id no puede ser nulo o vacío") + if download_request is None: + raise ValueError("download_request no puede ser nulo") + + endpoint = f"download-requests/{request_id}" + return self.send_request("PUT", endpoint, DownloadRequest, payload=download_request) + + def delete(self, request_id: str) -> ApiResponse[bool]: + """ + Elimina una solicitud de descarga. + + Args: + request_id (str): ID de la solicitud de descarga a eliminar + + Returns: + ApiResponse[bool]: True si se eliminó correctamente + + Raises: + ValueError: Si request_id es None o vacío + """ + if not request_id: + raise ValueError("request_id no puede ser nulo o vacío") + + endpoint = f"download-requests/{request_id}" + return self.send_request("DELETE", endpoint, bool) + + def get_xmls(self, request_id: str) -> ApiResponse[PagedList[Xml]]: + """ + Obtiene los XMLs asociados a una solicitud de descarga. + + Args: + request_id (str): ID de la solicitud de descarga + + Returns: + ApiResponse[PagedList[Xml]]: Lista paginada de XMLs + + Raises: + ValueError: Si request_id es None o vacío + """ + if not request_id: + raise ValueError("request_id no puede ser nulo o vacío") + + endpoint = f"download-requests/{request_id}/xmls" + return self.send_request("GET", endpoint, PagedList[Xml]) + + def get_metadata_items(self, request_id: str) -> ApiResponse[PagedList[MetadataItem]]: + """ + Obtiene los elementos de metadatos asociados a una solicitud de descarga. + + Args: + request_id (str): ID de la solicitud de descarga + + Returns: + ApiResponse[PagedList[MetadataItem]]: Lista paginada de elementos de metadatos + + Raises: + ValueError: Si request_id es None o vacío + """ + if not request_id: + raise ValueError("request_id no puede ser nulo o vacío") + + endpoint = f"download-requests/{request_id}/meta-items" + return self.send_request("GET", endpoint, PagedList[MetadataItem]) + + def download_package(self, request_id: str) -> ApiResponse[List[FileResponse]]: + """ + Descarga el paquete de archivos asociado a una solicitud de descarga. + + Args: + request_id (str): ID de la solicitud de descarga + + Returns: + ApiResponse[List[FileResponse]]: Lista de archivos del paquete + + Raises: + ValueError: Si request_id es None o vacío + """ + if not request_id: + raise ValueError("request_id no puede ser nulo o vacío") + + endpoint = f"download-requests/{request_id}/package" + return self.send_request("GET", endpoint, List[FileResponse]) + + def download_sat_request(self, request_id: str) -> ApiResponse[FileResponse]: + """ + Descarga la solicitud cruda enviada al SAT. + + Args: + request_id (str): ID de la solicitud de descarga + + Returns: + ApiResponse[FileResponse]: Archivo de la solicitud SAT + + Raises: + ValueError: Si request_id es None o vacío + """ + if not request_id: + raise ValueError("request_id no puede ser nulo o vacío") + + endpoint = f"download-requests/{request_id}/raw-request" + return self.send_request("GET", endpoint, FileResponse) + + def download_sat_response(self, request_id: str) -> ApiResponse[FileResponse]: + """ + Descarga la respuesta cruda recibida del SAT. + + Args: + request_id (str): ID de la solicitud de descarga + + Returns: + ApiResponse[FileResponse]: Archivo de la respuesta SAT + + Raises: + ValueError: Si request_id es None o vacío + """ + if not request_id: + raise ValueError("request_id no puede ser nulo o vacío") + + endpoint = f"download-requests/{request_id}/raw-response" + return self.send_request("GET", endpoint, FileResponse) + + def search(self, created_at: datetime) -> ApiResponse[List[DownloadRequest]]: + """ + Busca solicitudes de descarga por fecha de creación. + + Args: + created_at (datetime): Fecha de creación para buscar + + Returns: + ApiResponse[List[DownloadRequest]]: Lista de solicitudes encontradas + + Raises: + ValueError: Si created_at es None + """ + if created_at is None: + raise ValueError("created_at no puede ser nulo") + + created_at_str = created_at.strftime("%Y-%m-%d") + endpoint = f"download-requests/search?createdAt={created_at_str}" + print(endpoint) + return self.send_request("GET", endpoint, List[DownloadRequest]) \ No newline at end of file diff --git a/fiscalapi/services/download_rule_service.py b/fiscalapi/services/download_rule_service.py new file mode 100644 index 0000000..be6e234 --- /dev/null +++ b/fiscalapi/services/download_rule_service.py @@ -0,0 +1,37 @@ +from fiscalapi.models.common_models import ApiResponse, PagedList +from fiscalapi.models.fiscalapi_models import DownloadRule, DownloadRequest +from fiscalapi.services.base_service import BaseService + + +class DownloadRuleService(BaseService): + """Servicio para gestionar reglas de descarga de CFDI.""" + + # get paged list of download rules + def get_list(self, page_number: int, page_size: int) -> ApiResponse[PagedList[DownloadRule]]: + endpoint = f"download-rules?pageNumber={page_number}&pageSize={page_size}" + return self.send_request("GET", endpoint, PagedList[DownloadRule]) + + # get download rule by id + def get_by_id(self, download_rule_id: str) -> ApiResponse[DownloadRule]: + endpoint = f"download-rules/{download_rule_id}" + return self.send_request("GET", endpoint, DownloadRule) + + # create download rule + def create(self, download_rule: DownloadRule) -> ApiResponse[DownloadRule]: + endpoint = "download-rules" + return self.send_request("POST", endpoint, DownloadRule, payload=download_rule) + + # create test download rule + def create_test_rule(self) -> ApiResponse[DownloadRule]: + endpoint = "download-rules/test" + return self.send_request("POST", endpoint, DownloadRule) + + # update download rule + def update(self, download_rule: DownloadRule) -> ApiResponse[DownloadRule]: + endpoint = f"download-rules/{download_rule.id}" + return self.send_request("PUT", endpoint, DownloadRule, payload=download_rule) + + # delete download rule + def delete(self, download_rule_id: str) -> ApiResponse[bool]: + endpoint = f"download-rules/{download_rule_id}" + return self.send_request("DELETE", endpoint, bool) \ No newline at end of file diff --git a/fiscalapi/services/fiscalapi_client.py b/fiscalapi/services/fiscalapi_client.py index 42ccd6e..24f7ea7 100644 --- a/fiscalapi/services/fiscalapi_client.py +++ b/fiscalapi/services/fiscalapi_client.py @@ -5,6 +5,9 @@ from fiscalapi.services.product_service import ProductService from fiscalapi.services.tax_file_servive import TaxFileService from fiscalapi.services.api_key_service import ApiKeyService +from fiscalapi.services.download_catalog_service import DownloadCatalogService +from fiscalapi.services.download_rule_service import DownloadRuleService +from fiscalapi.services.download_request_service import DownloadRequestService @@ -17,4 +20,7 @@ def __init__(self, settings: FiscalApiSettings): self.catalogs = CatalogService(settings) self.invoices = InvoiceService(settings) self.api_keys = ApiKeyService(settings) + self.download_catalogs = DownloadCatalogService(settings) + self.download_rules = DownloadRuleService(settings) + self.download_requests = DownloadRequestService(settings) self.settings = settings \ No newline at end of file diff --git a/setup.py b/setup.py index 043faa3..a02b494 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,10 @@ import os from setuptools import setup, find_packages -VERSION = "4.0.151" -# Descripción breve basada en el .csproj +VERSION = "4.0.270" + DESCRIPTION = "Genera facturas CFDI válidas ante el SAT consumiendo el API de https://www.fiscalapi.com" -# Carga un README.md (si existe) como descripción larga current_dir = os.path.abspath(os.path.dirname(__file__)) long_description_file = os.path.join(current_dir, "README.md") if os.path.exists(long_description_file): @@ -16,16 +15,16 @@ LONG_DESCRIPTION = DESCRIPTION # fallback si no se encuentra README.md setup( - name="fiscalapi", # Normalmente en minúsculas en PyPI + name="fiscalapi", version=VERSION, author="Fiscalapi", - author_email="contacto@fiscalapi.com", # Ajusta con el correo que corresponda + author_email="contacto@fiscalapi.com", url="https://www.fiscalapi.com", description=DESCRIPTION, long_description=LONG_DESCRIPTION, long_description_content_type="text/markdown", - # Licencia Pública de Mozilla (por ejemplo, MPL 2.0) + # Licencia Pública de Mozilla license="MPL-2.0", packages=find_packages( @@ -33,7 +32,7 @@ ), keywords=["factura", "cfdi", "facturacion", "mexico", "sat", "fiscalapi"], - python_requires=">=3.7", # Ajusta según la compatibilidad mínima de Python + python_requires=">=3.7", install_requires=[ "pydantic>=2.0.0",