From 7a2d791556b42f2cbb0d1c8fa36bdda3ecfb2eef Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 18:32:10 -0600 Subject: [PATCH 01/28] python 3.9 mandatory added --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a02b494..53ad351 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ ), keywords=["factura", "cfdi", "facturacion", "mexico", "sat", "fiscalapi"], - python_requires=">=3.7", + python_requires=">=3.9", install_requires=[ "pydantic>=2.0.0", @@ -46,7 +46,10 @@ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Office/Business :: Financial", ], ) From fdcbfad0ad142f5d06ae3b2dea6f9d4106ca2a4e Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 19:39:39 -0600 Subject: [PATCH 02/28] Fix type errors and Pydantic v2 compatibility issues - Remove TypeVar bound constraint to allow ApiResponse[bool], ApiResponse[list[str]] - Remove invalid http_status_code parameter from ApiResponse instantiations - Convert deprecated class Config to model_config = ConfigDict() - Standardize List/Dict type hints to Python 3.9+ built-in list/dict - Fix mutable default arguments using default_factory=list - Add missing model_config to PagedList and ValidationFailure - Add return type annotation to InvoiceService.send() method --- fiscalapi/models/common_models.py | 19 ++--- fiscalapi/models/fiscalapi_models.py | 71 +++++++++---------- fiscalapi/services/base_service.py | 6 +- .../services/download_catalog_service.py | 23 +++--- fiscalapi/services/invoice_service.py | 6 +- 5 files changed, 58 insertions(+), 67 deletions(-) diff --git a/fiscalapi/models/common_models.py b/fiscalapi/models/common_models.py index ae12225..0a0cd2f 100644 --- a/fiscalapi/models/common_models.py +++ b/fiscalapi/models/common_models.py @@ -1,9 +1,9 @@ from datetime import datetime -from typing import Any, Generic, List, Optional, TypeVar +from typing import Any, Generic, Optional, TypeVar from pydantic import BaseModel, ConfigDict, Field from pydantic.alias_generators import to_snake -T = TypeVar('T', bound=BaseModel) +T = TypeVar('T') class ApiResponse(BaseModel, Generic[T]): succeeded: bool = Field(alias="succeeded") @@ -19,13 +19,15 @@ class ApiResponse(BaseModel, Generic[T]): class PagedList(BaseModel, Generic[T]): """Modelo para la estructura de la lista paginada.""" - items: List[T] = Field(default=[], alias="items", description="Lista de elementos paginados") + items: list[T] = Field(default_factory=list, alias="items", description="Lista de elementos paginados") page_number: int = Field(alias="pageNumber", description="Número de página actual") total_pages: int = Field(alias="totalPages", description="Cantidad total de páginas") total_count: int = Field(alias="totalCount", description="Cantidad total de elementos") has_previous_page: bool = Field(alias="hasPreviousPage", description="Indica si hay una página anterior") has_next_page: bool = Field(alias="hasNextPage", description="Indica si hay una página siguiente") + model_config = ConfigDict(populate_by_name=True) + class ValidationFailure(BaseModel): """Modelo para errores de validación.""" @@ -37,6 +39,8 @@ class ValidationFailure(BaseModel): errorCode: Optional[str] = None formattedMessagePlaceholderValues: Optional[dict] = None + model_config = ConfigDict(populate_by_name=True) + class BaseDto(BaseModel): """Modelo base para DTOs.""" @@ -50,8 +54,6 @@ class CatalogDto(BaseDto): """Modelo para catálogos.""" description: str = Field(alias="description") - model_config = ConfigDict(populate_by_name=True) - class FiscalApiSettings(BaseModel): """ @@ -64,6 +66,7 @@ class FiscalApiSettings(BaseModel): time_zone: str = Field("America/Mexico_City", description="Zona horaria ") debug: bool = Field(False, description="Indica si se debe imprimir el payload request y response.") - class Config: - title = "FiscalApi Settings" - description = "Configuración para Fiscalapi" \ No newline at end of file + model_config = ConfigDict( + title="FiscalApi Settings", + json_schema_extra={"description": "Configuración para Fiscalapi"} + ) \ No newline at end of file diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index cc4a472..857e9ae 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -1,7 +1,7 @@ from decimal import Decimal from pydantic import ConfigDict, EmailStr, Field from fiscalapi.models.common_models import BaseDto, CatalogDto -from typing import Dict, List, Literal, Optional +from typing import Literal, Optional from datetime import datetime # products models @@ -111,7 +111,7 @@ class InvoiceIssuer(BaseDto): tin: Optional[str] = Field(default=None, alias="tin", description="RFC del emisor (Tax Identification Number).") legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del emisor sin regimen de capital.") tax_regime_code: Optional[str] = Field(default=None, alias="taxRegimeCode", description="Código del régimen fiscal del emisor.") - tax_credentials: Optional[List[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor (archivos .cer y .key).") + tax_credentials: Optional[list[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor (archivos .cer y .key).") class InvoiceRecipient(BaseDto): """Modelo para el receptor de la factura.""" @@ -147,7 +147,7 @@ class InvoiceItem(BaseDto): unit_price: Optional[Decimal] = Field(default=None, alias="unitPrice", description="Precio unitario del producto o servicio.") tax_object_code: Optional[str] = Field(default=None, alias="taxObjectCode", description="Código SAT de obligaciones de impuesto.") item_sku: Optional[str] = Field(default=None, alias="itemSku", description="SKU o clave del sistema externo.") - item_taxes: Optional[List[ItemTax]] = Field(default=None, alias="itemTaxes", description="Impuestos aplicables al producto o servicio.") + item_taxes: Optional[list[ItemTax]] = Field(default=None, alias="itemTaxes", description="Impuestos aplicables al producto o servicio.") model_config = ConfigDict( populate_by_name=True, @@ -192,7 +192,7 @@ class PaidInvoice(BaseDto): currency_code: str = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en la factura pagada.") tax_object_code: str = Field(..., alias="taxObjectCode", description="Código de obligaciones de impuesto.") equivalence: Optional[Decimal] = Field(default=1, description="Equivalencia de la moneda. Este campo es obligatorio cuando la moneda del documento relacionado (PaidInvoice.CurrencyCode) difiere de la moneda en que se realiza el pago ( InvoicePayment.CurrencyCode).") - paid_invoice_taxes: List[PaidInvoiceTax] = Field(..., alias="paidInvoiceTaxes", description="Impuestos aplicables a la factura pagada.") + paid_invoice_taxes: list[PaidInvoiceTax] = Field(..., alias="paidInvoiceTaxes", description="Impuestos aplicables a la factura pagada.") model_config = ConfigDict( populate_by_name=True, @@ -213,7 +213,7 @@ class InvoicePayment(BaseDto): source_bank_account: str = Field(..., alias="sourceBankAccount", description="Cuenta bancaria origen. (Cuenta bancaria del banco emisor del pago)") target_bank_tin: str = Field(..., alias="targetBankTin", description="RFC del banco destino. (Rfc del banco receptor del pago)") target_bank_account: str = Field(..., alias="targetBankAccount", description="Cuenta bancaria destino (Cuenta bancaria del banco receptor del pago)") - paid_invoices: List[PaidInvoice] = Field(..., alias="paidInvoices", description="Facturas pagadas con el pago recibido.") + paid_invoices: list[PaidInvoice] = Field(..., alias="paidInvoices", description="Facturas pagadas con el pago recibido.") model_config = ConfigDict( populate_by_name=True, @@ -263,11 +263,11 @@ class Invoice(BaseDto): exchange_rate: Optional[Decimal] = Field(default=1, alias="exchangeRate", description="Tipo de cambio FIX.") issuer: Optional[InvoiceIssuer] = Field(..., description="El emisor de la factura.") recipient: Optional[InvoiceRecipient] = Field(..., description="El receptor de la factura.") - items: Optional[List[InvoiceItem]] = Field(default=[], description="Conceptos de la factura (productos o servicios).") + items: Optional[list[InvoiceItem]] = Field(default_factory=list, description="Conceptos de la factura (productos o servicios).") global_information: Optional[GlobalInformation] = Field(default=None, alias="globalInformation", description="Información global de la factura.") - related_invoices: Optional[List[RelatedInvoice]] = Field(default=None, alias="relatedInvoices", description="Facturas relacionadas.") - payments: Optional[List[InvoicePayment]] = Field(default=None, description="Pago o pagos recibidos para liquidar la factura cuando la factura es un complemento de pago.") - responses: Optional[List[InvoiceResponse]] = Field(default=None, description="Respuestas del SAT. Contiene la información de timbrado de la factura.") + related_invoices: Optional[list[RelatedInvoice]] = Field(default=None, alias="relatedInvoices", description="Facturas relacionadas.") + payments: Optional[list[InvoicePayment]] = Field(default=None, description="Pago o pagos recibidos para liquidar la factura cuando la factura es un complemento de pago.") + responses: Optional[list[InvoiceResponse]] = Field(default=None, description="Respuestas del SAT. Contiene la información de timbrado de la factura.") model_config = ConfigDict( @@ -284,18 +284,16 @@ class CancelInvoiceRequest(BaseDto): tin: Optional[str] = Field(default=None, alias="tin", description="RFC del emisor de la factura. Obligatorio cuando se cancela por valores.") cancellation_reason_code: Literal["01", "02", "03", "04"] = Field(..., alias="cancellationReasonCode", description="Código del motivo de cancelación.") replacement_uuid: Optional[str] = Field(default=None, alias="replacementUuid", description="UUID de la factura de reemplazo. Obligatorio si el motivo de cancelación es '01'.") - tax_credentials: Optional[List[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor. Obligatorio cuando se cancela por valores.") + tax_credentials: Optional[list[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor. Obligatorio cuando se cancela por valores.") + + model_config = ConfigDict(populate_by_name=True) - class Config: - populate_by_name = True - class CancelInvoiceResponse(BaseDto): """Modelo de respuesta para la cancelación de factura.""" base64_cancellation_acknowledgement: str = Field(default=None, alias="base64CancellationAcknowledgement", description="Acuse de cancelación en formato base64. Contiene el XML del acuse de cancelación del SAT codificado en base64.") - invoice_uuids: Optional[Dict[str, str]] = Field(default=None, alias="invoiceUuids", description="Diccionario de UUIDs de facturas con su respectivo código de estatus de cancelación. La llave es el UUID de la factura y el valor es el código de estatus.") + invoice_uuids: Optional[dict[str, str]] = Field(default=None, alias="invoiceUuids", description="Diccionario de UUIDs de facturas con su respectivo código de estatus de cancelación. La llave es el UUID de la factura y el valor es el código de estatus.") - class Config: - populate_by_name = True + model_config = ConfigDict(populate_by_name=True) class CreatePdfRequest(BaseDto): @@ -305,8 +303,7 @@ class CreatePdfRequest(BaseDto): font_color: Optional[str] = Field(default=None, alias="fontColor", description="Color de la fuente del texto sobre la banda en formato hexadecimal. Ejemplo: '#FFFFFF'.") base64_logo: Optional[str] = Field(default=None, alias="base64Logo", description="Logotipo en formato base64 que se mostrará en el PDF.") - class Config: - populate_by_name = True + model_config = ConfigDict(populate_by_name=True) class FileResponse(BaseDto): """Modelo de respuesta para la generación de PDF o recuperación de XML.""" @@ -314,10 +311,9 @@ class FileResponse(BaseDto): file_name: Optional[str] = Field(default=None, alias="fileName", description="Nombre del archivo generado.") file_extension: Optional[str] = Field(default=None, alias="fileExtension", description="Extensión del archivo. Ejemplo: '.pdf'.") - class Config: - populate_by_name = True - - + model_config = ConfigDict(populate_by_name=True) + + class SendInvoiceRequest(BaseDto): """Modelo para el envío de facturas por correo electrónico.""" invoice_id: str = Field(..., alias="invoiceId", description="ID de la factura para la cual se enviará el PDF.") @@ -326,8 +322,7 @@ class SendInvoiceRequest(BaseDto): font_color: Optional[str] = Field(default=None, alias="fontColor", description="Color de la fuente del texto sobre la banda en formato hexadecimal. Ejemplo: '#FFFFFF'.") base64_logo: Optional[str] = Field(default=None, alias="base64Logo", description="Logotipo en formato base64 que se mostrará en el PDF.") - class Config: - populate_by_name = True + model_config = ConfigDict(populate_by_name=True) class InvoiceStatusRequest(BaseDto): @@ -339,10 +334,10 @@ class InvoiceStatusRequest(BaseDto): invoice_uuid: Optional[str] = Field(default=None, alias="invoiceUuid", description="Folio fiscal factura a consultar") last8_digits_issuer_signature: Optional[str] = Field(default=None, alias="last8DigitsIssuerSignature", description="Últimos ocho caracteres del sello digital del emisor") - model_config = { - "populate_by_name": True, - "json_encoders": {Decimal: str} - } + model_config = ConfigDict( + populate_by_name=True, + json_encoders={Decimal: str} + ) class InvoiceStatusResponse(BaseDto): """Modelo de respuesta de consulta de estado de facturas.""" @@ -352,9 +347,7 @@ class InvoiceStatusResponse(BaseDto): cancellation_status: str = Field(..., alias="cancellationStatus", description="Detalle del estatus de cancelación") efos_validation: str = Field(..., alias="efosValidation", description="Codigo que indica si el RFC Emisor se encuentra dentro de la lista negra de EFOS") - model_config = { - "populate_by_name": True - } + model_config = ConfigDict(populate_by_name=True) @@ -448,7 +441,7 @@ class DownloadRequest(BaseDto): 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.") + 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.") @@ -596,17 +589,17 @@ class XmlItem(BaseDto): 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( + 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( + 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.") + taxes: Optional[list[XmlItemTax]] = Field(default_factory=list, description="Impuestos del concepto.") model_config = ConfigDict( populate_by_name=True, @@ -691,10 +684,10 @@ class Xml(BaseDto): 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.") + 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).") + 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.") @@ -703,10 +696,10 @@ class Xml(BaseDto): 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.") + 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_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.") diff --git a/fiscalapi/services/base_service.py b/fiscalapi/services/base_service.py index 4172b89..7b9513b 100644 --- a/fiscalapi/services/base_service.py +++ b/fiscalapi/services/base_service.py @@ -17,7 +17,7 @@ def __init__(self, settings: FiscalApiSettings): self.api_key = settings.api_key self.debug = settings.debug - def _get_headers(self) -> dict: + def _get_headers(self) -> dict[str, str]: return { "Content-Type": "application/json", "X-TENANT-KEY": self.settings.tenant, @@ -130,7 +130,6 @@ def _process_response(self, response: requests.Response, response_model: Type[T] except ValueError: return ApiResponse[T]( succeeded=False, - http_status_code=status_code, message="Error processing server response", details=raw_content, data=None @@ -166,7 +165,6 @@ def _process_response(self, response: requests.Response, response_model: Type[T] except Exception: return ApiResponse[T]( succeeded=False, - http_status_code=status_code, message="Error processing server error response", details=raw_content, data=None @@ -179,7 +177,6 @@ def _process_response(self, response: requests.Response, response_model: Type[T] details_str = "; ".join(f"{f.propertyName}: {f.errorMessage}" for f in failures) return ApiResponse[T]( succeeded=False, - http_status_code=400, message=generic_error.message, details=details_str, data=None @@ -189,7 +186,6 @@ def _process_response(self, response: requests.Response, response_model: Type[T] return ApiResponse[T]( succeeded=False, - http_status_code=status_code, message=generic_error.message or f"HTTP Error {status_code}", details=generic_error.details or raw_content, data=None diff --git a/fiscalapi/services/download_catalog_service.py b/fiscalapi/services/download_catalog_service.py index 9b94d6a..43137a3 100644 --- a/fiscalapi/services/download_catalog_service.py +++ b/fiscalapi/services/download_catalog_service.py @@ -1,32 +1,31 @@ -from typing import List -from fiscalapi.models.common_models import ApiResponse, CatalogDto, PagedList +from fiscalapi.models.common_models import ApiResponse, CatalogDto 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]]: + + def get_list(self) -> ApiResponse[list[str]]: """ Obtiene una lista de catálogos disponibles. - + Returns: - ApiResponse[List[str]]: Lista de catálogos disponibles + 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]]: + 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 + ApiResponse[list[CatalogDto]]: Lista de registros de un catálogo """ endpoint = f"download-catalogs/{catalog_name}" - return self.send_request("GET", endpoint, List[CatalogDto]) + return self.send_request("GET", endpoint, list[CatalogDto]) \ No newline at end of file diff --git a/fiscalapi/services/invoice_service.py b/fiscalapi/services/invoice_service.py index 9f5a14d..227c2e5 100644 --- a/fiscalapi/services/invoice_service.py +++ b/fiscalapi/services/invoice_service.py @@ -63,12 +63,12 @@ def get_xml(self, invoice_id: int) -> ApiResponse[FileResponse]: # send invoice by email - def send(self, send_invoice_request : SendInvoiceRequest): + def send(self, send_invoice_request: SendInvoiceRequest) -> ApiResponse[bool]: if not send_invoice_request: raise ValueError("Invalid request") - + endpoint = "invoices/send" - + return self.send_request("POST", endpoint, bool, payload=send_invoice_request) # consultar estado de facturas From 0ae14500d29421dd11694489ef3990df65a4f5c6 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 19:45:26 -0600 Subject: [PATCH 03/28] Fix additional type annotations for Pylance/Pyright compatibility - Add Any type annotation to **kwargs in base_service.py methods - Add default=None to ApiResponse Optional fields (message, details) - Fix dict type annotation to dict[str, Any] in ValidationFailure - Fix TaxFile.file_type to Optional[Literal[0, 1]] - Replace List[...] with list[...] in download_request_service.py --- fiscalapi/models/common_models.py | 8 ++++---- fiscalapi/models/fiscalapi_models.py | 2 +- fiscalapi/services/base_service.py | 8 ++++---- fiscalapi/services/download_request_service.py | 13 ++++++------- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/fiscalapi/models/common_models.py b/fiscalapi/models/common_models.py index 0a0cd2f..18b7839 100644 --- a/fiscalapi/models/common_models.py +++ b/fiscalapi/models/common_models.py @@ -7,9 +7,9 @@ class ApiResponse(BaseModel, Generic[T]): succeeded: bool = Field(alias="succeeded") - message: Optional[str] = Field(alias="message") - details: Optional[str] = Field(alias="details") - data: Optional[T] = Field(None, alias="data") + message: Optional[str] = Field(default=None, alias="message") + details: Optional[str] = Field(default=None, alias="details") + data: Optional[T] = Field(default=None, alias="data") model_config = ConfigDict( populate_by_name=True, @@ -37,7 +37,7 @@ class ValidationFailure(BaseModel): customState: Optional[Any] = None severity: Optional[int] = None errorCode: Optional[str] = None - formattedMessagePlaceholderValues: Optional[dict] = None + formattedMessagePlaceholderValues: Optional[dict[str, Any]] = None model_config = ConfigDict(populate_by_name=True) diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index 857e9ae..533c949 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -85,7 +85,7 @@ class TaxFile(BaseDto): person_id: Optional[str] = Field(default=None, alias="personId", description="Id de la persona propietaria del certificado.") tin: Optional[str] = Field(default=None, alias="tin", description="RFC del propietario del certificado. Debe coincidir con el RFC del certificado.") base64_file: Optional[str] = Field(default=None, alias="base64File", description="Archivo certificado o llave privada en formato base64.") - file_type: Literal[0, 1] = Field(default=None, alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.") + file_type: Optional[Literal[0, 1]] = Field(default=None, alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.") password: Optional[str] = Field(default=None, alias="password", description="Contraseña de la llave privada.") valid_from: Optional[datetime] = Field(default=None, alias="validFrom", description="Fecha de inicio de vigencia del certificado o llave privada.") valid_to: Optional[datetime] = Field(default=None, alias="validTo", description="Fecha de fin de vigencia del certificado o llave privada.") diff --git a/fiscalapi/services/base_service.py b/fiscalapi/services/base_service.py index 7b9513b..4e042fb 100644 --- a/fiscalapi/services/base_service.py +++ b/fiscalapi/services/base_service.py @@ -1,6 +1,6 @@ -import urllib3 +import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) -from typing import Type, TypeVar, get_args, get_origin +from typing import Any, Type, TypeVar, get_args, get_origin import certifi from pydantic import BaseModel import requests @@ -25,7 +25,7 @@ def _get_headers(self) -> dict[str, str]: "X-TIME-ZONE": self.settings.time_zone, } - def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response: + def _request(self, method: str, endpoint: str, **kwargs: Any) -> requests.Response: url = f"{self.base_url}/api/{self.api_version}/{endpoint}" headers = self._get_headers() @@ -202,7 +202,7 @@ def _process_response(self, response: requests.Response, response_model: Type[T] # response = self._request(method, endpoint, **kwargs) # return self._process_response(response, response_model) - def send_request(self, method: str, endpoint: str, response_model: Type[T], details: bool = False, **kwargs) -> ApiResponse[T]: + def send_request(self, method: str, endpoint: str, response_model: Type[T], details: bool = False, **kwargs: Any) -> ApiResponse[T]: if details: endpoint += "?details=true" diff --git a/fiscalapi/services/download_request_service.py b/fiscalapi/services/download_request_service.py index 7fee886..16b0be8 100644 --- a/fiscalapi/services/download_request_service.py +++ b/fiscalapi/services/download_request_service.py @@ -1,5 +1,4 @@ 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 @@ -134,7 +133,7 @@ def get_metadata_items(self, request_id: str) -> ApiResponse[PagedList[MetadataI 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]]: + def download_package(self, request_id: str) -> ApiResponse[list[FileResponse]]: """ Descarga el paquete de archivos asociado a una solicitud de descarga. @@ -142,7 +141,7 @@ def download_package(self, request_id: str) -> ApiResponse[List[FileResponse]]: request_id (str): ID de la solicitud de descarga Returns: - ApiResponse[List[FileResponse]]: Lista de archivos del paquete + ApiResponse[list[FileResponse]]: Lista de archivos del paquete Raises: ValueError: Si request_id es None o vacío @@ -151,7 +150,7 @@ def download_package(self, request_id: str) -> ApiResponse[List[FileResponse]]: 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]) + return self.send_request("GET", endpoint, list[FileResponse]) def download_sat_request(self, request_id: str) -> ApiResponse[FileResponse]: """ @@ -191,7 +190,7 @@ def download_sat_response(self, request_id: str) -> ApiResponse[FileResponse]: endpoint = f"download-requests/{request_id}/raw-response" return self.send_request("GET", endpoint, FileResponse) - def search(self, created_at: datetime) -> ApiResponse[List[DownloadRequest]]: + def search(self, created_at: datetime) -> ApiResponse[list[DownloadRequest]]: """ Busca solicitudes de descarga por fecha de creación. @@ -199,7 +198,7 @@ def search(self, created_at: datetime) -> ApiResponse[List[DownloadRequest]]: created_at (datetime): Fecha de creación para buscar Returns: - ApiResponse[List[DownloadRequest]]: Lista de solicitudes encontradas + ApiResponse[list[DownloadRequest]]: Lista de solicitudes encontradas Raises: ValueError: Si created_at es None @@ -210,4 +209,4 @@ def search(self, created_at: datetime) -> ApiResponse[List[DownloadRequest]]: 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 + return self.send_request("GET", endpoint, list[DownloadRequest]) \ No newline at end of file From 4b80a6dc32557fc44ddd1f2f55b021597876b76c Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 19:52:16 -0600 Subject: [PATCH 04/28] Remove unnecessary None checks on non-Optional parameters Type annotations already prevent None from being passed, making these runtime checks redundant and causing Pylance warnings about conditions that always evaluate to False. --- .../services/download_request_service.py | 33 +++++-------------- fiscalapi/services/invoice_service.py | 26 ++------------- 2 files changed, 12 insertions(+), 47 deletions(-) diff --git a/fiscalapi/services/download_request_service.py b/fiscalapi/services/download_request_service.py index 16b0be8..93f3a50 100644 --- a/fiscalapi/services/download_request_service.py +++ b/fiscalapi/services/download_request_service.py @@ -38,41 +38,33 @@ def get_by_id(self, request_id: str, details: bool = False) -> ApiResponse[Downl 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 + ValueError: Si request_id es vacío """ 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) @@ -193,20 +185,13 @@ def download_sat_response(self, request_id: str) -> ApiResponse[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/invoice_service.py b/fiscalapi/services/invoice_service.py index 227c2e5..691a77f 100644 --- a/fiscalapi/services/invoice_service.py +++ b/fiscalapi/services/invoice_service.py @@ -29,62 +29,42 @@ def _get_endpoint_by_type(self, type_code: str) -> str: # create invoice def create(self, invoice: Invoice) -> ApiResponse[Invoice]: - if invoice is None: - raise ValueError("request_model cannot be null") - endpoint = self._get_endpoint_by_type(invoice.type_code) return self.send_request("POST", endpoint, Invoice, payload=invoice) # cancel invoice def cancel(self, cancel_invoice_request: CancelInvoiceRequest) -> ApiResponse[CancelInvoiceResponse]: - if cancel_invoice_request is None: - raise ValueError("request_model cannot be null") - endpoint = "invoices" return self.send_request("DELETE", endpoint, CancelInvoiceResponse, payload=cancel_invoice_request) # create invoice's pdf - def get_pdf(self, create_pdf_request:CreatePdfRequest) -> ApiResponse[FileResponse]: - if create_pdf_request is None: - raise ValueError("request_model cannot be null") - + def get_pdf(self, create_pdf_request: CreatePdfRequest) -> ApiResponse[FileResponse]: endpoint = "invoices/pdf" return self.send_request("POST", endpoint, FileResponse, payload=create_pdf_request) # get invoice's xml by id /api/v4/invoices//xml def get_xml(self, invoice_id: int) -> ApiResponse[FileResponse]: - if invoice_id is None: - raise ValueError("invoice_id cannot be null") - endpoint = f"invoices/{invoice_id}/xml" return self.send_request("GET", endpoint, FileResponse) # send invoice by email - def send(self, send_invoice_request: SendInvoiceRequest) -> ApiResponse[bool]: - if not send_invoice_request: - raise ValueError("Invalid request") - endpoint = "invoices/send" - return self.send_request("POST", endpoint, bool, payload=send_invoice_request) # consultar estado de facturas def get_status(self, request: InvoiceStatusRequest) -> ApiResponse[InvoiceStatusResponse]: """ Obtiene el estado de una factura. - + Args: request (InvoiceStatusRequest): Solicitud para consultar estado - + Returns: ApiResponse[InvoiceStatusResponse]: Respuesta con el estado de la factura """ - if request is None: - raise ValueError("request cannot be null") - endpoint = "invoices/status" return self.send_request("POST", endpoint, InvoiceStatusResponse, payload=request) From 12cace05e03d9be5369c8a5b272e6117a386ff09 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 19:53:41 -0600 Subject: [PATCH 05/28] Add type_code None check before calling _get_endpoint_by_type --- fiscalapi/services/invoice_service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fiscalapi/services/invoice_service.py b/fiscalapi/services/invoice_service.py index 691a77f..702bfd2 100644 --- a/fiscalapi/services/invoice_service.py +++ b/fiscalapi/services/invoice_service.py @@ -29,6 +29,8 @@ def _get_endpoint_by_type(self, type_code: str) -> str: # create invoice def create(self, invoice: Invoice) -> ApiResponse[Invoice]: + if invoice.type_code is None: + raise ValueError("Invoice type_code is required") endpoint = self._get_endpoint_by_type(invoice.type_code) return self.send_request("POST", endpoint, Invoice, payload=invoice) From 56292ba3df18868cefa1d14037933cac8330d8a9 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 19:56:42 -0600 Subject: [PATCH 06/28] Fix type errors in fiscalapi_models.py - Remove Optional from issuer/recipient fields (they use Field(...) which is required) - Add Optional to base64_cancellation_acknowledgement (has default=None) - Add model_config to TaxCredential, InvoiceIssuer, InvoiceRecipient, GlobalInformation, and RelatedInvoice for consistency --- fiscalapi/models/fiscalapi_models.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index 533c949..cd7ba11 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -105,6 +105,9 @@ class TaxCredential(BaseDto): file_type: Literal[0, 1] = Field(..., alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.") password: str = Field(..., alias="password", description="Contraseña del archivo .key independientemente de si es un archivo .cer o .key.") + model_config = ConfigDict(populate_by_name=True) + + class InvoiceIssuer(BaseDto): """Modelo para el emisor de la factura.""" id: Optional[str] = Field(default=None, alias="id", description="ID de la persona (emisora) en fiscalapi.") @@ -113,6 +116,9 @@ class InvoiceIssuer(BaseDto): tax_regime_code: Optional[str] = Field(default=None, alias="taxRegimeCode", description="Código del régimen fiscal del emisor.") tax_credentials: Optional[list[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor (archivos .cer y .key).") + model_config = ConfigDict(populate_by_name=True) + + class InvoiceRecipient(BaseDto): """Modelo para el receptor de la factura.""" id: Optional[str] = Field(default=None, alias="id", description="ID de la persona (receptora) en fiscalapi.") @@ -123,6 +129,9 @@ class InvoiceRecipient(BaseDto): zip_code: Optional[str] = Field(default=None, alias="zipCode", description="Código postal del receptor. Debe coincidir con el código postal de su constancia de residencia fiscal.") email: Optional[str] = Field(default=None, description="Correo electrónico del receptor.") + model_config = ConfigDict(populate_by_name=True) + + class ItemTax(BaseDto): """Modelo para los impuestos aplicables a un producto o servicio.""" tax_code: str = Field(..., alias="taxCode", description="Código del impuesto.") @@ -160,11 +169,17 @@ class GlobalInformation(BaseDto): month_code: str = Field(..., alias="monthCode", description="Código SAT del mes de la factura global.") year: int = Field(..., description="Año de la factura global a 4 dígitos.") + model_config = ConfigDict(populate_by_name=True) + + class RelatedInvoice(BaseDto): """Modelo para representar la relacion entre la factura actual y otras facturas previas.""" relationship_type_code: str = Field(..., alias="relationshipTypeCode", description="Código de la relación de la factura relacionada.") uuid: str = Field(..., description="UUID de la factura relacionada.") + model_config = ConfigDict(populate_by_name=True) + + class PaidInvoiceTax(BaseDto): """Modelo para los impuestos aplicables a la factura pagada.""" tax_code: str = Field(..., alias="taxCode", description="Código del impuesto.") @@ -261,8 +276,8 @@ class Invoice(BaseDto): export_code: Optional[Literal["01", "02", "03", "04"]] = Field(default="01", alias="exportCode", description="Código que identifica si la factura ampara una operación de exportación.") payment_method_code: Optional[Literal["PUE", "PPD"]] = Field(default=None, alias="paymentMethodCode", description="Código de método para la factura de pago.") exchange_rate: Optional[Decimal] = Field(default=1, alias="exchangeRate", description="Tipo de cambio FIX.") - issuer: Optional[InvoiceIssuer] = Field(..., description="El emisor de la factura.") - recipient: Optional[InvoiceRecipient] = Field(..., description="El receptor de la factura.") + issuer: InvoiceIssuer = Field(..., description="El emisor de la factura.") + recipient: InvoiceRecipient = Field(..., description="El receptor de la factura.") items: Optional[list[InvoiceItem]] = Field(default_factory=list, description="Conceptos de la factura (productos o servicios).") global_information: Optional[GlobalInformation] = Field(default=None, alias="globalInformation", description="Información global de la factura.") related_invoices: Optional[list[RelatedInvoice]] = Field(default=None, alias="relatedInvoices", description="Facturas relacionadas.") @@ -290,7 +305,7 @@ class CancelInvoiceRequest(BaseDto): class CancelInvoiceResponse(BaseDto): """Modelo de respuesta para la cancelación de factura.""" - base64_cancellation_acknowledgement: str = Field(default=None, alias="base64CancellationAcknowledgement", description="Acuse de cancelación en formato base64. Contiene el XML del acuse de cancelación del SAT codificado en base64.") + base64_cancellation_acknowledgement: Optional[str] = Field(default=None, alias="base64CancellationAcknowledgement", description="Acuse de cancelación en formato base64. Contiene el XML del acuse de cancelación del SAT codificado en base64.") invoice_uuids: Optional[dict[str, str]] = Field(default=None, alias="invoiceUuids", description="Diccionario de UUIDs de facturas con su respectivo código de estatus de cancelación. La llave es el UUID de la factura y el valor es el código de estatus.") model_config = ConfigDict(populate_by_name=True) From e1958b33b556c62f5376fdb1173500c9c77b5f43 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 20:01:06 -0600 Subject: [PATCH 07/28] Add explicit default=... to Field() for Pylance type inference Replace Field(..., alias) with Field(default=..., alias) pattern to help Pylance/Pyright properly infer field types. --- fiscalapi/models/common_models.py | 14 ++-- fiscalapi/models/fiscalapi_models.py | 100 +++++++++++++-------------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/fiscalapi/models/common_models.py b/fiscalapi/models/common_models.py index 18b7839..9db6cf9 100644 --- a/fiscalapi/models/common_models.py +++ b/fiscalapi/models/common_models.py @@ -6,7 +6,7 @@ T = TypeVar('T') class ApiResponse(BaseModel, Generic[T]): - succeeded: bool = Field(alias="succeeded") + succeeded: bool = Field(default=..., alias="succeeded") message: Optional[str] = Field(default=None, alias="message") details: Optional[str] = Field(default=None, alias="details") data: Optional[T] = Field(default=None, alias="data") @@ -20,11 +20,11 @@ class ApiResponse(BaseModel, Generic[T]): class PagedList(BaseModel, Generic[T]): """Modelo para la estructura de la lista paginada.""" items: list[T] = Field(default_factory=list, alias="items", description="Lista de elementos paginados") - page_number: int = Field(alias="pageNumber", description="Número de página actual") - total_pages: int = Field(alias="totalPages", description="Cantidad total de páginas") - total_count: int = Field(alias="totalCount", description="Cantidad total de elementos") - has_previous_page: bool = Field(alias="hasPreviousPage", description="Indica si hay una página anterior") - has_next_page: bool = Field(alias="hasNextPage", description="Indica si hay una página siguiente") + page_number: int = Field(default=..., alias="pageNumber", description="Número de página actual") + total_pages: int = Field(default=..., alias="totalPages", description="Cantidad total de páginas") + total_count: int = Field(default=..., alias="totalCount", description="Cantidad total de elementos") + has_previous_page: bool = Field(default=..., alias="hasPreviousPage", description="Indica si hay una página anterior") + has_next_page: bool = Field(default=..., alias="hasNextPage", description="Indica si hay una página siguiente") model_config = ConfigDict(populate_by_name=True) @@ -52,7 +52,7 @@ class BaseDto(BaseModel): class CatalogDto(BaseDto): """Modelo para catálogos.""" - description: str = Field(alias="description") + description: str = Field(default=..., alias="description") class FiscalApiSettings(BaseModel): diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index cd7ba11..f22b9d9 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -9,7 +9,7 @@ class ProductTax(BaseDto): """Modelo impuesto de producto.""" - rate: Decimal = Field(ge=0, le=1, alias="rate", description="Tasa de impuesto") + rate: Decimal = Field(default=..., ge=0, le=1, alias="rate", description="Tasa de impuesto") tax_id: Optional[Literal["001", "002", "003"]] = Field(default=None, alias="taxId", description="Impuesto") tax: Optional[CatalogDto] = Field(default=None, alias="tax", description="Impuesto expandido") @@ -28,8 +28,8 @@ class ProductTax(BaseDto): class Product(BaseDto): """Modelo producto.""" - description: str = Field(alias="description") - unit_price: Decimal = Field(alias="unitPrice") + description: str = Field(default=..., alias="description") + unit_price: Decimal = Field(default=..., alias="unitPrice") sat_unit_measurement_id: Optional[str] = Field(default="H87", alias="satUnitMeasurementId", description="Unidad de medida SAT") sat_unit_measurement: Optional[CatalogDto] = Field(default=None, alias="satUnitMeasurement", description="Unidad de medida SAT expandida") @@ -101,9 +101,9 @@ class TaxFile(BaseDto): class TaxCredential(BaseDto): """Modelo para los sellos del emisor (archivos .cer y .key).""" - base64_file: str = Field(..., alias="base64File", description="Archivo en formato base64.") - file_type: Literal[0, 1] = Field(..., alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.") - password: str = Field(..., alias="password", description="Contraseña del archivo .key independientemente de si es un archivo .cer o .key.") + base64_file: str = Field(default=..., alias="base64File", description="Archivo en formato base64.") + file_type: Literal[0, 1] = Field(default=..., alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.") + password: str = Field(default=..., alias="password", description="Contraseña del archivo .key independientemente de si es un archivo .cer o .key.") model_config = ConfigDict(populate_by_name=True) @@ -134,9 +134,9 @@ class InvoiceRecipient(BaseDto): class ItemTax(BaseDto): """Modelo para los impuestos aplicables a un producto o servicio.""" - tax_code: str = Field(..., alias="taxCode", description="Código del impuesto.") - tax_type_code: str = Field(..., alias="taxTypeCode", description="Tipo de factor.") - tax_rate: Decimal = Field(..., alias="taxRate", description="Tasa del impuesto.") + tax_code: str = Field(default=..., alias="taxCode", description="Código del impuesto.") + tax_type_code: str = Field(default=..., alias="taxTypeCode", description="Tipo de factor.") + tax_rate: Decimal = Field(default=..., alias="taxRate", description="Tasa del impuesto.") tax_flag_code: Optional[Literal["T", "R"]] = Field(default=None, alias="taxFlagCode", description="Código que indica la naturaleza del impuesto. (T)raslado o (R)etención.") model_config = ConfigDict( @@ -149,7 +149,7 @@ class InvoiceItem(BaseDto): """Modelo para los conceptos de la factura (productos o servicios).""" id: Optional[str] = Field(default=None, alias="id", description="ID del producto en fiscalapi.") item_code: Optional[str] = Field(default=None, alias="itemCode", description="Código SAT del producto o servicio.") - quantity: Decimal = Field(..., alias="quantity", description="Cantidad del producto o servicio.") + quantity: Decimal = Field(default=..., alias="quantity", description="Cantidad del producto o servicio.") discount: Optional[Decimal] = Field(default=None, alias="discount", description="Cantidad monetaria del descuento aplicado.") unit_of_measurement_code: Optional[str] = Field(default=None, alias="unitOfMeasurementCode", description="Código SAT de la unidad de medida.") description: Optional[str] = Field(default=None,alias="description", description="Descripción del producto o servicio.") @@ -165,26 +165,26 @@ class InvoiceItem(BaseDto): class GlobalInformation(BaseDto): """Modelo para la información global de la factura global.""" - periodicity_code: str = Field(..., alias="periodicityCode", description="Código SAT de la periodicidad de la factura global.") - month_code: str = Field(..., alias="monthCode", description="Código SAT del mes de la factura global.") - year: int = Field(..., description="Año de la factura global a 4 dígitos.") + periodicity_code: str = Field(default=..., alias="periodicityCode", description="Código SAT de la periodicidad de la factura global.") + month_code: str = Field(default=..., alias="monthCode", description="Código SAT del mes de la factura global.") + year: int = Field(default=..., description="Año de la factura global a 4 dígitos.") model_config = ConfigDict(populate_by_name=True) class RelatedInvoice(BaseDto): """Modelo para representar la relacion entre la factura actual y otras facturas previas.""" - relationship_type_code: str = Field(..., alias="relationshipTypeCode", description="Código de la relación de la factura relacionada.") - uuid: str = Field(..., description="UUID de la factura relacionada.") + relationship_type_code: str = Field(default=..., alias="relationshipTypeCode", description="Código de la relación de la factura relacionada.") + uuid: str = Field(default=..., description="UUID de la factura relacionada.") model_config = ConfigDict(populate_by_name=True) class PaidInvoiceTax(BaseDto): """Modelo para los impuestos aplicables a la factura pagada.""" - tax_code: str = Field(..., alias="taxCode", description="Código del impuesto.") - tax_type_code: str = Field(..., alias="taxTypeCode", description="Tipo de factor.") - tax_rate: Decimal = Field(..., alias="taxRate", description="Tasa del impuesto.") + tax_code: str = Field(default=..., alias="taxCode", description="Código del impuesto.") + tax_type_code: str = Field(default=..., alias="taxTypeCode", description="Tipo de factor.") + tax_rate: Decimal = Field(default=..., alias="taxRate", description="Tasa del impuesto.") tax_flag_code: Optional[Literal["T", "R"]] = Field(default=None, alias="taxFlagCode", description="Código que indica la naturaleza del impuesto. (T)raslado o (R)etención.") model_config = ConfigDict( @@ -194,20 +194,20 @@ class PaidInvoiceTax(BaseDto): class PaidInvoice(BaseDto): """Modelo para las facturas pagadas con el pago recibido.""" - uuid: str = Field(..., alias="uuid", description="UUID de la factura pagada.") - series: str = Field(..., alias="series", description="Serie de la factura pagada.") + uuid: str = Field(default=..., alias="uuid", description="UUID de la factura pagada.") + series: str = Field(default=..., alias="series", description="Serie de la factura pagada.") - partiality_number: int = Field(..., alias="partialityNumber", description="Número de parcialidad.") - sub_total: Decimal = Field(..., alias="subTotal", description="Subtotal de la factura pagada.") - previous_balance: Decimal = Field(..., alias="previousBalance", description="Saldo anterior de la factura pagada.") - payment_amount: Decimal = Field(..., alias="paymentAmount", description="Monto pagado de la factura.") - remaining_balance: Decimal = Field(..., alias="remainingBalance", description="Saldo restante de la factura pagada.") + partiality_number: int = Field(default=..., alias="partialityNumber", description="Número de parcialidad.") + sub_total: Decimal = Field(default=..., alias="subTotal", description="Subtotal de la factura pagada.") + previous_balance: Decimal = Field(default=..., alias="previousBalance", description="Saldo anterior de la factura pagada.") + payment_amount: Decimal = Field(default=..., alias="paymentAmount", description="Monto pagado de la factura.") + remaining_balance: Decimal = Field(default=..., alias="remainingBalance", description="Saldo restante de la factura pagada.") - number: str = Field(..., alias="number", description="Folio de la factura pagada.") + number: str = Field(default=..., alias="number", description="Folio de la factura pagada.") currency_code: str = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en la factura pagada.") - tax_object_code: str = Field(..., alias="taxObjectCode", description="Código de obligaciones de impuesto.") + tax_object_code: str = Field(default=..., alias="taxObjectCode", description="Código de obligaciones de impuesto.") equivalence: Optional[Decimal] = Field(default=1, description="Equivalencia de la moneda. Este campo es obligatorio cuando la moneda del documento relacionado (PaidInvoice.CurrencyCode) difiere de la moneda en que se realiza el pago ( InvoicePayment.CurrencyCode).") - paid_invoice_taxes: list[PaidInvoiceTax] = Field(..., alias="paidInvoiceTaxes", description="Impuestos aplicables a la factura pagada.") + paid_invoice_taxes: list[PaidInvoiceTax] = Field(default=..., alias="paidInvoiceTaxes", description="Impuestos aplicables a la factura pagada.") model_config = ConfigDict( populate_by_name=True, @@ -218,17 +218,17 @@ class PaidInvoice(BaseDto): class InvoicePayment(BaseDto): """Modelo para los pagos recibidos para liquidar la factura.""" - payment_date: str = Field(..., alias="paymentDate", description="Fecha de pago.") - payment_form_code: str = Field(..., alias="paymentFormCode", description="Código de la forma de pago.") + payment_date: str = Field(default=..., alias="paymentDate", description="Fecha de pago.") + payment_form_code: str = Field(default=..., alias="paymentFormCode", description="Código de la forma de pago.") currency_code: Literal ["MXN", "USD", "EUR"] = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en el pago.") exchange_rate: Optional[Decimal] = Field(default=1, alias="exchangeRate", description="Tipo de cambio FIX conforme a la moneda registrada en la factura. Si la moneda es MXN, el tipo de cambio debe ser 1..") - amount: Decimal = Field(..., description="Monto del pago.") - source_bank_tin: str = Field(..., alias="sourceBankTin", description="RFC del banco origen. (Rfc del banco emisor del pago)") - source_bank_account: str = Field(..., alias="sourceBankAccount", description="Cuenta bancaria origen. (Cuenta bancaria del banco emisor del pago)") - target_bank_tin: str = Field(..., alias="targetBankTin", description="RFC del banco destino. (Rfc del banco receptor del pago)") - target_bank_account: str = Field(..., alias="targetBankAccount", description="Cuenta bancaria destino (Cuenta bancaria del banco receptor del pago)") - paid_invoices: list[PaidInvoice] = Field(..., alias="paidInvoices", description="Facturas pagadas con el pago recibido.") + amount: Decimal = Field(default=..., description="Monto del pago.") + source_bank_tin: str = Field(default=..., alias="sourceBankTin", description="RFC del banco origen. (Rfc del banco emisor del pago)") + source_bank_account: str = Field(default=..., alias="sourceBankAccount", description="Cuenta bancaria origen. (Cuenta bancaria del banco emisor del pago)") + target_bank_tin: str = Field(default=..., alias="targetBankTin", description="RFC del banco destino. (Rfc del banco receptor del pago)") + target_bank_account: str = Field(default=..., alias="targetBankAccount", description="Cuenta bancaria destino (Cuenta bancaria del banco receptor del pago)") + paid_invoices: list[PaidInvoice] = Field(default=..., alias="paidInvoices", description="Facturas pagadas con el pago recibido.") model_config = ConfigDict( populate_by_name=True, @@ -267,17 +267,17 @@ class Invoice(BaseDto): total: Optional[Decimal] = Field(default=None, description="Total de la factura. Generado automáticamente por Fiscalapi.") uuid: Optional[str] = Field(default=None, description="UUID de la factura, es el folio fiscal asignado por el SAT al momento del timbrado.") status: Optional[CatalogDto] = Field(default=None, description="El estatus de la factura") - series: str = Field(..., description="Número de serie que utiliza el contribuyente para control interno.") - date: datetime = Field(..., description="Fecha y hora de expedición del comprobante fiscal.") + series: str = Field(default=..., description="Número de serie que utiliza el contribuyente para control interno.") + date: datetime = Field(default=..., description="Fecha y hora de expedición del comprobante fiscal.") payment_form_code: Optional[str] = Field(default=None, alias="paymentFormCode", description="Código de la forma de pago.") currency_code: Literal["MXN", "USD", "EUR", "XXX"] = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada.") type_code: Optional[Literal["I", "E", "T", "N", "P"]] = Field(default="I", alias="typeCode", description="Código de tipo de factura.") - expedition_zip_code: str = Field(..., alias="expeditionZipCode", description="Código postal del emisor.") + expedition_zip_code: str = Field(default=..., alias="expeditionZipCode", description="Código postal del emisor.") export_code: Optional[Literal["01", "02", "03", "04"]] = Field(default="01", alias="exportCode", description="Código que identifica si la factura ampara una operación de exportación.") payment_method_code: Optional[Literal["PUE", "PPD"]] = Field(default=None, alias="paymentMethodCode", description="Código de método para la factura de pago.") exchange_rate: Optional[Decimal] = Field(default=1, alias="exchangeRate", description="Tipo de cambio FIX.") - issuer: InvoiceIssuer = Field(..., description="El emisor de la factura.") - recipient: InvoiceRecipient = Field(..., description="El receptor de la factura.") + issuer: InvoiceIssuer = Field(default=..., description="El emisor de la factura.") + recipient: InvoiceRecipient = Field(default=..., description="El receptor de la factura.") items: Optional[list[InvoiceItem]] = Field(default_factory=list, description="Conceptos de la factura (productos o servicios).") global_information: Optional[GlobalInformation] = Field(default=None, alias="globalInformation", description="Información global de la factura.") related_invoices: Optional[list[RelatedInvoice]] = Field(default=None, alias="relatedInvoices", description="Facturas relacionadas.") @@ -297,7 +297,7 @@ class CancelInvoiceRequest(BaseDto): id: Optional[str] = Field(default=None, alias="id", description="ID de la factura a cancelar. Obligatorio cuando se cancela por referencias.") invoice_uuid: Optional[str] = Field(default=None, alias="invoiceUuid", description="UUID de la factura a cancelar. Obligatorio cuando se cancela por valores.") tin: Optional[str] = Field(default=None, alias="tin", description="RFC del emisor de la factura. Obligatorio cuando se cancela por valores.") - cancellation_reason_code: Literal["01", "02", "03", "04"] = Field(..., alias="cancellationReasonCode", description="Código del motivo de cancelación.") + cancellation_reason_code: Literal["01", "02", "03", "04"] = Field(default=..., alias="cancellationReasonCode", description="Código del motivo de cancelación.") replacement_uuid: Optional[str] = Field(default=None, alias="replacementUuid", description="UUID de la factura de reemplazo. Obligatorio si el motivo de cancelación es '01'.") tax_credentials: Optional[list[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor. Obligatorio cuando se cancela por valores.") @@ -313,7 +313,7 @@ class CancelInvoiceResponse(BaseDto): class CreatePdfRequest(BaseDto): """Modelo para la generación de PDF de una factura.""" - invoice_id: str = Field(..., alias="invoiceId", description="ID de la factura para la cual se generará el PDF.") + invoice_id: str = Field(default=..., alias="invoiceId", description="ID de la factura para la cual se generará el PDF.") band_color: Optional[str] = Field(default=None, alias="bandColor", description="Color de la banda del PDF en formato hexadecimal. Ejemplo: '#FFA500'.") font_color: Optional[str] = Field(default=None, alias="fontColor", description="Color de la fuente del texto sobre la banda en formato hexadecimal. Ejemplo: '#FFFFFF'.") base64_logo: Optional[str] = Field(default=None, alias="base64Logo", description="Logotipo en formato base64 que se mostrará en el PDF.") @@ -331,8 +331,8 @@ class FileResponse(BaseDto): class SendInvoiceRequest(BaseDto): """Modelo para el envío de facturas por correo electrónico.""" - invoice_id: str = Field(..., alias="invoiceId", description="ID de la factura para la cual se enviará el PDF.") - to_email: str = Field(..., alias="toEmail", description="Correo electrónico del destinatario.") + invoice_id: str = Field(default=..., alias="invoiceId", description="ID de la factura para la cual se enviará el PDF.") + to_email: str = Field(default=..., alias="toEmail", description="Correo electrónico del destinatario.") band_color: Optional[str] = Field(default=None, alias="bandColor", description="Color de la banda del PDF en formato hexadecimal. Ejemplo: '#FFA500'.") font_color: Optional[str] = Field(default=None, alias="fontColor", description="Color de la fuente del texto sobre la banda en formato hexadecimal. Ejemplo: '#FFFFFF'.") base64_logo: Optional[str] = Field(default=None, alias="base64Logo", description="Logotipo en formato base64 que se mostrará en el PDF.") @@ -356,11 +356,11 @@ class InvoiceStatusRequest(BaseDto): class InvoiceStatusResponse(BaseDto): """Modelo de respuesta de consulta de estado de facturas.""" - status_code: str = Field(..., alias="statusCode", description="Código de estatus retornado por el SAT") - status: str = Field(..., description="Estado actual de la factura. Posibles valores: 'Vigente' | 'Cancelado' | 'No Encontrado'") - cancelable_status: str = Field(..., alias="cancelableStatus", description="Indica si la factura es cancelable. Posibles valores: 'Cancelable con aceptación' | 'No cancelable' | 'Cancelable sin aceptación'") - cancellation_status: str = Field(..., alias="cancellationStatus", description="Detalle del estatus de cancelación") - efos_validation: str = Field(..., alias="efosValidation", description="Codigo que indica si el RFC Emisor se encuentra dentro de la lista negra de EFOS") + status_code: str = Field(default=..., alias="statusCode", description="Código de estatus retornado por el SAT") + status: str = Field(default=..., description="Estado actual de la factura. Posibles valores: 'Vigente' | 'Cancelado' | 'No Encontrado'") + cancelable_status: str = Field(default=..., alias="cancelableStatus", description="Indica si la factura es cancelable. Posibles valores: 'Cancelable con aceptación' | 'No cancelable' | 'Cancelable sin aceptación'") + cancellation_status: str = Field(default=..., alias="cancellationStatus", description="Detalle del estatus de cancelación") + efos_validation: str = Field(default=..., alias="efosValidation", description="Codigo que indica si el RFC Emisor se encuentra dentro de la lista negra de EFOS") model_config = ConfigDict(populate_by_name=True) From 32687444b9838be3ebd5a5c5f4c7ecb5eaceb7b0 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 20:12:23 -0600 Subject: [PATCH 08/28] Fix FiscalApiSettings defaults and Decimal type mismatches - Add explicit default= to FiscalApiSettings optional fields - Change int defaults to Decimal("1") for Decimal fields to satisfy Pylance --- fiscalapi/models/common_models.py | 12 ++++++------ fiscalapi/models/fiscalapi_models.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/fiscalapi/models/common_models.py b/fiscalapi/models/common_models.py index 9db6cf9..a6917a3 100644 --- a/fiscalapi/models/common_models.py +++ b/fiscalapi/models/common_models.py @@ -59,12 +59,12 @@ class FiscalApiSettings(BaseModel): """ Objeto que contiene la configuración necesaria para interactuar con Fiscalapi. """ - api_url: str = Field(..., description="URL base de la api.") - api_key: str = Field(..., description="Api Key") - tenant: str = Field(..., description="Tenant Key.") - api_version: str = Field("v4", description="Versión de la api.") - time_zone: str = Field("America/Mexico_City", description="Zona horaria ") - debug: bool = Field(False, description="Indica si se debe imprimir el payload request y response.") + api_url: str = Field(default=..., description="URL base de la api.") + api_key: str = Field(default=..., description="Api Key") + tenant: str = Field(default=..., description="Tenant Key.") + api_version: str = Field(default="v4", description="Versión de la api.") + time_zone: str = Field(default="America/Mexico_City", description="Zona horaria ") + debug: bool = Field(default=False, description="Indica si se debe imprimir el payload request y response.") model_config = ConfigDict( title="FiscalApi Settings", diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index f22b9d9..606b076 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -206,7 +206,7 @@ class PaidInvoice(BaseDto): number: str = Field(default=..., alias="number", description="Folio de la factura pagada.") currency_code: str = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en la factura pagada.") tax_object_code: str = Field(default=..., alias="taxObjectCode", description="Código de obligaciones de impuesto.") - equivalence: Optional[Decimal] = Field(default=1, description="Equivalencia de la moneda. Este campo es obligatorio cuando la moneda del documento relacionado (PaidInvoice.CurrencyCode) difiere de la moneda en que se realiza el pago ( InvoicePayment.CurrencyCode).") + equivalence: Optional[Decimal] = Field(default=Decimal("1"), description="Equivalencia de la moneda. Este campo es obligatorio cuando la moneda del documento relacionado (PaidInvoice.CurrencyCode) difiere de la moneda en que se realiza el pago ( InvoicePayment.CurrencyCode).") paid_invoice_taxes: list[PaidInvoiceTax] = Field(default=..., alias="paidInvoiceTaxes", description="Impuestos aplicables a la factura pagada.") model_config = ConfigDict( @@ -222,7 +222,7 @@ class InvoicePayment(BaseDto): payment_form_code: str = Field(default=..., alias="paymentFormCode", description="Código de la forma de pago.") currency_code: Literal ["MXN", "USD", "EUR"] = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en el pago.") - exchange_rate: Optional[Decimal] = Field(default=1, alias="exchangeRate", description="Tipo de cambio FIX conforme a la moneda registrada en la factura. Si la moneda es MXN, el tipo de cambio debe ser 1..") + exchange_rate: Optional[Decimal] = Field(default=Decimal("1"), alias="exchangeRate", description="Tipo de cambio FIX conforme a la moneda registrada en la factura. Si la moneda es MXN, el tipo de cambio debe ser 1..") amount: Decimal = Field(default=..., description="Monto del pago.") source_bank_tin: str = Field(default=..., alias="sourceBankTin", description="RFC del banco origen. (Rfc del banco emisor del pago)") source_bank_account: str = Field(default=..., alias="sourceBankAccount", description="Cuenta bancaria origen. (Cuenta bancaria del banco emisor del pago)") @@ -275,7 +275,7 @@ class Invoice(BaseDto): expedition_zip_code: str = Field(default=..., alias="expeditionZipCode", description="Código postal del emisor.") export_code: Optional[Literal["01", "02", "03", "04"]] = Field(default="01", alias="exportCode", description="Código que identifica si la factura ampara una operación de exportación.") payment_method_code: Optional[Literal["PUE", "PPD"]] = Field(default=None, alias="paymentMethodCode", description="Código de método para la factura de pago.") - exchange_rate: Optional[Decimal] = Field(default=1, alias="exchangeRate", description="Tipo de cambio FIX.") + exchange_rate: Optional[Decimal] = Field(default=Decimal("1"), alias="exchangeRate", description="Tipo de cambio FIX.") issuer: InvoiceIssuer = Field(default=..., description="El emisor de la factura.") recipient: InvoiceRecipient = Field(default=..., description="El receptor de la factura.") items: Optional[list[InvoiceItem]] = Field(default_factory=list, description="Conceptos de la factura (productos o servicios).") From 82e2c660087f03db326384aaf3ba7a02d4e80ab3 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 20:21:56 -0600 Subject: [PATCH 09/28] python version updated --- examples.py | 8 +++----- fiscalapi/services/download_rule_service.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/examples.py b/examples.py index a872a23..89da1fd 100644 --- a/examples.py +++ b/examples.py @@ -1,7 +1,5 @@ -from datetime import datetime, timedelta -from decimal import Decimal +from datetime import datetime from fiscalapi.models.common_models import FiscalApiSettings -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 (): @@ -1466,8 +1464,8 @@ def main (): # print(api_response) # Buscar solicitud de descarga masiva por fecha de creación. - # api_response = client.download_requests.search(datetime.now()) - # print(api_response) + api_response = client.download_requests.search(datetime.now()) + print(api_response) diff --git a/fiscalapi/services/download_rule_service.py b/fiscalapi/services/download_rule_service.py index be6e234..2717bdb 100644 --- a/fiscalapi/services/download_rule_service.py +++ b/fiscalapi/services/download_rule_service.py @@ -1,5 +1,5 @@ from fiscalapi.models.common_models import ApiResponse, PagedList -from fiscalapi.models.fiscalapi_models import DownloadRule, DownloadRequest +from fiscalapi.models.fiscalapi_models import DownloadRule from fiscalapi.services.base_service import BaseService From 161ae94f8feaa19420af01c1209dfe655a3000ca Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 20:33:15 -0600 Subject: [PATCH 10/28] Update CI/CD pipeline to use Python 3.9.13 Co-Authored-By: Claude Opus 4.5 --- .github/workflows/CICD.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index cb3bab0..2229b04 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.9" + python-version: "3.9.13" - name: Install build dependencies run: | From a61ecf8a850486995aa19edc2783a8c2484e5abc Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 20:37:10 -0600 Subject: [PATCH 11/28] Update CLAUDE.md with Python 3.9 requirements and code standards - Update Python version requirement to >= 3.9 (CI/CD uses 3.9.13) - Add development setup instructions - Add Pydantic v2 compatibility guidelines - Add type annotation standards Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6c8560b..d83ea15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,11 +94,42 @@ See `examples.py` and README.md for detailed examples of both modes. ## Dependencies -- Python >= 3.7 +- Python >= 3.9 (CI/CD uses Python 3.9.13) - pydantic >= 2.0.0 (validation & serialization) - requests >= 2.0.0 (HTTP client) - email_validator >= 2.2.0 +## Development Setup + +```bash +# Create virtual environment with Python 3.9.13 +python -m venv venv + +# Activate (Windows) +.\venv\Scripts\activate + +# Activate (Linux/Mac) +source venv/bin/activate + +# Install dependencies +pip install -r requirements.txt +``` + +## Code Standards + +### Pydantic v2 Compatibility + +- Use `model_config = ConfigDict(...)` instead of `class Config:` +- Use `list[T]` and `dict[K,V]` (Python 3.9+ built-in generics) instead of `List[T]` and `Dict[K,V]` +- Use `default_factory=list` for mutable defaults, never `default=[]` +- All Field() calls should have explicit `default=...` for required fields + +### Type Annotations + +- All service methods must have return type annotations +- Use `Optional[T]` only for truly optional fields +- `ApiResponse[T]` supports any type T (not just BaseModel subclasses) + ## External Resources - API Documentation: https://docs.fiscalapi.com From cb7e5fe3f171e347ad2a1f7576947ee1f467ebd3 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 18 Jan 2026 20:43:10 -0600 Subject: [PATCH 12/28] Add http_status_code field to ApiResponse model - Add http_status_code field to ApiResponse in common_models.py - Restore http_status_code parameter in all ApiResponse instantiations in base_service.py Co-Authored-By: Claude Opus 4.5 --- fiscalapi/models/common_models.py | 1 + fiscalapi/services/base_service.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/fiscalapi/models/common_models.py b/fiscalapi/models/common_models.py index a6917a3..541c086 100644 --- a/fiscalapi/models/common_models.py +++ b/fiscalapi/models/common_models.py @@ -10,6 +10,7 @@ class ApiResponse(BaseModel, Generic[T]): message: Optional[str] = Field(default=None, alias="message") details: Optional[str] = Field(default=None, alias="details") data: Optional[T] = Field(default=None, alias="data") + http_status_code: Optional[int] = Field(default=None, alias="httpStatusCode") model_config = ConfigDict( populate_by_name=True, diff --git a/fiscalapi/services/base_service.py b/fiscalapi/services/base_service.py index 4e042fb..122ad60 100644 --- a/fiscalapi/services/base_service.py +++ b/fiscalapi/services/base_service.py @@ -130,6 +130,7 @@ def _process_response(self, response: requests.Response, response_model: Type[T] except ValueError: return ApiResponse[T]( succeeded=False, + http_status_code=status_code, message="Error processing server response", details=raw_content, data=None @@ -165,6 +166,7 @@ def _process_response(self, response: requests.Response, response_model: Type[T] except Exception: return ApiResponse[T]( succeeded=False, + http_status_code=status_code, message="Error processing server error response", details=raw_content, data=None @@ -177,6 +179,7 @@ def _process_response(self, response: requests.Response, response_model: Type[T] details_str = "; ".join(f"{f.propertyName}: {f.errorMessage}" for f in failures) return ApiResponse[T]( succeeded=False, + http_status_code=status_code, message=generic_error.message, details=details_str, data=None @@ -186,6 +189,7 @@ def _process_response(self, response: requests.Response, response_model: Type[T] return ApiResponse[T]( succeeded=False, + http_status_code=status_code, message=generic_error.message or f"HTTP Error {status_code}", details=generic_error.details or raw_content, data=None From eb0f4394d7b1d41cb7425772ca6168bed04dcc39 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Tue, 20 Jan 2026 18:17:32 -0600 Subject: [PATCH 13/28] Add Employee/Employer services and improve package structure - Add EmployeeData and EmployerData models for CFDI nomina support - Add EmployeeService and EmployerService as sub-resources of PeopleService - Fix typo: rename tax_file_servive.py to tax_file_service.py - Add py.typed marker for PEP 561 type checking support - Add MANIFEST.in for proper sdist packaging - Reorganize exports in __init__.py files for models and services - Export BaseService for custom service implementations - Fix ValidationFailure model to use snake_case with aliases - Remove debug print statements from base_service.py - Remove commented-out code from base_service.py - Fix indentation in TaxFileService Co-Authored-By: Claude Opus 4.5 --- MANIFEST.in | 3 + fiscalapi/__init__.py | 167 ++++++++++++++----------- fiscalapi/models/__init__.py | 103 +++++++++++++++ fiscalapi/models/common_models.py | 10 +- fiscalapi/models/fiscalapi_models.py | 53 +++++++- fiscalapi/py.typed | 0 fiscalapi/services/__init__.py | 31 +++++ fiscalapi/services/base_service.py | 99 ++------------- fiscalapi/services/employee_service.py | 23 ++++ fiscalapi/services/employer_service.py | 23 ++++ fiscalapi/services/fiscalapi_client.py | 2 +- fiscalapi/services/people_service.py | 27 +++- fiscalapi/services/tax_file_service.py | 32 +++++ fiscalapi/services/tax_file_servive.py | 41 ------ setup.py | 4 + 15 files changed, 398 insertions(+), 220 deletions(-) create mode 100644 MANIFEST.in create mode 100644 fiscalapi/py.typed create mode 100644 fiscalapi/services/employee_service.py create mode 100644 fiscalapi/services/employer_service.py create mode 100644 fiscalapi/services/tax_file_service.py delete mode 100644 fiscalapi/services/tax_file_servive.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c78194b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include fiscalapi/py.typed +include README.md +include LICENSE.txt diff --git a/fiscalapi/__init__.py b/fiscalapi/__init__.py index 402dd6d..6b2fa57 100644 --- a/fiscalapi/__init__.py +++ b/fiscalapi/__init__.py @@ -1,130 +1,149 @@ -# fiscalapi/__init__.py +""" +FiscalAPI Python SDK -# Re-exportar modelos de common_models +SDK oficial para integración con FiscalAPI, la plataforma de facturación +electrónica (CFDI 4.0) y servicios fiscales de México. + +Ejemplo de uso: + >>> from fiscalapi import FiscalApiClient, FiscalApiSettings + >>> settings = FiscalApiSettings(api_url="...", api_key="...", tenant="...") + >>> client = FiscalApiClient(settings=settings) + >>> response = client.invoices.get_list(page_number=1, page_size=10) +""" + +# Modelos base from .models.common_models import ( ApiResponse, - PagedList, - ValidationFailure, BaseDto, CatalogDto, FiscalApiSettings, + PagedList, + ValidationFailure, ) -# Re-exportar modelos de fiscalapi_models +# Modelos de dominio from .models.fiscalapi_models import ( - ProductTax, - Product, - Person, - TaxFile, - TaxCredential, - InvoiceIssuer, - InvoiceRecipient, - ItemTax, - InvoiceItem, - GlobalInformation, - RelatedInvoice, - PaidInvoiceTax, - PaidInvoice, - InvoicePayment, - InvoiceResponse, - Invoice, + ApiKey, CancelInvoiceRequest, CancelInvoiceResponse, CreatePdfRequest, + DownloadRequest, + DownloadRule, + EmployeeData, + EmployerData, FileResponse, - SendInvoiceRequest, + GlobalInformation, + Invoice, + InvoiceIssuer, + InvoiceItem, + InvoicePayment, + InvoiceRecipient, + InvoiceResponse, InvoiceStatusRequest, InvoiceStatusResponse, - ApiKey, - DownloadRule, - DownloadRequest, + ItemTax, MetadataItem, + PaidInvoice, + PaidInvoiceTax, + Person, + Product, + ProductTax, + RelatedInvoice, + SendInvoiceRequest, + TaxCredential, + TaxFile, + Xml, + XmlComplement, XmlGlobalInformation, XmlIssuer, - XmlRecipient, - XmlRelated, - XmlTax, + XmlItem, XmlItemCustomsInformation, XmlItemPropertyAccount, XmlItemTax, - XmlItem, - XmlComplement, - Xml, + XmlRecipient, + XmlRelated, + XmlTax, ) -# Re-exportar servicios +# Servicios +from .services.base_service import BaseService +from .services.api_key_service import ApiKeyService from .services.catalog_service import CatalogService +from .services.download_catalog_service import DownloadCatalogService +from .services.download_request_service import DownloadRequestService +from .services.download_rule_service import DownloadRuleService +from .services.employee_service import EmployeeService +from .services.employer_service import EmployerService from .services.invoice_service import InvoiceService from .services.people_service import PeopleService 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 +from .services.tax_file_service import TaxFileService -# Re-exportar la clase FiscalApiClient -# (asumiendo que la definición está en fiscalapi/services/fiscalapi_client.py) +# Cliente principal from .services.fiscalapi_client import FiscalApiClient __all__ = [ - # Modelos + # Modelos base "ApiResponse", - "PagedList", - "ValidationFailure", "BaseDto", "CatalogDto", "FiscalApiSettings", - "ProductTax", - "Product", - "Person", - "TaxFile", - "TaxCredential", - "InvoiceIssuer", - "InvoiceRecipient", - "ItemTax", - "InvoiceItem", - "GlobalInformation", - "RelatedInvoice", - "PaidInvoiceTax", - "PaidInvoice", - "InvoicePayment", - "InvoiceResponse", - "Invoice", + "PagedList", + "ValidationFailure", + # Modelos de dominio + "ApiKey", "CancelInvoiceRequest", "CancelInvoiceResponse", "CreatePdfRequest", + "DownloadRequest", + "DownloadRule", + "EmployeeData", + "EmployerData", "FileResponse", - "SendInvoiceRequest", + "GlobalInformation", + "Invoice", + "InvoiceIssuer", + "InvoiceItem", + "InvoicePayment", + "InvoiceRecipient", + "InvoiceResponse", "InvoiceStatusRequest", "InvoiceStatusResponse", - "ApiKey", - "DownloadRule", - "DownloadRequest", + "ItemTax", "MetadataItem", + "PaidInvoice", + "PaidInvoiceTax", + "Person", + "Product", + "ProductTax", + "RelatedInvoice", + "SendInvoiceRequest", + "TaxCredential", + "TaxFile", + "Xml", + "XmlComplement", "XmlGlobalInformation", "XmlIssuer", - "XmlRecipient", - "XmlRelated", - "XmlTax", + "XmlItem", "XmlItemCustomsInformation", "XmlItemPropertyAccount", "XmlItemTax", - "XmlItem", - "XmlComplement", - "Xml", - + "XmlRecipient", + "XmlRelated", + "XmlTax", # Servicios + "BaseService", + "ApiKeyService", "CatalogService", + "DownloadCatalogService", + "DownloadRequestService", + "DownloadRuleService", + "EmployeeService", + "EmployerService", "InvoiceService", "PeopleService", "ProductService", "TaxFileService", - "ApiKeyService", - "DownloadCatalogService", - "DownloadRuleService", - "DownloadRequestService", - # Cliente principal "FiscalApiClient", ] diff --git a/fiscalapi/models/__init__.py b/fiscalapi/models/__init__.py index e69de29..da402a1 100644 --- a/fiscalapi/models/__init__.py +++ b/fiscalapi/models/__init__.py @@ -0,0 +1,103 @@ +"""Modelos de FiscalAPI.""" + +from .common_models import ( + ApiResponse, + BaseDto, + CatalogDto, + FiscalApiSettings, + PagedList, + ValidationFailure, +) +from .fiscalapi_models import ( + ApiKey, + CancelInvoiceRequest, + CancelInvoiceResponse, + CreatePdfRequest, + DownloadRequest, + DownloadRule, + EmployeeData, + EmployerData, + FileResponse, + GlobalInformation, + Invoice, + InvoiceIssuer, + InvoiceItem, + InvoicePayment, + InvoiceRecipient, + InvoiceResponse, + InvoiceStatusRequest, + InvoiceStatusResponse, + ItemTax, + MetadataItem, + PaidInvoice, + PaidInvoiceTax, + Person, + Product, + ProductTax, + RelatedInvoice, + SendInvoiceRequest, + TaxCredential, + TaxFile, + Xml, + XmlComplement, + XmlGlobalInformation, + XmlIssuer, + XmlItem, + XmlItemCustomsInformation, + XmlItemPropertyAccount, + XmlItemTax, + XmlRecipient, + XmlRelated, + XmlTax, +) + +__all__ = [ + # common_models + "ApiResponse", + "BaseDto", + "CatalogDto", + "FiscalApiSettings", + "PagedList", + "ValidationFailure", + # fiscalapi_models + "ApiKey", + "CancelInvoiceRequest", + "CancelInvoiceResponse", + "CreatePdfRequest", + "DownloadRequest", + "DownloadRule", + "EmployeeData", + "EmployerData", + "FileResponse", + "GlobalInformation", + "Invoice", + "InvoiceIssuer", + "InvoiceItem", + "InvoicePayment", + "InvoiceRecipient", + "InvoiceResponse", + "InvoiceStatusRequest", + "InvoiceStatusResponse", + "ItemTax", + "MetadataItem", + "PaidInvoice", + "PaidInvoiceTax", + "Person", + "Product", + "ProductTax", + "RelatedInvoice", + "SendInvoiceRequest", + "TaxCredential", + "TaxFile", + "Xml", + "XmlComplement", + "XmlGlobalInformation", + "XmlIssuer", + "XmlItem", + "XmlItemCustomsInformation", + "XmlItemPropertyAccount", + "XmlItemTax", + "XmlRecipient", + "XmlRelated", + "XmlTax", +] diff --git a/fiscalapi/models/common_models.py b/fiscalapi/models/common_models.py index 541c086..4d11bcf 100644 --- a/fiscalapi/models/common_models.py +++ b/fiscalapi/models/common_models.py @@ -32,13 +32,9 @@ class PagedList(BaseModel, Generic[T]): class ValidationFailure(BaseModel): """Modelo para errores de validación.""" - propertyName: str - errorMessage: str - attemptedValue: Optional[Any] = None - customState: Optional[Any] = None - severity: Optional[int] = None - errorCode: Optional[str] = None - formattedMessagePlaceholderValues: Optional[dict[str, Any]] = None + property_name: str = Field(alias="propertyName") + error_message: str = Field(alias="errorMessage") + attempted_value: Optional[Any] = Field(default=None, alias="attemptedValue") model_config = ConfigDict(populate_by_name=True) diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index 606b076..1bb1ddd 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -77,8 +77,57 @@ class Person(BaseDto): populate_by_name=True, json_encoders={Decimal: str} ) - - + + +class EmployeeData(BaseDto): + """Modelo empleado para CFDI de nomina.""" + + employer_person_id: Optional[str] = Field(default=None, alias="employerPersonId") + employee_person_id: Optional[str] = Field(default=None, alias="employeePersonId") + social_security_number: Optional[str] = Field(default=None, alias="socialSecurityNumber") + labor_relation_start_date: Optional[datetime] = Field(default=None, alias="laborRelationStartDate") + seniority: Optional[int] = Field(default=None, alias="seniority") + sat_contract_type_id: Optional[str] = Field(default=None, alias="satContractTypeId") + sat_contract_type: Optional[CatalogDto] = Field(default=None, alias="satContractType") + sat_unionized_status_id: Optional[str] = Field(default=None, alias="satUnionizedStatusId") + sat_unionized_status: Optional[CatalogDto] = Field(default=None, alias="satUnionizedStatus") + sat_tax_regime_type_id: Optional[str] = Field(default=None, alias="satTaxRegimeTypeId") + sat_tax_regime_type: Optional[CatalogDto] = Field(default=None, alias="satTaxRegimeType") + sat_workday_type_id: Optional[str] = Field(default=None, alias="satWorkdayTypeId") + sat_workday_type: Optional[CatalogDto] = Field(default=None, alias="satWorkdayType") + sat_job_risk_id: Optional[str] = Field(default=None, alias="satJobRiskId") + sat_job_risk: Optional[CatalogDto] = Field(default=None, alias="satJobRisk") + sat_payment_periodicity_id: Optional[str] = Field(default=None, alias="satPaymentPeriodicityId") + sat_payment_periodicity: Optional[CatalogDto] = Field(default=None, alias="satPaymentPeriodicity") + employee_number: Optional[str] = Field(default=None, alias="employeeNumber") + sat_bank_id: Optional[str] = Field(default=None, alias="satBankId") + sat_bank: Optional[CatalogDto] = Field(default=None, alias="satBank") + sat_payroll_state_id: Optional[str] = Field(default=None, alias="satPayrollStateId") + sat_payroll_state: Optional[CatalogDto] = Field(default=None, alias="satPayrollState") + department: Optional[str] = Field(default=None, alias="department") + position: Optional[str] = Field(default=None, alias="position") + bank_account: Optional[str] = Field(default=None, alias="bankAccount") + base_salary_for_contributions: Optional[Decimal] = Field(default=None, alias="baseSalaryForContributions") + integrated_daily_salary: Optional[Decimal] = Field(default=None, alias="integratedDailySalary") + subcontractor_rfc: Optional[str] = Field(default=None, alias="subcontractorRfc") + time_percentage: Optional[Decimal] = Field(default=None, alias="timePercentage") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class EmployerData(BaseDto): + """Modelo empleador para CFDI de nomina.""" + + person_id: Optional[str] = Field(default=None, alias="personId") + employer_registration: Optional[str] = Field(default=None, alias="employerRegistration") + origin_employer_tin: Optional[str] = Field(default=None, alias="originEmployerTin") + sat_fund_source_id: Optional[str] = Field(default=None, alias="satFundSourceId") + sat_fund_source: Optional[CatalogDto] = Field(default=None, alias="satFundSource") + own_resource_amount: Optional[Decimal] = Field(default=None, alias="ownResourceAmount") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + class TaxFile(BaseDto): """Modelo TaxFile que representa un componente de un par CSD: certificado (.cer) o llave privada (.key).""" diff --git a/fiscalapi/py.typed b/fiscalapi/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/fiscalapi/services/__init__.py b/fiscalapi/services/__init__.py index e69de29..2b82b6e 100644 --- a/fiscalapi/services/__init__.py +++ b/fiscalapi/services/__init__.py @@ -0,0 +1,31 @@ +"""Servicios de FiscalAPI.""" + +from .base_service import BaseService +from .api_key_service import ApiKeyService +from .catalog_service import CatalogService +from .download_catalog_service import DownloadCatalogService +from .download_request_service import DownloadRequestService +from .download_rule_service import DownloadRuleService +from .employee_service import EmployeeService +from .employer_service import EmployerService +from .fiscalapi_client import FiscalApiClient +from .invoice_service import InvoiceService +from .people_service import PeopleService +from .product_service import ProductService +from .tax_file_service import TaxFileService + +__all__ = [ + "BaseService", + "ApiKeyService", + "CatalogService", + "DownloadCatalogService", + "DownloadRequestService", + "DownloadRuleService", + "EmployeeService", + "EmployerService", + "FiscalApiClient", + "InvoiceService", + "PeopleService", + "ProductService", + "TaxFileService", +] diff --git a/fiscalapi/services/base_service.py b/fiscalapi/services/base_service.py index 122ad60..9405820 100644 --- a/fiscalapi/services/base_service.py +++ b/fiscalapi/services/base_service.py @@ -32,95 +32,24 @@ def _request(self, method: str, endpoint: str, **kwargs: Any) -> requests.Respon if "headers" in kwargs: headers.update(kwargs.pop("headers")) - # Disable certificate validation (for development only!) - # kwargs.setdefault("verify", False) - - - # print payload request if self.settings.debug: print("***Payload Request:", kwargs.get("json")) - - # print line breaks - print("\n\n") - - - # *** DEV ONLY: Disable SSL verification for localhost *** + + # Disable SSL verification for localhost (development only) if "localhost" in url or "127.0.0.1" in url: kwargs["verify"] = False else: - # Use the default cert store kwargs["verify"] = certifi.where() - - - # send request + + # Send request response = requests.request(method=method, url=url, headers=headers, **kwargs) # print payload response if self.settings.debug: print("***Payload Response:", response.text) - - # print line breaks - print("\n\n") return response - # def _process_response(self, response: requests.Response, response_model: Type[T]) -> ApiResponse[T]: - # status_code = response.status_code - # raw_content = response.text - - # try: - # response_data = response.json() - # except ValueError: - # return ApiResponse[T]( - # succeeded=False, - # http_status_code=status_code, - # message="Error processing server response", - # details=raw_content, - # data=None - # ) - - # if 200 <= status_code < 300: - - # if issubclass(response_model, BaseModel) and isinstance(response_data["data"], dict): - # response_data["data"] = response_model.model_validate(response_data["data"]) - - # return ApiResponse[T].model_validate(response_data) - - # try: - # generic_error = ApiResponse[object].model_validate(response_data) - # except Exception: - # return ApiResponse[T]( - # succeeded=False, - # http_status_code=status_code, - # message="Error processing server error response", - # details=raw_content, - # data=None - # ) - - # if status_code == 400 and isinstance(response_data.get("data"), list): - # try: - # failures = [ValidationFailure.model_validate(item) for item in response_data["data"]] - # if failures: - # details_str = "; ".join(f"{f.propertyName}: {f.errorMessage}" for f in failures) - # return ApiResponse[T]( - # succeeded=False, - # http_status_code=400, - # message=generic_error.message, - # details=details_str, - # data=None - # ) - # except Exception: - # pass - - # return ApiResponse[T]( - # succeeded=False, - # http_status_code=status_code, - # message=generic_error.message or f"HTTP Error {status_code}", - # details=generic_error.details or raw_content, - # data=None - # ) - - def _process_response(self, response: requests.Response, response_model: Type[T]) -> ApiResponse[T]: status_code = response.status_code raw_content = response.text @@ -176,7 +105,7 @@ def _process_response(self, response: requests.Response, response_model: Type[T] try: failures = [ValidationFailure.model_validate(item) for item in response_data["data"]] if failures: - details_str = "; ".join(f"{f.propertyName}: {f.errorMessage}" for f in failures) + details_str = "; ".join(f"{f.property_name}: {f.error_message}" for f in failures) return ApiResponse[T]( succeeded=False, http_status_code=status_code, @@ -194,28 +123,14 @@ def _process_response(self, response: requests.Response, response_model: Type[T] details=generic_error.details or raw_content, data=None ) - - - - # def send_request(self, method: str, endpoint: str, response_model: Type[T], **kwargs) -> ApiResponse[T]: - # payload = kwargs.pop("payload", None) - # if payload is not None and isinstance(payload, BaseModel): - # # Excluir propiedades con valor None - # kwargs["json"] = payload.model_dump(mode="json", by_alias=True, exclude_none=True) - - # response = self._request(method, endpoint, **kwargs) - # return self._process_response(response, response_model) - + def send_request(self, method: str, endpoint: str, response_model: Type[T], details: bool = False, **kwargs: Any) -> ApiResponse[T]: if details: endpoint += "?details=true" - + payload = kwargs.pop("payload", None) if payload is not None and isinstance(payload, BaseModel): - # Excluir propiedades con valor None kwargs["json"] = payload.model_dump(mode="json", by_alias=True, exclude_none=True) response = self._request(method, endpoint, **kwargs) return self._process_response(response, response_model) - - \ No newline at end of file diff --git a/fiscalapi/services/employee_service.py b/fiscalapi/services/employee_service.py new file mode 100644 index 0000000..322fe54 --- /dev/null +++ b/fiscalapi/services/employee_service.py @@ -0,0 +1,23 @@ +from fiscalapi.models.common_models import ApiResponse +from fiscalapi.models.fiscalapi_models import EmployeeData +from fiscalapi.services.base_service import BaseService + + +class EmployeeService(BaseService): + """Servicio para gestionar empleados (sub-recurso de personas).""" + + def get_by_id(self, person_id: str) -> ApiResponse[EmployeeData]: + endpoint = f"people/{person_id}/employee" + return self.send_request("GET", endpoint, EmployeeData) + + def create(self, employee: EmployeeData) -> ApiResponse[EmployeeData]: + endpoint = f"people/{employee.employee_person_id}/employee" + return self.send_request("POST", endpoint, EmployeeData, payload=employee) + + def update(self, employee: EmployeeData) -> ApiResponse[EmployeeData]: + endpoint = f"people/{employee.employee_person_id}/employee" + return self.send_request("PUT", endpoint, EmployeeData, payload=employee) + + def delete(self, person_id: str) -> ApiResponse[bool]: + endpoint = f"people/{person_id}/employee" + return self.send_request("DELETE", endpoint, bool) diff --git a/fiscalapi/services/employer_service.py b/fiscalapi/services/employer_service.py new file mode 100644 index 0000000..716aef2 --- /dev/null +++ b/fiscalapi/services/employer_service.py @@ -0,0 +1,23 @@ +from fiscalapi.models.common_models import ApiResponse +from fiscalapi.models.fiscalapi_models import EmployerData +from fiscalapi.services.base_service import BaseService + + +class EmployerService(BaseService): + """Servicio para gestionar empleadores (sub-recurso de personas).""" + + def get_by_id(self, person_id: str) -> ApiResponse[EmployerData]: + endpoint = f"people/{person_id}/employer" + return self.send_request("GET", endpoint, EmployerData) + + def create(self, employer: EmployerData) -> ApiResponse[EmployerData]: + endpoint = f"people/{employer.person_id}/employer" + return self.send_request("POST", endpoint, EmployerData, payload=employer) + + def update(self, employer: EmployerData) -> ApiResponse[EmployerData]: + endpoint = f"people/{employer.person_id}/employer" + return self.send_request("PUT", endpoint, EmployerData, payload=employer) + + def delete(self, person_id: str) -> ApiResponse[bool]: + endpoint = f"people/{person_id}/employer" + return self.send_request("DELETE", endpoint, bool) diff --git a/fiscalapi/services/fiscalapi_client.py b/fiscalapi/services/fiscalapi_client.py index 24f7ea7..faba9e4 100644 --- a/fiscalapi/services/fiscalapi_client.py +++ b/fiscalapi/services/fiscalapi_client.py @@ -3,7 +3,7 @@ from fiscalapi.services.invoice_service import InvoiceService from fiscalapi.services.people_service import PeopleService from fiscalapi.services.product_service import ProductService -from fiscalapi.services.tax_file_servive import TaxFileService +from fiscalapi.services.tax_file_service 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 diff --git a/fiscalapi/services/people_service.py b/fiscalapi/services/people_service.py index 6eb70d9..9f14d45 100644 --- a/fiscalapi/services/people_service.py +++ b/fiscalapi/services/people_service.py @@ -1,10 +1,31 @@ -from fiscalapi.models.common_models import ApiResponse, PagedList -from fiscalapi.models.fiscalapi_models import Person, Product +from typing import Optional + +from fiscalapi.models.common_models import ApiResponse, FiscalApiSettings, PagedList +from fiscalapi.models.fiscalapi_models import Person from fiscalapi.services.base_service import BaseService +from fiscalapi.services.employee_service import EmployeeService +from fiscalapi.services.employer_service import EmployerService class PeopleService(BaseService): - + + def __init__(self, settings: FiscalApiSettings): + super().__init__(settings) + self._employee: Optional[EmployeeService] = None + self._employer: Optional[EmployerService] = None + + @property + def employee(self) -> EmployeeService: + if self._employee is None: + self._employee = EmployeeService(self.settings) + return self._employee + + @property + def employer(self) -> EmployerService: + if self._employer is None: + self._employer = EmployerService(self.settings) + return self._employer + # get paged list of people def get_list(self, page_number: int, page_size: int) -> ApiResponse[PagedList[Person]]: endpoint = f"people?pageNumber={page_number}&pageSize={page_size}" diff --git a/fiscalapi/services/tax_file_service.py b/fiscalapi/services/tax_file_service.py new file mode 100644 index 0000000..5269f03 --- /dev/null +++ b/fiscalapi/services/tax_file_service.py @@ -0,0 +1,32 @@ +from fiscalapi.models.common_models import ApiResponse, PagedList +from fiscalapi.models.fiscalapi_models import TaxFile +from fiscalapi.services.base_service import BaseService + + +class TaxFileService(BaseService): + + def get_list(self, page_number: int, page_size: int) -> ApiResponse[PagedList[TaxFile]]: + endpoint = f"tax-files?pageNumber={page_number}&pageSize={page_size}" + return self.send_request("GET", endpoint, PagedList[TaxFile]) + + def get_by_id(self, tax_file_id: str) -> ApiResponse[TaxFile]: + endpoint = f"tax-files/{tax_file_id}" + return self.send_request("GET", endpoint, TaxFile) + + def create(self, tax_file: TaxFile) -> ApiResponse[TaxFile]: + endpoint = "tax-files" + return self.send_request("POST", endpoint, TaxFile, payload=tax_file) + + def delete(self, tax_file_id: str) -> ApiResponse[bool]: + endpoint = f"tax-files/{tax_file_id}" + return self.send_request("DELETE", endpoint, bool) + + def get_default_values(self, person_id: str) -> ApiResponse[list[TaxFile]]: + """Obtiene el último par de certificados válidos y vigentes de una persona.""" + endpoint = f"tax-files/{person_id}/default-values" + return self.send_request("GET", endpoint, list[TaxFile]) + + def get_default_references(self, person_id: str) -> ApiResponse[list[TaxFile]]: + """Obtiene el último par de IDs de certificados válidos y vigentes de una persona.""" + endpoint = f"tax-files/{person_id}/default-references" + return self.send_request("GET", endpoint, list[TaxFile]) diff --git a/fiscalapi/services/tax_file_servive.py b/fiscalapi/services/tax_file_servive.py deleted file mode 100644 index 68ab13b..0000000 --- a/fiscalapi/services/tax_file_servive.py +++ /dev/null @@ -1,41 +0,0 @@ -from fiscalapi.models.common_models import ApiResponse, PagedList -from fiscalapi.models.fiscalapi_models import TaxFile -from fiscalapi.services.base_service import BaseService - -class TaxFileService(BaseService): - - # get paged list of tax files - def get_list(self, page_number: int, page_size: int) -> ApiResponse[PagedList[TaxFile]]: - endpoint = f"tax-files?pageNumber={page_number}&pageSize={page_size}" - return self.send_request("GET", endpoint, PagedList[TaxFile]) - - # get tax file by id - def get_by_id(self, tax_file_id: str) -> ApiResponse[TaxFile]: - endpoint = f"tax-files/{tax_file_id}" - return self.send_request("GET", endpoint, TaxFile) - - - # create tax file (upload tax file) - def create(self, tax_file: TaxFile) -> ApiResponse[TaxFile]: - endpoint = "tax-files" - return self.send_request("POST", endpoint, TaxFile, payload=tax_file) - - - # delete tax file - def delete(self, tax_file_id: str) -> ApiResponse[bool]: - endpoint = f"tax-files/{tax_file_id}" - return self.send_request("DELETE", endpoint, bool) - - # get default tax files for a given person) - # obtiene el último par de certificados válidos y vigente de una persona. Es decir sus certificados por defecto. - def get_default_values(self, person_id: str) -> ApiResponse[list[TaxFile]]: - endpoint = f"tax-files/{person_id}/default-values" - return self.send_request("GET", endpoint, list[TaxFile]) - - - # get default references for a given person - # obtiene el último par de ids de certificados válidos y vigente de una persona. Es decir sus certificados por defecto (solo los ids) - def get_default_references(self, person_id: str) -> ApiResponse[list[TaxFile]]: - endpoint = f"tax-files/{person_id}/default-references" - return self.send_request("GET", endpoint, list[TaxFile]) - \ No newline at end of file diff --git a/setup.py b/setup.py index 53ad351..e7b160d 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,10 @@ packages=find_packages( exclude=["tests", "*.tests", "*.tests.*", "tests.*"] ), + package_data={ + "fiscalapi": ["py.typed"], + }, + include_package_data=True, keywords=["factura", "cfdi", "facturacion", "mexico", "sat", "fiscalapi"], python_requires=">=3.9", From 64c0370d1dab0bbe6fab07d82d1e5dd67fb70570 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Tue, 20 Jan 2026 18:35:25 -0600 Subject: [PATCH 14/28] Refactor InvoiceService to use single unified endpoint - Simplify InvoiceService to use single /invoices endpoint - Add complement models for localTaxes, payment, payroll, and lading - Add new invoice sub-models (ItemTax, ItemOnBehalfOf, ItemCustomsInfo, etc.) - Add InvoiceIssuerEmployerData and InvoiceRecipientEmployeeData models - Update InvoiceIssuer, InvoiceRecipient, InvoiceItem, and Invoice models - Invoice type now determined by type_code field (I, E, P, N, T) --- fiscalapi/__init__.py | 169 +++++++++---- fiscalapi/models/__init__.py | 169 +++++++++---- fiscalapi/models/fiscalapi_models.py | 344 ++++++++++++++++++++++---- fiscalapi/services/invoice_service.py | 92 ++++--- 4 files changed, 578 insertions(+), 196 deletions(-) diff --git a/fiscalapi/__init__.py b/fiscalapi/__init__.py index 6b2fa57..4543c4f 100644 --- a/fiscalapi/__init__.py +++ b/fiscalapi/__init__.py @@ -23,46 +23,78 @@ # Modelos de dominio from .models.fiscalapi_models import ( - ApiKey, - CancelInvoiceRequest, - CancelInvoiceResponse, - CreatePdfRequest, - DownloadRequest, - DownloadRule, + # Product models + ProductTax, + Product, + # Person models + Person, EmployeeData, EmployerData, - FileResponse, - GlobalInformation, - Invoice, + TaxFile, + # Invoice issuer/recipient models + InvoiceIssuerEmployerData, + InvoiceRecipientEmployeeData, + TaxCredential, InvoiceIssuer, + InvoiceRecipient, + # Invoice item models + ItemTax, + ItemOnBehalfOf, + ItemCustomsInfo, + ItemPropertyInfo, + ItemPart, InvoiceItem, + # Invoice related models + GlobalInformation, + RelatedInvoice, + PaidInvoiceTax, + PaidInvoice, InvoicePayment, - InvoiceRecipient, + # Complement models + LocalTax, + LocalTaxesComplement, + PaymentComplement, + PayrollStockOptions, + PayrollOvertime, + PayrollEarning, + PayrollBalanceCompensation, + PayrollOtherPayment, + PayrollRetirement, + PayrollSeverance, + PayrollEarningsComplement, + PayrollDeduction, + PayrollDisability, + PayrollComplement, + LadingComplement, + InvoiceComplement, + # Invoice models InvoiceResponse, + Invoice, + CancelInvoiceRequest, + CancelInvoiceResponse, + CreatePdfRequest, + FileResponse, + SendInvoiceRequest, InvoiceStatusRequest, InvoiceStatusResponse, - ItemTax, + # API Key models + ApiKey, + # Download models + DownloadRule, + DownloadRequest, MetadataItem, - PaidInvoice, - PaidInvoiceTax, - Person, - Product, - ProductTax, - RelatedInvoice, - SendInvoiceRequest, - TaxCredential, - TaxFile, - Xml, - XmlComplement, + # XML models XmlGlobalInformation, XmlIssuer, - XmlItem, - XmlItemCustomsInformation, - XmlItemPropertyAccount, - XmlItemTax, XmlRecipient, XmlRelated, XmlTax, + XmlItemCustomsInformation, + XmlItemPropertyAccount, + XmlItemTax, + XmlItem, + XmlComplement, + Xml, ) # Servicios @@ -90,47 +122,78 @@ "FiscalApiSettings", "PagedList", "ValidationFailure", - # Modelos de dominio - "ApiKey", - "CancelInvoiceRequest", - "CancelInvoiceResponse", - "CreatePdfRequest", - "DownloadRequest", - "DownloadRule", + # Product models + "ProductTax", + "Product", + # Person models + "Person", "EmployeeData", "EmployerData", - "FileResponse", - "GlobalInformation", - "Invoice", + "TaxFile", + # Invoice issuer/recipient models + "InvoiceIssuerEmployerData", + "InvoiceRecipientEmployeeData", + "TaxCredential", "InvoiceIssuer", + "InvoiceRecipient", + # Invoice item models + "ItemTax", + "ItemOnBehalfOf", + "ItemCustomsInfo", + "ItemPropertyInfo", + "ItemPart", "InvoiceItem", + # Invoice related models + "GlobalInformation", + "RelatedInvoice", + "PaidInvoiceTax", + "PaidInvoice", "InvoicePayment", - "InvoiceRecipient", + # Complement models + "LocalTax", + "LocalTaxesComplement", + "PaymentComplement", + "PayrollStockOptions", + "PayrollOvertime", + "PayrollEarning", + "PayrollBalanceCompensation", + "PayrollOtherPayment", + "PayrollRetirement", + "PayrollSeverance", + "PayrollEarningsComplement", + "PayrollDeduction", + "PayrollDisability", + "PayrollComplement", + "LadingComplement", + "InvoiceComplement", + # Invoice models "InvoiceResponse", + "Invoice", + "CancelInvoiceRequest", + "CancelInvoiceResponse", + "CreatePdfRequest", + "FileResponse", + "SendInvoiceRequest", "InvoiceStatusRequest", "InvoiceStatusResponse", - "ItemTax", + # API Key models + "ApiKey", + # Download models + "DownloadRule", + "DownloadRequest", "MetadataItem", - "PaidInvoice", - "PaidInvoiceTax", - "Person", - "Product", - "ProductTax", - "RelatedInvoice", - "SendInvoiceRequest", - "TaxCredential", - "TaxFile", - "Xml", - "XmlComplement", + # XML models "XmlGlobalInformation", "XmlIssuer", - "XmlItem", - "XmlItemCustomsInformation", - "XmlItemPropertyAccount", - "XmlItemTax", "XmlRecipient", "XmlRelated", "XmlTax", + "XmlItemCustomsInformation", + "XmlItemPropertyAccount", + "XmlItemTax", + "XmlItem", + "XmlComplement", + "Xml", # Servicios "BaseService", "ApiKeyService", diff --git a/fiscalapi/models/__init__.py b/fiscalapi/models/__init__.py index da402a1..d4ba103 100644 --- a/fiscalapi/models/__init__.py +++ b/fiscalapi/models/__init__.py @@ -9,46 +9,78 @@ ValidationFailure, ) from .fiscalapi_models import ( - ApiKey, - CancelInvoiceRequest, - CancelInvoiceResponse, - CreatePdfRequest, - DownloadRequest, - DownloadRule, + # Product models + ProductTax, + Product, + # Person models + Person, EmployeeData, EmployerData, - FileResponse, - GlobalInformation, - Invoice, + TaxFile, + # Invoice issuer/recipient models + InvoiceIssuerEmployerData, + InvoiceRecipientEmployeeData, + TaxCredential, InvoiceIssuer, + InvoiceRecipient, + # Invoice item models + ItemTax, + ItemOnBehalfOf, + ItemCustomsInfo, + ItemPropertyInfo, + ItemPart, InvoiceItem, + # Invoice related models + GlobalInformation, + RelatedInvoice, + PaidInvoiceTax, + PaidInvoice, InvoicePayment, - InvoiceRecipient, + # Complement models + LocalTax, + LocalTaxesComplement, + PaymentComplement, + PayrollStockOptions, + PayrollOvertime, + PayrollEarning, + PayrollBalanceCompensation, + PayrollOtherPayment, + PayrollRetirement, + PayrollSeverance, + PayrollEarningsComplement, + PayrollDeduction, + PayrollDisability, + PayrollComplement, + LadingComplement, + InvoiceComplement, + # Invoice models InvoiceResponse, + Invoice, + CancelInvoiceRequest, + CancelInvoiceResponse, + CreatePdfRequest, + FileResponse, + SendInvoiceRequest, InvoiceStatusRequest, InvoiceStatusResponse, - ItemTax, + # API Key models + ApiKey, + # Download models + DownloadRule, + DownloadRequest, MetadataItem, - PaidInvoice, - PaidInvoiceTax, - Person, - Product, - ProductTax, - RelatedInvoice, - SendInvoiceRequest, - TaxCredential, - TaxFile, - Xml, - XmlComplement, + # XML models XmlGlobalInformation, XmlIssuer, - XmlItem, - XmlItemCustomsInformation, - XmlItemPropertyAccount, - XmlItemTax, XmlRecipient, XmlRelated, XmlTax, + XmlItemCustomsInformation, + XmlItemPropertyAccount, + XmlItemTax, + XmlItem, + XmlComplement, + Xml, ) __all__ = [ @@ -59,45 +91,76 @@ "FiscalApiSettings", "PagedList", "ValidationFailure", - # fiscalapi_models - "ApiKey", - "CancelInvoiceRequest", - "CancelInvoiceResponse", - "CreatePdfRequest", - "DownloadRequest", - "DownloadRule", + # Product models + "ProductTax", + "Product", + # Person models + "Person", "EmployeeData", "EmployerData", - "FileResponse", - "GlobalInformation", - "Invoice", + "TaxFile", + # Invoice issuer/recipient models + "InvoiceIssuerEmployerData", + "InvoiceRecipientEmployeeData", + "TaxCredential", "InvoiceIssuer", + "InvoiceRecipient", + # Invoice item models + "ItemTax", + "ItemOnBehalfOf", + "ItemCustomsInfo", + "ItemPropertyInfo", + "ItemPart", "InvoiceItem", + # Invoice related models + "GlobalInformation", + "RelatedInvoice", + "PaidInvoiceTax", + "PaidInvoice", "InvoicePayment", - "InvoiceRecipient", + # Complement models + "LocalTax", + "LocalTaxesComplement", + "PaymentComplement", + "PayrollStockOptions", + "PayrollOvertime", + "PayrollEarning", + "PayrollBalanceCompensation", + "PayrollOtherPayment", + "PayrollRetirement", + "PayrollSeverance", + "PayrollEarningsComplement", + "PayrollDeduction", + "PayrollDisability", + "PayrollComplement", + "LadingComplement", + "InvoiceComplement", + # Invoice models "InvoiceResponse", + "Invoice", + "CancelInvoiceRequest", + "CancelInvoiceResponse", + "CreatePdfRequest", + "FileResponse", + "SendInvoiceRequest", "InvoiceStatusRequest", "InvoiceStatusResponse", - "ItemTax", + # API Key models + "ApiKey", + # Download models + "DownloadRule", + "DownloadRequest", "MetadataItem", - "PaidInvoice", - "PaidInvoiceTax", - "Person", - "Product", - "ProductTax", - "RelatedInvoice", - "SendInvoiceRequest", - "TaxCredential", - "TaxFile", - "Xml", - "XmlComplement", + # XML models "XmlGlobalInformation", "XmlIssuer", - "XmlItem", - "XmlItemCustomsInformation", - "XmlItemPropertyAccount", - "XmlItemTax", "XmlRecipient", "XmlRelated", "XmlTax", + "XmlItemCustomsInformation", + "XmlItemPropertyAccount", + "XmlItemTax", + "XmlItem", + "XmlComplement", + "Xml", ] diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index 1bb1ddd..82daf22 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -148,6 +148,43 @@ class TaxFile(BaseDto): # invoices models +# ===== Issuer/Recipient sub-models for Invoice ===== + +class InvoiceIssuerEmployerData(BaseDto): + """Datos del empleador para el emisor en CFDI de nómina.""" + curp: Optional[str] = Field(default=None, alias="curp") + employer_registration: Optional[str] = Field(default=None, alias="employerRegistration") + origin_employer_tin: Optional[str] = Field(default=None, alias="originEmployerTin") + sat_fund_source_id: Optional[str] = Field(default=None, alias="satFundSourceId") + own_resource_amount: Optional[Decimal] = Field(default=None, alias="ownResourceAmount") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class InvoiceRecipientEmployeeData(BaseDto): + """Datos del empleado para el receptor en CFDI de nómina.""" + curp: Optional[str] = Field(default=None, alias="curp") + social_security_number: Optional[str] = Field(default=None, alias="socialSecurityNumber") + labor_relation_start_date: Optional[datetime] = Field(default=None, alias="laborRelationStartDate") + seniority: Optional[int] = Field(default=None, alias="seniority") + sat_contract_type_id: Optional[str] = Field(default=None, alias="satContractTypeId") + sat_unionized_status_id: Optional[str] = Field(default=None, alias="satUnionizedStatusId") + sat_workday_type_id: Optional[str] = Field(default=None, alias="satWorkdayTypeId") + sat_tax_regime_type_id: Optional[str] = Field(default=None, alias="satTaxRegimeTypeId") + employee_number: Optional[str] = Field(default=None, alias="employeeNumber") + department: Optional[str] = Field(default=None, alias="department") + position: Optional[str] = Field(default=None, alias="position") + sat_job_risk_id: Optional[str] = Field(default=None, alias="satJobRiskId") + sat_payment_periodicity_id: Optional[str] = Field(default=None, alias="satPaymentPeriodicityId") + sat_bank_id: Optional[str] = Field(default=None, alias="satBankId") + bank_account: Optional[str] = Field(default=None, alias="bankAccount") + base_salary_for_contributions: Optional[Decimal] = Field(default=None, alias="baseSalaryForContributions") + integrated_daily_salary: Optional[Decimal] = Field(default=None, alias="integratedDailySalary") + sat_payroll_state_id: Optional[str] = Field(default=None, alias="satPayrollStateId") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + class TaxCredential(BaseDto): """Modelo para los sellos del emisor (archivos .cer y .key).""" base64_file: str = Field(default=..., alias="base64File", description="Archivo en formato base64.") @@ -163,6 +200,7 @@ class InvoiceIssuer(BaseDto): tin: Optional[str] = Field(default=None, alias="tin", description="RFC del emisor (Tax Identification Number).") legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del emisor sin regimen de capital.") tax_regime_code: Optional[str] = Field(default=None, alias="taxRegimeCode", description="Código del régimen fiscal del emisor.") + employer_data: Optional[InvoiceIssuerEmployerData] = Field(default=None, alias="employerData", description="Datos del empleador para CFDI de nómina.") tax_credentials: Optional[list[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor (archivos .cer y .key).") model_config = ConfigDict(populate_by_name=True) @@ -173,44 +211,84 @@ class InvoiceRecipient(BaseDto): id: Optional[str] = Field(default=None, alias="id", description="ID de la persona (receptora) en fiscalapi.") tin: Optional[str] = Field(default=None, alias="tin", description="RFC del receptor (Tax Identification Number).") legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del receptor sin regimen de capital.") + zip_code: Optional[str] = Field(default=None, alias="zipCode", description="Código postal del receptor.") tax_regime_code: Optional[str] = Field(default=None, alias="taxRegimeCode", description="Código del régimen fiscal del receptor.") cfdi_use_code: Optional[str] = Field(default=None, alias="cfdiUseCode", description="Código del uso CFDI.") - zip_code: Optional[str] = Field(default=None, alias="zipCode", description="Código postal del receptor. Debe coincidir con el código postal de su constancia de residencia fiscal.") email: Optional[str] = Field(default=None, description="Correo electrónico del receptor.") + foreign_country_code: Optional[str] = Field(default=None, alias="foreignCountryCode", description="Código del país de residencia para extranjeros.") + foreign_tin: Optional[str] = Field(default=None, alias="foreignTin", description="Número de identificación fiscal del extranjero.") + employee_data: Optional[InvoiceRecipientEmployeeData] = Field(default=None, alias="employeeData", description="Datos del empleado para CFDI de nómina.") model_config = ConfigDict(populate_by_name=True) +# ===== InvoiceItem sub-models ===== + class ItemTax(BaseDto): """Modelo para los impuestos aplicables a un producto o servicio.""" - tax_code: str = Field(default=..., alias="taxCode", description="Código del impuesto.") - tax_type_code: str = Field(default=..., alias="taxTypeCode", description="Tipo de factor.") - tax_rate: Decimal = Field(default=..., alias="taxRate", description="Tasa del impuesto.") + tax_code: Optional[str] = Field(default=None, alias="taxCode", description="Código del impuesto.") + tax_type_code: Optional[str] = Field(default=None, alias="taxTypeCode", description="Tipo de factor.") + tax_rate: Optional[Decimal] = Field(default=None, alias="taxRate", description="Tasa del impuesto.") tax_flag_code: Optional[Literal["T", "R"]] = Field(default=None, alias="taxFlagCode", description="Código que indica la naturaleza del impuesto. (T)raslado o (R)etención.") - - model_config = ConfigDict( - populate_by_name=True, - json_encoders={Decimal: str} - ) + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class ItemOnBehalfOf(BaseDto): + """Modelo para la información de cuenta de terceros en un concepto.""" + tin: Optional[str] = Field(default=None, alias="tin", description="RFC del tercero.") + legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del tercero.") + tax_regime_code: Optional[str] = Field(default=None, alias="taxRegimeCode", description="Régimen fiscal del tercero.") + zip_code: Optional[str] = Field(default=None, alias="zipCode", description="Código postal del tercero.") + + model_config = ConfigDict(populate_by_name=True) + + +class ItemCustomsInfo(BaseDto): + """Modelo para la información aduanera de un concepto.""" + customs_number: Optional[str] = Field(default=None, alias="customsNumber", description="Número de pedimento aduanero.") + + model_config = ConfigDict(populate_by_name=True) + + +class ItemPropertyInfo(BaseDto): + """Modelo para la información predial de un concepto.""" + number: Optional[str] = Field(default=None, alias="number", description="Número de cuenta predial.") + + model_config = ConfigDict(populate_by_name=True) + + +class ItemPart(BaseDto): + """Modelo para las partes de un concepto.""" + item_code: Optional[str] = Field(default=None, alias="itemCode", description="Código SAT del producto o servicio.") + item_sku: Optional[str] = Field(default=None, alias="itemSku", description="SKU o clave del sistema externo.") + quantity: Optional[Decimal] = Field(default=None, alias="quantity", description="Cantidad.") + unit_of_measurement_code: Optional[str] = Field(default=None, alias="unitOfMeasurementCode", description="Código SAT de la unidad de medida.") + description: Optional[str] = Field(default=None, alias="description", description="Descripción de la parte.") + unit_price: Optional[Decimal] = Field(default=None, alias="unitPrice", description="Precio unitario.") + customs_info: Optional[list["ItemCustomsInfo"]] = Field(default=None, alias="customsInfo", description="Información aduanera.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) class InvoiceItem(BaseDto): """Modelo para los conceptos de la factura (productos o servicios).""" id: Optional[str] = Field(default=None, alias="id", description="ID del producto en fiscalapi.") item_code: Optional[str] = Field(default=None, alias="itemCode", description="Código SAT del producto o servicio.") - quantity: Decimal = Field(default=..., alias="quantity", description="Cantidad del producto o servicio.") - discount: Optional[Decimal] = Field(default=None, alias="discount", description="Cantidad monetaria del descuento aplicado.") + item_sku: Optional[str] = Field(default=None, alias="itemSku", description="SKU o clave del sistema externo.") + quantity: Optional[Decimal] = Field(default=None, alias="quantity", description="Cantidad del producto o servicio.") unit_of_measurement_code: Optional[str] = Field(default=None, alias="unitOfMeasurementCode", description="Código SAT de la unidad de medida.") - description: Optional[str] = Field(default=None,alias="description", description="Descripción del producto o servicio.") + description: Optional[str] = Field(default=None, alias="description", description="Descripción del producto o servicio.") unit_price: Optional[Decimal] = Field(default=None, alias="unitPrice", description="Precio unitario del producto o servicio.") + discount: Optional[Decimal] = Field(default=None, alias="discount", description="Cantidad monetaria del descuento aplicado.") tax_object_code: Optional[str] = Field(default=None, alias="taxObjectCode", description="Código SAT de obligaciones de impuesto.") - item_sku: Optional[str] = Field(default=None, alias="itemSku", description="SKU o clave del sistema externo.") item_taxes: Optional[list[ItemTax]] = Field(default=None, alias="itemTaxes", description="Impuestos aplicables al producto o servicio.") + on_behalf_of: Optional[ItemOnBehalfOf] = Field(default=None, alias="onBehalfOf", description="Información de cuenta de terceros.") + customs_info: Optional[list[ItemCustomsInfo]] = Field(default=None, alias="customsInfo", description="Información aduanera.") + property_info: Optional[list[ItemPropertyInfo]] = Field(default=None, alias="propertyInfo", description="Información predial.") + parts: Optional[list[ItemPart]] = Field(default=None, alias="parts", description="Partes del concepto.") - model_config = ConfigDict( - populate_by_name=True, - json_encoders={Decimal: str} - ) + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) class GlobalInformation(BaseDto): """Modelo para la información global de la factura global.""" @@ -286,6 +364,179 @@ class InvoicePayment(BaseDto): +# ===== Complement models ===== + +class LocalTax(BaseDto): + """Modelo para impuestos locales.""" + tax_name: Optional[str] = Field(default=None, alias="taxName", description="Nombre del impuesto local.") + tax_rate: Optional[Decimal] = Field(default=None, alias="taxRate", description="Tasa del impuesto local.") + tax_amount: Optional[Decimal] = Field(default=None, alias="taxAmount", description="Monto del impuesto local.") + tax_flag_code: Optional[Literal["T", "R"]] = Field(default=None, alias="taxFlagCode", description="Traslado o Retención.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class LocalTaxesComplement(BaseDto): + """Modelo para el complemento de impuestos locales.""" + taxes: Optional[list[LocalTax]] = Field(default=None, alias="taxes", description="Lista de impuestos locales.") + + model_config = ConfigDict(populate_by_name=True) + + +class PaymentComplement(BaseDto): + """Modelo para el complemento de pago.""" + payment_date: Optional[str] = Field(default=None, alias="paymentDate", description="Fecha de pago.") + payment_form_code: Optional[str] = Field(default=None, alias="paymentFormCode", description="Código de la forma de pago.") + currency_code: Optional[str] = Field(default=None, alias="currencyCode", description="Código de la moneda.") + exchange_rate: Optional[Decimal] = Field(default=None, alias="exchangeRate", description="Tipo de cambio.") + amount: Optional[Decimal] = Field(default=None, alias="amount", description="Monto del pago.") + operation_number: Optional[str] = Field(default=None, alias="operationNumber", description="Número de operación.") + source_bank_tin: Optional[str] = Field(default=None, alias="sourceBankTin", description="RFC del banco origen.") + source_bank_account: Optional[str] = Field(default=None, alias="sourceBankAccount", description="Cuenta bancaria origen.") + target_bank_tin: Optional[str] = Field(default=None, alias="targetBankTin", description="RFC del banco destino.") + target_bank_account: Optional[str] = Field(default=None, alias="targetBankAccount", description="Cuenta bancaria destino.") + paid_invoices: Optional[list["PaidInvoice"]] = Field(default=None, alias="paidInvoices", description="Facturas pagadas.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +# ----- Payroll sub-models ----- + +class PayrollStockOptions(BaseDto): + """Opciones de acciones para percepciones de nómina.""" + market_price: Optional[Decimal] = Field(default=None, alias="marketPrice", description="Valor de mercado.") + grant_price: Optional[Decimal] = Field(default=None, alias="grantPrice", description="Precio de ejercicio.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollOvertime(BaseDto): + """Horas extra para percepciones de nómina.""" + days: Optional[int] = Field(default=None, alias="days", description="Días de horas extra.") + hours_type_code: Optional[str] = Field(default=None, alias="hoursTypeCode", description="Tipo de horas.") + extra_hours: Optional[int] = Field(default=None, alias="extraHours", description="Cantidad de horas extra.") + amount_paid: Optional[Decimal] = Field(default=None, alias="amountPaid", description="Monto pagado.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollEarning(BaseDto): + """Percepción de nómina.""" + earning_type_code: Optional[str] = Field(default=None, alias="earningTypeCode", description="Tipo de percepción.") + code: Optional[str] = Field(default=None, alias="code", description="Código de la percepción.") + concept: Optional[str] = Field(default=None, alias="concept", description="Concepto de la percepción.") + taxed_amount: Optional[Decimal] = Field(default=None, alias="taxedAmount", description="Monto gravado.") + exempt_amount: Optional[Decimal] = Field(default=None, alias="exemptAmount", description="Monto exento.") + stock_options: Optional[PayrollStockOptions] = Field(default=None, alias="stockOptions", description="Opciones de acciones.") + overtime: Optional[list[PayrollOvertime]] = Field(default=None, alias="overtime", description="Horas extra.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollBalanceCompensation(BaseDto): + """Compensación de saldos a favor.""" + favorable_balance: Optional[Decimal] = Field(default=None, alias="favorableBalance", description="Saldo a favor.") + year: Optional[int] = Field(default=None, alias="year", description="Año del saldo.") + remaining_favorable_balance: Optional[Decimal] = Field(default=None, alias="remainingFavorableBalance", description="Remanente del saldo.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollOtherPayment(BaseDto): + """Otros pagos de nómina.""" + other_payment_type_code: Optional[str] = Field(default=None, alias="otherPaymentTypeCode", description="Tipo de otro pago.") + code: Optional[str] = Field(default=None, alias="code", description="Código del pago.") + concept: Optional[str] = Field(default=None, alias="concept", description="Concepto del pago.") + amount: Optional[Decimal] = Field(default=None, alias="amount", description="Monto del pago.") + subsidy_caused: Optional[Decimal] = Field(default=None, alias="subsidyCaused", description="Subsidio causado.") + balance_compensation: Optional[PayrollBalanceCompensation] = Field(default=None, alias="balanceCompensation", description="Compensación de saldos.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollRetirement(BaseDto): + """Jubilación, pensión o retiro.""" + total_one_time: Optional[Decimal] = Field(default=None, alias="totalOneTime", description="Total por pago único.") + total_installments: Optional[Decimal] = Field(default=None, alias="totalInstallments", description="Total parcialidades.") + daily_amount: Optional[Decimal] = Field(default=None, alias="dailyAmount", description="Monto diario.") + accumulable_income: Optional[Decimal] = Field(default=None, alias="accumulableIncome", description="Ingreso acumulable.") + non_accumulable_income: Optional[Decimal] = Field(default=None, alias="nonAccumulableIncome", description="Ingreso no acumulable.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollSeverance(BaseDto): + """Separación o indemnización.""" + total_paid: Optional[Decimal] = Field(default=None, alias="totalPaid", description="Total pagado.") + years_of_service: Optional[int] = Field(default=None, alias="yearsOfService", description="Años de servicio.") + last_monthly_salary: Optional[Decimal] = Field(default=None, alias="lastMonthlySalary", description="Último sueldo mensual.") + accumulable_income: Optional[Decimal] = Field(default=None, alias="accumulableIncome", description="Ingreso acumulable.") + non_accumulable_income: Optional[Decimal] = Field(default=None, alias="nonAccumulableIncome", description="Ingreso no acumulable.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollEarningsComplement(BaseDto): + """Contenedor de percepciones de nómina.""" + earnings: Optional[list[PayrollEarning]] = Field(default=None, alias="earnings", description="Percepciones.") + other_payments: Optional[list[PayrollOtherPayment]] = Field(default=None, alias="otherPayments", description="Otros pagos.") + retirement: Optional[PayrollRetirement] = Field(default=None, alias="retirement", description="Jubilación.") + severance: Optional[PayrollSeverance] = Field(default=None, alias="severance", description="Separación.") + + model_config = ConfigDict(populate_by_name=True) + + +class PayrollDeduction(BaseDto): + """Deducción de nómina.""" + deduction_type_code: Optional[str] = Field(default=None, alias="deductionTypeCode", description="Tipo de deducción.") + code: Optional[str] = Field(default=None, alias="code", description="Código de la deducción.") + concept: Optional[str] = Field(default=None, alias="concept", description="Concepto de la deducción.") + amount: Optional[Decimal] = Field(default=None, alias="amount", description="Monto de la deducción.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollDisability(BaseDto): + """Incapacidad de nómina.""" + disability_days: Optional[int] = Field(default=None, alias="disabilityDays", description="Días de incapacidad.") + disability_type_code: Optional[str] = Field(default=None, alias="disabilityTypeCode", description="Tipo de incapacidad.") + monetary_amount: Optional[Decimal] = Field(default=None, alias="monetaryAmount", description="Monto monetario.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class PayrollComplement(BaseDto): + """Modelo para el complemento de nómina.""" + version: Optional[str] = Field(default=None, alias="version", description="Versión del complemento de nómina.") + payroll_type_code: Optional[str] = Field(default=None, alias="payrollTypeCode", description="Tipo de nómina.") + payment_date: Optional[str] = Field(default=None, alias="paymentDate", description="Fecha de pago.") + initial_payment_date: Optional[str] = Field(default=None, alias="initialPaymentDate", description="Fecha inicial de pago.") + final_payment_date: Optional[str] = Field(default=None, alias="finalPaymentDate", description="Fecha final de pago.") + days_paid: Optional[Decimal] = Field(default=None, alias="daysPaid", description="Días pagados.") + earnings: Optional[PayrollEarningsComplement] = Field(default=None, alias="earnings", description="Percepciones.") + deductions: Optional[list[PayrollDeduction]] = Field(default=None, alias="deductions", description="Deducciones.") + disabilities: Optional[list[PayrollDisability]] = Field(default=None, alias="disabilities", description="Incapacidades.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) + + +class LadingComplement(BaseDto): + """Modelo para el complemento de carta porte.""" + # Placeholder for carta porte fields - to be expanded as needed + + model_config = ConfigDict(populate_by_name=True) + + +class InvoiceComplement(BaseDto): + """Modelo contenedor de complementos de factura.""" + local_taxes: Optional[LocalTaxesComplement] = Field(default=None, alias="localTaxes", description="Complemento de impuestos locales.") + payment: Optional[PaymentComplement] = Field(default=None, alias="payment", description="Complemento de pago.") + payroll: Optional[PayrollComplement] = Field(default=None, alias="payroll", description="Complemento de nómina.") + lading: Optional[LadingComplement] = Field(default=None, alias="lading", description="Complemento de carta porte.") + + model_config = ConfigDict(populate_by_name=True) + + class InvoiceResponse(BaseDto): """Modelo para la respuesta del SAT después del timbrado de la factura.""" id: Optional[str] = Field(default=None, description="ID de la respuesta.") @@ -308,36 +559,43 @@ class InvoiceResponse(BaseDto): class Invoice(BaseDto): """Modelo para la factura.""" - version_code: Optional[str] = Field(default="4.0", alias="versionCode", description="Código de la versión de la factura.") - consecutive: Optional[int] = Field(default=None, description="Consecutivo de facturas por cuenta. Se incrementa con cada factura generada en tu cuenta independientemente del RFC emisor.") - number: Optional[str] = Field(default=None, description="Consecutivo de facturas por RFC emisor. Se incrementa por cada factura generada por el mismo RFC emisor.") - subtotal: Optional[Decimal] = Field(default=None, description="Subtotal de la factura. Generado automáticamente por Fiscalapi.") - discount: Optional[Decimal] = Field(default=None, description="Descuento aplicado a la factura. Generado automáticamente por Fiscalapi a partir de los descuentos aplicados a los productos o servicios.") - total: Optional[Decimal] = Field(default=None, description="Total de la factura. Generado automáticamente por Fiscalapi.") - uuid: Optional[str] = Field(default=None, description="UUID de la factura, es el folio fiscal asignado por el SAT al momento del timbrado.") - status: Optional[CatalogDto] = Field(default=None, description="El estatus de la factura") - series: str = Field(default=..., description="Número de serie que utiliza el contribuyente para control interno.") - date: datetime = Field(default=..., description="Fecha y hora de expedición del comprobante fiscal.") + # Request fields (input) + version_code: Optional[str] = Field(default=None, alias="versionCode", description="Código de la versión de la factura.") payment_form_code: Optional[str] = Field(default=None, alias="paymentFormCode", description="Código de la forma de pago.") - currency_code: Literal["MXN", "USD", "EUR", "XXX"] = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada.") - type_code: Optional[Literal["I", "E", "T", "N", "P"]] = Field(default="I", alias="typeCode", description="Código de tipo de factura.") - expedition_zip_code: str = Field(default=..., alias="expeditionZipCode", description="Código postal del emisor.") - export_code: Optional[Literal["01", "02", "03", "04"]] = Field(default="01", alias="exportCode", description="Código que identifica si la factura ampara una operación de exportación.") - payment_method_code: Optional[Literal["PUE", "PPD"]] = Field(default=None, alias="paymentMethodCode", description="Código de método para la factura de pago.") - exchange_rate: Optional[Decimal] = Field(default=Decimal("1"), alias="exchangeRate", description="Tipo de cambio FIX.") - issuer: InvoiceIssuer = Field(default=..., description="El emisor de la factura.") - recipient: InvoiceRecipient = Field(default=..., description="El receptor de la factura.") - items: Optional[list[InvoiceItem]] = Field(default_factory=list, description="Conceptos de la factura (productos o servicios).") + payment_method_code: Optional[str] = Field(default=None, alias="paymentMethodCode", description="Código de método de pago (PUE, PPD).") + currency_code: Optional[str] = Field(default=None, alias="currencyCode", description="Código de la moneda utilizada.") + type_code: Optional[str] = Field(default=None, alias="typeCode", description="Código de tipo de factura (I, E, T, N, P).") + expedition_zip_code: Optional[str] = Field(default=None, alias="expeditionZipCode", description="Código postal del lugar de expedición.") + pac_confirmation: Optional[str] = Field(default=None, alias="pacConfirmation", description="Confirmación del PAC.") + series: Optional[str] = Field(default=None, alias="series", description="Serie de la factura.") + number: Optional[str] = Field(default=None, alias="number", description="Folio de la factura.") + date: Optional[datetime] = Field(default=None, alias="date", description="Fecha y hora de expedición.") + payment_conditions: Optional[str] = Field(default=None, alias="paymentConditions", description="Condiciones de pago.") + exchange_rate: Optional[Decimal] = Field(default=None, alias="exchangeRate", description="Tipo de cambio FIX.") + export_code: Optional[str] = Field(default=None, alias="exportCode", description="Código de exportación.") + + # Main components + issuer: Optional[InvoiceIssuer] = Field(default=None, alias="issuer", description="El emisor de la factura.") + recipient: Optional[InvoiceRecipient] = Field(default=None, alias="recipient", description="El receptor de la factura.") + items: Optional[list[InvoiceItem]] = Field(default=None, alias="items", description="Conceptos de la factura.") global_information: Optional[GlobalInformation] = Field(default=None, alias="globalInformation", description="Información global de la factura.") related_invoices: Optional[list[RelatedInvoice]] = Field(default=None, alias="relatedInvoices", description="Facturas relacionadas.") - payments: Optional[list[InvoicePayment]] = Field(default=None, description="Pago o pagos recibidos para liquidar la factura cuando la factura es un complemento de pago.") - responses: Optional[list[InvoiceResponse]] = Field(default=None, description="Respuestas del SAT. Contiene la información de timbrado de la factura.") - + complement: Optional[InvoiceComplement] = Field(default=None, alias="complement", description="Complementos de la factura.") + metadata: Optional[dict] = Field(default=None, alias="metadata", description="Metadatos adicionales.") - model_config = ConfigDict( - populate_by_name=True, - json_encoders={Decimal: str} - ) + # Response fields (output - populated by API) + consecutive: Optional[int] = Field(default=None, alias="consecutive", description="Consecutivo de facturas por cuenta.") + subtotal: Optional[Decimal] = Field(default=None, alias="subtotal", description="Subtotal de la factura.") + discount: Optional[Decimal] = Field(default=None, alias="discount", description="Descuento aplicado.") + total: Optional[Decimal] = Field(default=None, alias="total", description="Total de la factura.") + uuid: Optional[str] = Field(default=None, alias="uuid", description="UUID de la factura (folio fiscal).") + status: Optional[CatalogDto] = Field(default=None, alias="status", description="Estatus de la factura.") + responses: Optional[list[InvoiceResponse]] = Field(default=None, alias="responses", description="Respuestas del SAT.") + + # Legacy field for backward compatibility + payments: Optional[list[InvoicePayment]] = Field(default=None, alias="payments", description="[Deprecado] Use complement.payment en su lugar.") + + model_config = ConfigDict(populate_by_name=True, json_encoders={Decimal: str}) diff --git a/fiscalapi/services/invoice_service.py b/fiscalapi/services/invoice_service.py index 702bfd2..860ae40 100644 --- a/fiscalapi/services/invoice_service.py +++ b/fiscalapi/services/invoice_service.py @@ -1,73 +1,71 @@ from fiscalapi.models.common_models import ApiResponse, PagedList -from fiscalapi.models.fiscalapi_models import CancelInvoiceRequest, CancelInvoiceResponse, CreatePdfRequest, FileResponse, Invoice, InvoiceStatusRequest, InvoiceStatusResponse, SendInvoiceRequest +from fiscalapi.models.fiscalapi_models import ( + CancelInvoiceRequest, + CancelInvoiceResponse, + CreatePdfRequest, + FileResponse, + Invoice, + InvoiceStatusRequest, + InvoiceStatusResponse, + SendInvoiceRequest, +) from fiscalapi.services.base_service import BaseService class InvoiceService(BaseService): - - # get paged list of invoices + """Servicio para gestionar facturas CFDI.""" + def get_list(self, page_number: int, page_size: int) -> ApiResponse[PagedList[Invoice]]: + """Obtiene una lista paginada de facturas.""" endpoint = f"invoices?pageNumber={page_number}&pageSize={page_size}" return self.send_request("GET", endpoint, PagedList[Invoice]) - - # get invoice by id - def get_by_id(self, invoice_id: int, details: bool = False) -> ApiResponse[Invoice]: + + def get_by_id(self, invoice_id: str, details: bool = False) -> ApiResponse[Invoice]: + """Obtiene una factura por su ID.""" endpoint = f"invoices/{invoice_id}" return self.send_request("GET", endpoint, Invoice, details=details) - - - # helper method to determine the endpoint based on invoice type - def _get_endpoint_by_type(self, type_code: str) -> str: - if type_code == "I": - return "invoices/income" - elif type_code == "E": - return "invoices/credit-note" - elif type_code == "P": - return "invoices/payment" - else: - raise ValueError(f"Unsupported invoice type: {type_code}") - - # create invoice + def create(self, invoice: Invoice) -> ApiResponse[Invoice]: - if invoice.type_code is None: - raise ValueError("Invoice type_code is required") - endpoint = self._get_endpoint_by_type(invoice.type_code) + """ + Crea una nueva factura CFDI. + + El tipo de factura se determina por el campo type_code del modelo Invoice: + - 'I': Factura de ingreso + - 'E': Nota de crédito + - 'P': Complemento de pago + - 'N': Nómina + - 'T': Traslado + + Args: + invoice: Modelo de factura con los datos requeridos. + + Returns: + ApiResponse con la factura creada y timbrada. + """ + endpoint = "invoices" return self.send_request("POST", endpoint, Invoice, payload=invoice) - - - # cancel invoice + def cancel(self, cancel_invoice_request: CancelInvoiceRequest) -> ApiResponse[CancelInvoiceResponse]: + """Cancela una factura.""" endpoint = "invoices" return self.send_request("DELETE", endpoint, CancelInvoiceResponse, payload=cancel_invoice_request) - - # create invoice's pdf + def get_pdf(self, create_pdf_request: CreatePdfRequest) -> ApiResponse[FileResponse]: + """Genera el PDF de una factura.""" endpoint = "invoices/pdf" return self.send_request("POST", endpoint, FileResponse, payload=create_pdf_request) - - # get invoice's xml by id /api/v4/invoices//xml - def get_xml(self, invoice_id: int) -> ApiResponse[FileResponse]: + + def get_xml(self, invoice_id: str) -> ApiResponse[FileResponse]: + """Obtiene el XML de una factura por su ID.""" endpoint = f"invoices/{invoice_id}/xml" return self.send_request("GET", endpoint, FileResponse) - - - # send invoice by email + def send(self, send_invoice_request: SendInvoiceRequest) -> ApiResponse[bool]: + """Envía una factura por correo electrónico.""" endpoint = "invoices/send" return self.send_request("POST", endpoint, bool, payload=send_invoice_request) - - # consultar estado de facturas - def get_status(self, request: InvoiceStatusRequest) -> ApiResponse[InvoiceStatusResponse]: - """ - Obtiene el estado de una factura. - Args: - request (InvoiceStatusRequest): Solicitud para consultar estado - - Returns: - ApiResponse[InvoiceStatusResponse]: Respuesta con el estado de la factura - """ + def get_status(self, request: InvoiceStatusRequest) -> ApiResponse[InvoiceStatusResponse]: + """Consulta el estado de una factura en el SAT.""" endpoint = "invoices/status" return self.send_request("POST", endpoint, InvoiceStatusResponse, payload=request) - - \ No newline at end of file From 92f0dc5d0e7d4de7de3518263e9cd1800e840f9e Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Tue, 20 Jan 2026 18:46:37 -0600 Subject: [PATCH 15/28] Add payroll invoice example and fix field types - Add "Facturas de Nomina" section in examples.py with complete payroll invoice example - Fix seniority field type from int to str (ISO 8601 duration format) - Fix labor_relation_start_date field type from datetime to str --- examples.py | 183 ++++++++++++++++++++++++++- fiscalapi/models/fiscalapi_models.py | 4 +- 2 files changed, 183 insertions(+), 4 deletions(-) diff --git a/examples.py b/examples.py index 89da1fd..ccdf095 100644 --- a/examples.py +++ b/examples.py @@ -16,6 +16,8 @@ def main (): + base64_cer = "MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" + base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS/AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucoZQObOaLUEm+I+QZ7Y8Giupo+F1XWkLvAsdk/uZlJcTfKLJyJbJwsQYbSpLOCLataZ4O5MVnnmMbfG//NKJn9kSMvJQZhSwAwoGLYDm1ESGezrvZabgFJnoQv8Si1nAhVGTk9FkFBesxRzq07dmZYwFCnFSX4xt2fDHs1PMpQbeq83aL/PzLCce3kxbYSB5kQlzGtUYayiYXcu0cVRu228VwBLCD+2wTDDoCmRXtPesgrLKUR4WWWb5N2AqAU1mNDC+UEYsENAerOFXWnmwrcTAu5qyZ7GsBMTpipW4Dbou2yqQ0lpA/aB06n1kz1aL6mNqGPaJ+OqoFuc8Ugdhadd+MmjHfFzoI20SZ3b2geCsUMNCsAd6oXMsZdWm8lzjqCGWHFeol0ik/xHMQvuQkkeCsQ28PBxdnUgf7ZGer+TN+2ZLd2kvTBOk6pIVgy5yC6cZ+o1Tloql9hYGa6rT3xcMbXlW+9e5jM2MWXZliVW3ZhaPjptJFDbIfWxJPjz4QvKyJk0zok4muv13Iiwj2bCyefUTRz6psqI4cGaYm9JpscKO2RCJN8UluYGbbWmYQU+Int6LtZj/lv8p6xnVjWxYI+rBPdtkpfFYRp+MJiXjgPw5B6UGuoruv7+vHjOLHOotRo+RdjZt7NqL9dAJnl1Qb2jfW6+d7NYQSI/bAwxO0sk4taQIT6Gsu/8kfZOPC2xk9rphGqCSS/4q3Os0MMjA1bcJLyoWLp13pqhK6bmiiHw0BBXH4fbEp4xjSbpPx4tHXzbdn8oDsHKZkWh3pPC2J/nVl0k/yF1KDVowVtMDXE47k6TGVcBoqe8PDXCG9+vjRpzIidqNo5qebaUZu6riWMWzldz8x3Z/jLWXuDiM7/Yscn0Z2GIlfoeyz+GwP2eTdOw9EUedHjEQuJY32bq8LICimJ4Ht+zMJKUyhwVQyAER8byzQBwTYmYP5U0wdsyIFitphw+/IH8+v08Ia1iBLPQAeAvRfTTIFLCs8foyUrj5Zv2B/wTYIZy6ioUM+qADeXyo45uBLLqkN90Rf6kiTqDld78NxwsfyR5MxtJLVDFkmf2IMMJHTqSfhbi+7QJaC11OOUJTD0v9wo0X/oO5GvZhe0ZaGHnm9zqTopALuFEAxcaQlc4R81wjC4wrIrqWnbcl2dxiBtD73KW+wcC9ymsLf4I8BEmiN25lx/OUc1IHNyXZJYSFkEfaxCEZWKcnbiyf5sqFSSlEqZLc4lUPJFAoP6s1FHVcyO0odWqdadhRZLZC9RCzQgPlMRtji/OXy5phh7diOBZv5UYp5nb+MZ2NAB/eFXm2JLguxjvEstuvTDmZDUb6Uqv++RdhO5gvKf/AcwU38ifaHQ9uvRuDocYwVxZS2nr9rOwZ8nAh+P2o4e0tEXjxFKQGhxXYkn75H3hhfnFYjik/2qunHBBZfcdG148MaNP6DjX33M238T9Zw/GyGx00JMogr2pdP4JAErv9a5yt4YR41KGf8guSOUbOXVARw6+ybh7+meb7w4BeTlj3aZkv8tVGdfIt3lrwVnlbzhLjeQY6PplKp3/a5Kr5yM0T4wJoKQQ6v3vSNmrhpbuAtKxpMILe8CQoo=" # listar api-keys # api_response = client.api_keys.get_list(1, 10) @@ -1355,8 +1357,185 @@ def main (): # ) # api_response = client.invoices.get_status(invoice_status) # print(api_response) - - + + + # ======================================== + # FACTURAS DE NOMINA (CFDI de Nomina) + # ======================================== + + # Crear factura de nomina por valores (Sdk). + # El tipo de factura se determina por el campo type_code="N" (Nomina) + + # from fiscalapi import ( + # Invoice, InvoiceIssuer, InvoiceRecipient, + # InvoiceIssuerEmployerData, InvoiceRecipientEmployeeData, + # TaxCredential, InvoiceComplement, + # PayrollComplement, PayrollEarningsComplement, + # PayrollEarning, PayrollOtherPayment, PayrollDeduction + # ) + # from decimal import Decimal + + # payroll_invoice = Invoice( + # version_code="4.0", + # series="F", + # date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), + # payment_method_code="PUE", + # currency_code="MXN", + # type_code="N", # N = Nomina + # expedition_zip_code="20000", + # export_code="01", + # issuer=InvoiceIssuer( + # tin="EKU9003173C9", + # legal_name="ESCUELA KEMPER URGATE", + # tax_regime_code="601", + # employer_data=InvoiceIssuerEmployerData( + # employer_registration="B5510768108" + # ), + # tax_credentials=[ + # TaxCredential( + # base64_file=base64_cer, + # file_type=0, # Certificado + # password="12345678a" + # ), + # TaxCredential( + # base64_file=base64_key, + # file_type=1, # Llave privada + # password="12345678a" + # ) + # ] + # ), + # recipient=InvoiceRecipient( + # tin="FUNK671228PH6", + # legal_name="KARLA FUENTE NOLASCO", + # zip_code="01160", + # tax_regime_code="605", + # cfdi_use_code="CN01", # Nomina + # employee_data=InvoiceRecipientEmployeeData( + # curp="XEXX010101MNEXXXA8", + # social_security_number="04078873454", + # labor_relation_start_date="2024-08-18", + # seniority="P54W", # ISO 8601 duration (54 weeks) + # sat_contract_type_id="01", + # sat_tax_regime_type_id="02", + # employee_number="123456789", + # department="GenAI", + # position="Sr Software Engineer", + # sat_job_risk_id="1", + # sat_payment_periodicity_id="05", + # sat_bank_id="012", + # base_salary_for_contributions=Decimal("2828.50"), + # integrated_daily_salary=Decimal("0.00"), + # sat_payroll_state_id="JAL" + # ) + # ), + # complement=InvoiceComplement( + # payroll=PayrollComplement( + # version="1.2", + # payroll_type_code="O", # O = Ordinaria + # payment_date="2025-08-30", + # initial_payment_date="2025-07-31", + # final_payment_date="2025-08-30", + # days_paid=Decimal("30"), + # earnings=PayrollEarningsComplement( + # earnings=[ + # PayrollEarning( + # earning_type_code="001", + # code="1003", + # concept="Sueldo Nominal", + # taxed_amount=Decimal("95030.00"), + # exempt_amount=Decimal("0.00") + # ), + # PayrollEarning( + # earning_type_code="005", + # code="5913", + # concept="Fondo de Ahorro Aportacion Patron", + # taxed_amount=Decimal("0.00"), + # exempt_amount=Decimal("4412.46") + # ), + # PayrollEarning( + # earning_type_code="038", + # code="1885", + # concept="Bono Ingles", + # taxed_amount=Decimal("14254.50"), + # exempt_amount=Decimal("0.00") + # ), + # PayrollEarning( + # earning_type_code="029", + # code="1941", + # concept="Vales Despensa", + # taxed_amount=Decimal("0.00"), + # exempt_amount=Decimal("3439.00") + # ), + # PayrollEarning( + # earning_type_code="038", + # code="1824", + # concept="Herramientas Teletrabajo (telecom y prop. electri)", + # taxed_amount=Decimal("273.00"), + # exempt_amount=Decimal("0.00") + # ) + # ], + # other_payments=[ + # PayrollOtherPayment( + # other_payment_type_code="002", + # code="5050", + # concept="Exceso de subsidio al empleo", + # amount=Decimal("0.00"), + # subsidy_caused=Decimal("0.00") + # ) + # ] + # ), + # deductions=[ + # PayrollDeduction( + # deduction_type_code="002", + # code="5003", + # concept="ISR Causado", + # amount=Decimal("27645.52") + # ), + # PayrollDeduction( + # deduction_type_code="004", + # code="5910", + # concept="Fondo de ahorro Empleado Inversion", + # amount=Decimal("4412.46") + # ), + # PayrollDeduction( + # deduction_type_code="004", + # code="5914", + # concept="Fondo de Ahorro Patron Inversion", + # amount=Decimal("4412.46") + # ), + # PayrollDeduction( + # deduction_type_code="004", + # code="1966", + # concept="Contribucion poliza exceso GMM", + # amount=Decimal("519.91") + # ), + # PayrollDeduction( + # deduction_type_code="004", + # code="1934", + # concept="Descuento Vales Despensa", + # amount=Decimal("1.00") + # ), + # PayrollDeduction( + # deduction_type_code="004", + # code="1942", + # concept="Vales Despensa Electronico", + # amount=Decimal("3439.00") + # ), + # PayrollDeduction( + # deduction_type_code="001", + # code="1895", + # concept="IMSS", + # amount=Decimal("2391.13") + # ) + # ] + # ) + # ) + # ) + + # api_response = client.invoices.create(payroll_invoice) + # print(api_response) + + # ======================================== # EJEMPLOS DE DESCARGA MASIVA # ======================================== diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index 82daf22..bb14f04 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -165,8 +165,8 @@ class InvoiceRecipientEmployeeData(BaseDto): """Datos del empleado para el receptor en CFDI de nómina.""" curp: Optional[str] = Field(default=None, alias="curp") social_security_number: Optional[str] = Field(default=None, alias="socialSecurityNumber") - labor_relation_start_date: Optional[datetime] = Field(default=None, alias="laborRelationStartDate") - seniority: Optional[int] = Field(default=None, alias="seniority") + labor_relation_start_date: Optional[str] = Field(default=None, alias="laborRelationStartDate") + seniority: Optional[str] = Field(default=None, alias="seniority") sat_contract_type_id: Optional[str] = Field(default=None, alias="satContractTypeId") sat_unionized_status_id: Optional[str] = Field(default=None, alias="satUnionizedStatusId") sat_workday_type_id: Optional[str] = Field(default=None, alias="satWorkdayTypeId") From d33b828ac35ffcdac445e5b0e95e83c4da02253c Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Tue, 20 Jan 2026 19:05:45 -0600 Subject: [PATCH 16/28] Add payroll invoice example and IDE configuration - Add ejemplos-nomina.py with complete payroll invoice example - Add pyproject.toml with Pyright/Ruff configuration - Configure linters to ignore Pydantic field name warnings --- ejemplos-nomina.py | 185 +++++++++++++++++++++++++++++++++++++++++++++ examples.py | 176 ++---------------------------------------- pyproject.toml | 20 +++++ 3 files changed, 210 insertions(+), 171 deletions(-) create mode 100644 ejemplos-nomina.py create mode 100644 pyproject.toml diff --git a/ejemplos-nomina.py b/ejemplos-nomina.py new file mode 100644 index 0000000..dba2851 --- /dev/null +++ b/ejemplos-nomina.py @@ -0,0 +1,185 @@ +from datetime import datetime +from decimal import Decimal +from fiscalapi.models.common_models import FiscalApiSettings +from fiscalapi.models.fiscalapi_models import Invoice, InvoiceComplement, InvoiceIssuer, InvoiceIssuerEmployerData, InvoiceRecipient, InvoiceRecipientEmployeeData, PayrollComplement, PayrollDeduction, PayrollEarning, PayrollEarningsComplement, PayrollOtherPayment, TaxCredential +from fiscalapi.services.fiscalapi_client import FiscalApiClient + +def main (): + + print("Hello World!") + settings = FiscalApiSettings( + #api_url="https://test.fiscalapi.com", + #api_key="", + #tenant="", + api_url="http://localhost:5001", + api_key="sk_development_b470ea83_3c0f_4209_b933_85223b960d91", + tenant="102e5f13-e114-41dd-bea7-507fce177281" + ) + + client = FiscalApiClient(settings=settings) + + base64_cer = "MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" + base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS/AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucoZQObOaLUEm+I+QZ7Y8Giupo+F1XWkLvAsdk/uZlJcTfKLJyJbJwsQYbSpLOCLataZ4O5MVnnmMbfG//NKJn9kSMvJQZhSwAwoGLYDm1ESGezrvZabgFJnoQv8Si1nAhVGTk9FkFBesxRzq07dmZYwFCnFSX4xt2fDHs1PMpQbeq83aL/PzLCce3kxbYSB5kQlzGtUYayiYXcu0cVRu228VwBLCD+2wTDDoCmRXtPesgrLKUR4WWWb5N2AqAU1mNDC+UEYsENAerOFXWnmwrcTAu5qyZ7GsBMTpipW4Dbou2yqQ0lpA/aB06n1kz1aL6mNqGPaJ+OqoFuc8Ugdhadd+MmjHfFzoI20SZ3b2geCsUMNCsAd6oXMsZdWm8lzjqCGWHFeol0ik/xHMQvuQkkeCsQ28PBxdnUgf7ZGer+TN+2ZLd2kvTBOk6pIVgy5yC6cZ+o1Tloql9hYGa6rT3xcMbXlW+9e5jM2MWXZliVW3ZhaPjptJFDbIfWxJPjz4QvKyJk0zok4muv13Iiwj2bCyefUTRz6psqI4cGaYm9JpscKO2RCJN8UluYGbbWmYQU+Int6LtZj/lv8p6xnVjWxYI+rBPdtkpfFYRp+MJiXjgPw5B6UGuoruv7+vHjOLHOotRo+RdjZt7NqL9dAJnl1Qb2jfW6+d7NYQSI/bAwxO0sk4taQIT6Gsu/8kfZOPC2xk9rphGqCSS/4q3Os0MMjA1bcJLyoWLp13pqhK6bmiiHw0BBXH4fbEp4xjSbpPx4tHXzbdn8oDsHKZkWh3pPC2J/nVl0k/yF1KDVowVtMDXE47k6TGVcBoqe8PDXCG9+vjRpzIidqNo5qebaUZu6riWMWzldz8x3Z/jLWXuDiM7/Yscn0Z2GIlfoeyz+GwP2eTdOw9EUedHjEQuJY32bq8LICimJ4Ht+zMJKUyhwVQyAER8byzQBwTYmYP5U0wdsyIFitphw+/IH8+v08Ia1iBLPQAeAvRfTTIFLCs8foyUrj5Zv2B/wTYIZy6ioUM+qADeXyo45uBLLqkN90Rf6kiTqDld78NxwsfyR5MxtJLVDFkmf2IMMJHTqSfhbi+7QJaC11OOUJTD0v9wo0X/oO5GvZhe0ZaGHnm9zqTopALuFEAxcaQlc4R81wjC4wrIrqWnbcl2dxiBtD73KW+wcC9ymsLf4I8BEmiN25lx/OUc1IHNyXZJYSFkEfaxCEZWKcnbiyf5sqFSSlEqZLc4lUPJFAoP6s1FHVcyO0odWqdadhRZLZC9RCzQgPlMRtji/OXy5phh7diOBZv5UYp5nb+MZ2NAB/eFXm2JLguxjvEstuvTDmZDUb6Uqv++RdhO5gvKf/AcwU38ifaHQ9uvRuDocYwVxZS2nr9rOwZ8nAh+P2o4e0tEXjxFKQGhxXYkn75H3hhfnFYjik/2qunHBBZfcdG148MaNP6DjX33M238T9Zw/GyGx00JMogr2pdP4JAErv9a5yt4YR41KGf8guSOUbOXVARw6+ybh7+meb7w4BeTlj3aZkv8tVGdfIt3lrwVnlbzhLjeQY6PplKp3/a5Kr5yM0T4wJoKQQ6v3vSNmrhpbuAtKxpMILe8CQoo=" + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), + payment_method_code="PUE", + currency_code="MXN", + type_code="N", # N = Nomina + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108" + ), + tax_credentials=[ + TaxCredential( + base64_file=base64_cer, + file_type=0, # Certificado + password="12345678a" + ), + TaxCredential( + base64_file=base64_key, + file_type=1, # Llave privada + password="12345678a" + ) + ] + ), + recipient=InvoiceRecipient( + tin="FUNK671228PH6", + legal_name="KARLA FUENTE NOLASCO", + zip_code="01160", + tax_regime_code="605", + cfdi_use_code="CN01", # Nomina + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101MNEXXXA8", + social_security_number="04078873454", + labor_relation_start_date="2024-08-18", + seniority="P54W", # ISO 8601 duration (54 weeks) + sat_contract_type_id="01", + sat_tax_regime_type_id="02", + employee_number="123456789", + department="GenAI", + position="Sr Software Engineer", + sat_job_risk_id="1", + sat_payment_periodicity_id="05", + sat_bank_id="012", + base_salary_for_contributions=Decimal("2828.50"), + integrated_daily_salary=Decimal("0.00"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", # O = Ordinaria + payment_date="2025-08-30", + initial_payment_date="2025-07-31", + final_payment_date="2025-08-30", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="1003", + concept="Sueldo Nominal", + taxed_amount=Decimal("95030.00"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="005", + code="5913", + concept="Fondo de Ahorro Aportacion Patron", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("4412.46") + ), + PayrollEarning( + earning_type_code="038", + code="1885", + concept="Bono Ingles", + taxed_amount=Decimal("14254.50"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="029", + code="1941", + concept="Vales Despensa", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("3439.00") + ), + PayrollEarning( + earning_type_code="038", + code="1824", + concept="Herramientas Teletrabajo (telecom y prop. electri)", + taxed_amount=Decimal("273.00"), + exempt_amount=Decimal("0.00") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="002", + code="5050", + concept="Exceso de subsidio al empleo", + amount=Decimal("0.00"), + subsidy_caused=Decimal("0.00") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="002", + code="5003", + concept="ISR Causado", + amount=Decimal("27645.52") + ), + PayrollDeduction( + deduction_type_code="004", + code="5910", + concept="Fondo de ahorro Empleado Inversion", + amount=Decimal("4412.46") + ), + PayrollDeduction( + deduction_type_code="004", + code="5914", + concept="Fondo de Ahorro Patron Inversion", + amount=Decimal("4412.46") + ), + PayrollDeduction( + deduction_type_code="004", + code="1966", + concept="Contribucion poliza exceso GMM", + amount=Decimal("519.91") + ), + PayrollDeduction( + deduction_type_code="004", + code="1934", + concept="Descuento Vales Despensa", + amount=Decimal("1.00") + ), + PayrollDeduction( + deduction_type_code="004", + code="1942", + concept="Vales Despensa Electronico", + amount=Decimal("3439.00") + ), + PayrollDeduction( + deduction_type_code="001", + code="1895", + concept="IMSS", + amount=Decimal("2391.13") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(api_response) + +if __name__ == "__main__": + main() diff --git a/examples.py b/examples.py index ccdf095..d54a5d3 100644 --- a/examples.py +++ b/examples.py @@ -8,17 +8,12 @@ def main (): settings = FiscalApiSettings( api_url="https://test.fiscalapi.com", - api_key="", - tenant="", + #api_key="", + #tenant="", ) client = FiscalApiClient(settings=settings) - - - - base64_cer = "MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" - base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS/AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucoZQObOaLUEm+I+QZ7Y8Giupo+F1XWkLvAsdk/uZlJcTfKLJyJbJwsQYbSpLOCLataZ4O5MVnnmMbfG//NKJn9kSMvJQZhSwAwoGLYDm1ESGezrvZabgFJnoQv8Si1nAhVGTk9FkFBesxRzq07dmZYwFCnFSX4xt2fDHs1PMpQbeq83aL/PzLCce3kxbYSB5kQlzGtUYayiYXcu0cVRu228VwBLCD+2wTDDoCmRXtPesgrLKUR4WWWb5N2AqAU1mNDC+UEYsENAerOFXWnmwrcTAu5qyZ7GsBMTpipW4Dbou2yqQ0lpA/aB06n1kz1aL6mNqGPaJ+OqoFuc8Ugdhadd+MmjHfFzoI20SZ3b2geCsUMNCsAd6oXMsZdWm8lzjqCGWHFeol0ik/xHMQvuQkkeCsQ28PBxdnUgf7ZGer+TN+2ZLd2kvTBOk6pIVgy5yC6cZ+o1Tloql9hYGa6rT3xcMbXlW+9e5jM2MWXZliVW3ZhaPjptJFDbIfWxJPjz4QvKyJk0zok4muv13Iiwj2bCyefUTRz6psqI4cGaYm9JpscKO2RCJN8UluYGbbWmYQU+Int6LtZj/lv8p6xnVjWxYI+rBPdtkpfFYRp+MJiXjgPw5B6UGuoruv7+vHjOLHOotRo+RdjZt7NqL9dAJnl1Qb2jfW6+d7NYQSI/bAwxO0sk4taQIT6Gsu/8kfZOPC2xk9rphGqCSS/4q3Os0MMjA1bcJLyoWLp13pqhK6bmiiHw0BBXH4fbEp4xjSbpPx4tHXzbdn8oDsHKZkWh3pPC2J/nVl0k/yF1KDVowVtMDXE47k6TGVcBoqe8PDXCG9+vjRpzIidqNo5qebaUZu6riWMWzldz8x3Z/jLWXuDiM7/Yscn0Z2GIlfoeyz+GwP2eTdOw9EUedHjEQuJY32bq8LICimJ4Ht+zMJKUyhwVQyAER8byzQBwTYmYP5U0wdsyIFitphw+/IH8+v08Ia1iBLPQAeAvRfTTIFLCs8foyUrj5Zv2B/wTYIZy6ioUM+qADeXyo45uBLLqkN90Rf6kiTqDld78NxwsfyR5MxtJLVDFkmf2IMMJHTqSfhbi+7QJaC11OOUJTD0v9wo0X/oO5GvZhe0ZaGHnm9zqTopALuFEAxcaQlc4R81wjC4wrIrqWnbcl2dxiBtD73KW+wcC9ymsLf4I8BEmiN25lx/OUc1IHNyXZJYSFkEfaxCEZWKcnbiyf5sqFSSlEqZLc4lUPJFAoP6s1FHVcyO0odWqdadhRZLZC9RCzQgPlMRtji/OXy5phh7diOBZv5UYp5nb+MZ2NAB/eFXm2JLguxjvEstuvTDmZDUb6Uqv++RdhO5gvKf/AcwU38ifaHQ9uvRuDocYwVxZS2nr9rOwZ8nAh+P2o4e0tEXjxFKQGhxXYkn75H3hhfnFYjik/2qunHBBZfcdG148MaNP6DjX33M238T9Zw/GyGx00JMogr2pdP4JAErv9a5yt4YR41KGf8guSOUbOXVARw6+ybh7+meb7w4BeTlj3aZkv8tVGdfIt3lrwVnlbzhLjeQY6PplKp3/a5Kr5yM0T4wJoKQQ6v3vSNmrhpbuAtKxpMILe8CQoo=" - + # listar api-keys # api_response = client.api_keys.get_list(1, 10) # print(api_response) @@ -1375,165 +1370,7 @@ def main (): # ) # from decimal import Decimal - # payroll_invoice = Invoice( - # version_code="4.0", - # series="F", - # date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), - # payment_method_code="PUE", - # currency_code="MXN", - # type_code="N", # N = Nomina - # expedition_zip_code="20000", - # export_code="01", - # issuer=InvoiceIssuer( - # tin="EKU9003173C9", - # legal_name="ESCUELA KEMPER URGATE", - # tax_regime_code="601", - # employer_data=InvoiceIssuerEmployerData( - # employer_registration="B5510768108" - # ), - # tax_credentials=[ - # TaxCredential( - # base64_file=base64_cer, - # file_type=0, # Certificado - # password="12345678a" - # ), - # TaxCredential( - # base64_file=base64_key, - # file_type=1, # Llave privada - # password="12345678a" - # ) - # ] - # ), - # recipient=InvoiceRecipient( - # tin="FUNK671228PH6", - # legal_name="KARLA FUENTE NOLASCO", - # zip_code="01160", - # tax_regime_code="605", - # cfdi_use_code="CN01", # Nomina - # employee_data=InvoiceRecipientEmployeeData( - # curp="XEXX010101MNEXXXA8", - # social_security_number="04078873454", - # labor_relation_start_date="2024-08-18", - # seniority="P54W", # ISO 8601 duration (54 weeks) - # sat_contract_type_id="01", - # sat_tax_regime_type_id="02", - # employee_number="123456789", - # department="GenAI", - # position="Sr Software Engineer", - # sat_job_risk_id="1", - # sat_payment_periodicity_id="05", - # sat_bank_id="012", - # base_salary_for_contributions=Decimal("2828.50"), - # integrated_daily_salary=Decimal("0.00"), - # sat_payroll_state_id="JAL" - # ) - # ), - # complement=InvoiceComplement( - # payroll=PayrollComplement( - # version="1.2", - # payroll_type_code="O", # O = Ordinaria - # payment_date="2025-08-30", - # initial_payment_date="2025-07-31", - # final_payment_date="2025-08-30", - # days_paid=Decimal("30"), - # earnings=PayrollEarningsComplement( - # earnings=[ - # PayrollEarning( - # earning_type_code="001", - # code="1003", - # concept="Sueldo Nominal", - # taxed_amount=Decimal("95030.00"), - # exempt_amount=Decimal("0.00") - # ), - # PayrollEarning( - # earning_type_code="005", - # code="5913", - # concept="Fondo de Ahorro Aportacion Patron", - # taxed_amount=Decimal("0.00"), - # exempt_amount=Decimal("4412.46") - # ), - # PayrollEarning( - # earning_type_code="038", - # code="1885", - # concept="Bono Ingles", - # taxed_amount=Decimal("14254.50"), - # exempt_amount=Decimal("0.00") - # ), - # PayrollEarning( - # earning_type_code="029", - # code="1941", - # concept="Vales Despensa", - # taxed_amount=Decimal("0.00"), - # exempt_amount=Decimal("3439.00") - # ), - # PayrollEarning( - # earning_type_code="038", - # code="1824", - # concept="Herramientas Teletrabajo (telecom y prop. electri)", - # taxed_amount=Decimal("273.00"), - # exempt_amount=Decimal("0.00") - # ) - # ], - # other_payments=[ - # PayrollOtherPayment( - # other_payment_type_code="002", - # code="5050", - # concept="Exceso de subsidio al empleo", - # amount=Decimal("0.00"), - # subsidy_caused=Decimal("0.00") - # ) - # ] - # ), - # deductions=[ - # PayrollDeduction( - # deduction_type_code="002", - # code="5003", - # concept="ISR Causado", - # amount=Decimal("27645.52") - # ), - # PayrollDeduction( - # deduction_type_code="004", - # code="5910", - # concept="Fondo de ahorro Empleado Inversion", - # amount=Decimal("4412.46") - # ), - # PayrollDeduction( - # deduction_type_code="004", - # code="5914", - # concept="Fondo de Ahorro Patron Inversion", - # amount=Decimal("4412.46") - # ), - # PayrollDeduction( - # deduction_type_code="004", - # code="1966", - # concept="Contribucion poliza exceso GMM", - # amount=Decimal("519.91") - # ), - # PayrollDeduction( - # deduction_type_code="004", - # code="1934", - # concept="Descuento Vales Despensa", - # amount=Decimal("1.00") - # ), - # PayrollDeduction( - # deduction_type_code="004", - # code="1942", - # concept="Vales Despensa Electronico", - # amount=Decimal("3439.00") - # ), - # PayrollDeduction( - # deduction_type_code="001", - # code="1895", - # concept="IMSS", - # amount=Decimal("2391.13") - # ) - # ] - # ) - # ) - # ) - - # api_response = client.invoices.create(payroll_invoice) - # print(api_response) + # ======================================== @@ -1650,7 +1487,4 @@ def main (): if __name__ == "__main__": main() - - -if __name__ == "__main__": - main() \ No newline at end of file + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a9feb3a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.pyright] +# Enable Pydantic plugin support +pythonVersion = "3.9" +typeCheckingMode = "basic" + +# Pydantic models use populate_by_name=True which allows using Python attribute names +# instead of aliases. This setting prevents false positives. +reportArgumentType = "none" +reportCallIssue = "none" + +[tool.pylsp-mypy] +enabled = false + +[tool.ruff] +line-length = 120 +target-version = "py39" + +[tool.ruff.lint] +select = ["E", "F", "W"] +ignore = ["E501", "W293", "W291", "W292"] From c512b7ab78492cfaffd4a33a2a61ec265760ca7b Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Tue, 20 Jan 2026 19:22:55 -0600 Subject: [PATCH 17/28] initial sample added --- ejemplos-nomina.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ejemplos-nomina.py b/ejemplos-nomina.py index dba2851..58427cf 100644 --- a/ejemplos-nomina.py +++ b/ejemplos-nomina.py @@ -18,9 +18,12 @@ def main (): client = FiscalApiClient(settings=settings) - base64_cer = "MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" - base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS/AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucoZQObOaLUEm+I+QZ7Y8Giupo+F1XWkLvAsdk/uZlJcTfKLJyJbJwsQYbSpLOCLataZ4O5MVnnmMbfG//NKJn9kSMvJQZhSwAwoGLYDm1ESGezrvZabgFJnoQv8Si1nAhVGTk9FkFBesxRzq07dmZYwFCnFSX4xt2fDHs1PMpQbeq83aL/PzLCce3kxbYSB5kQlzGtUYayiYXcu0cVRu228VwBLCD+2wTDDoCmRXtPesgrLKUR4WWWb5N2AqAU1mNDC+UEYsENAerOFXWnmwrcTAu5qyZ7GsBMTpipW4Dbou2yqQ0lpA/aB06n1kz1aL6mNqGPaJ+OqoFuc8Ugdhadd+MmjHfFzoI20SZ3b2geCsUMNCsAd6oXMsZdWm8lzjqCGWHFeol0ik/xHMQvuQkkeCsQ28PBxdnUgf7ZGer+TN+2ZLd2kvTBOk6pIVgy5yC6cZ+o1Tloql9hYGa6rT3xcMbXlW+9e5jM2MWXZliVW3ZhaPjptJFDbIfWxJPjz4QvKyJk0zok4muv13Iiwj2bCyefUTRz6psqI4cGaYm9JpscKO2RCJN8UluYGbbWmYQU+Int6LtZj/lv8p6xnVjWxYI+rBPdtkpfFYRp+MJiXjgPw5B6UGuoruv7+vHjOLHOotRo+RdjZt7NqL9dAJnl1Qb2jfW6+d7NYQSI/bAwxO0sk4taQIT6Gsu/8kfZOPC2xk9rphGqCSS/4q3Os0MMjA1bcJLyoWLp13pqhK6bmiiHw0BBXH4fbEp4xjSbpPx4tHXzbdn8oDsHKZkWh3pPC2J/nVl0k/yF1KDVowVtMDXE47k6TGVcBoqe8PDXCG9+vjRpzIidqNo5qebaUZu6riWMWzldz8x3Z/jLWXuDiM7/Yscn0Z2GIlfoeyz+GwP2eTdOw9EUedHjEQuJY32bq8LICimJ4Ht+zMJKUyhwVQyAER8byzQBwTYmYP5U0wdsyIFitphw+/IH8+v08Ia1iBLPQAeAvRfTTIFLCs8foyUrj5Zv2B/wTYIZy6ioUM+qADeXyo45uBLLqkN90Rf6kiTqDld78NxwsfyR5MxtJLVDFkmf2IMMJHTqSfhbi+7QJaC11OOUJTD0v9wo0X/oO5GvZhe0ZaGHnm9zqTopALuFEAxcaQlc4R81wjC4wrIrqWnbcl2dxiBtD73KW+wcC9ymsLf4I8BEmiN25lx/OUc1IHNyXZJYSFkEfaxCEZWKcnbiyf5sqFSSlEqZLc4lUPJFAoP6s1FHVcyO0odWqdadhRZLZC9RCzQgPlMRtji/OXy5phh7diOBZv5UYp5nb+MZ2NAB/eFXm2JLguxjvEstuvTDmZDUb6Uqv++RdhO5gvKf/AcwU38ifaHQ9uvRuDocYwVxZS2nr9rOwZ8nAh+P2o4e0tEXjxFKQGhxXYkn75H3hhfnFYjik/2qunHBBZfcdG148MaNP6DjX33M238T9Zw/GyGx00JMogr2pdP4JAErv9a5yt4YR41KGf8guSOUbOXVARw6+ybh7+meb7w4BeTlj3aZkv8tVGdfIt3lrwVnlbzhLjeQY6PplKp3/a5Kr5yM0T4wJoKQQ6v3vSNmrhpbuAtKxpMILe8CQoo=" - + escuela_kemper_urgate_base64_cer = "MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" + escuela_kemper_urgate_base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS/AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucoZQObOaLUEm+I+QZ7Y8Giupo+F1XWkLvAsdk/uZlJcTfKLJyJbJwsQYbSpLOCLataZ4O5MVnnmMbfG//NKJn9kSMvJQZhSwAwoGLYDm1ESGezrvZabgFJnoQv8Si1nAhVGTk9FkFBesxRzq07dmZYwFCnFSX4xt2fDHs1PMpQbeq83aL/PzLCce3kxbYSB5kQlzGtUYayiYXcu0cVRu228VwBLCD+2wTDDoCmRXtPesgrLKUR4WWWb5N2AqAU1mNDC+UEYsENAerOFXWnmwrcTAu5qyZ7GsBMTpipW4Dbou2yqQ0lpA/aB06n1kz1aL6mNqGPaJ+OqoFuc8Ugdhadd+MmjHfFzoI20SZ3b2geCsUMNCsAd6oXMsZdWm8lzjqCGWHFeol0ik/xHMQvuQkkeCsQ28PBxdnUgf7ZGer+TN+2ZLd2kvTBOk6pIVgy5yC6cZ+o1Tloql9hYGa6rT3xcMbXlW+9e5jM2MWXZliVW3ZhaPjptJFDbIfWxJPjz4QvKyJk0zok4muv13Iiwj2bCyefUTRz6psqI4cGaYm9JpscKO2RCJN8UluYGbbWmYQU+Int6LtZj/lv8p6xnVjWxYI+rBPdtkpfFYRp+MJiXjgPw5B6UGuoruv7+vHjOLHOotRo+RdjZt7NqL9dAJnl1Qb2jfW6+d7NYQSI/bAwxO0sk4taQIT6Gsu/8kfZOPC2xk9rphGqCSS/4q3Os0MMjA1bcJLyoWLp13pqhK6bmiiHw0BBXH4fbEp4xjSbpPx4tHXzbdn8oDsHKZkWh3pPC2J/nVl0k/yF1KDVowVtMDXE47k6TGVcBoqe8PDXCG9+vjRpzIidqNo5qebaUZu6riWMWzldz8x3Z/jLWXuDiM7/Yscn0Z2GIlfoeyz+GwP2eTdOw9EUedHjEQuJY32bq8LICimJ4Ht+zMJKUyhwVQyAER8byzQBwTYmYP5U0wdsyIFitphw+/IH8+v08Ia1iBLPQAeAvRfTTIFLCs8foyUrj5Zv2B/wTYIZy6ioUM+qADeXyo45uBLLqkN90Rf6kiTqDld78NxwsfyR5MxtJLVDFkmf2IMMJHTqSfhbi+7QJaC11OOUJTD0v9wo0X/oO5GvZhe0ZaGHnm9zqTopALuFEAxcaQlc4R81wjC4wrIrqWnbcl2dxiBtD73KW+wcC9ymsLf4I8BEmiN25lx/OUc1IHNyXZJYSFkEfaxCEZWKcnbiyf5sqFSSlEqZLc4lUPJFAoP6s1FHVcyO0odWqdadhRZLZC9RCzQgPlMRtji/OXy5phh7diOBZv5UYp5nb+MZ2NAB/eFXm2JLguxjvEstuvTDmZDUb6Uqv++RdhO5gvKf/AcwU38ifaHQ9uvRuDocYwVxZS2nr9rOwZ8nAh+P2o4e0tEXjxFKQGhxXYkn75H3hhfnFYjik/2qunHBBZfcdG148MaNP6DjX33M238T9Zw/GyGx00JMogr2pdP4JAErv9a5yt4YR41KGf8guSOUbOXVARw6+ybh7+meb7w4BeTlj3aZkv8tVGdfIt3lrwVnlbzhLjeQY6PplKp3/a5Kr5yM0T4wJoKQQ6v3vSNmrhpbuAtKxpMILe8CQoo=" + organicos_navez_osorio_base64_cer = "MIIF1DCCA7ygAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MzkwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTI1NTE2WhcNMjcwNTE4MTI1NTE2WjCB+zEzMDEGA1UEAxQqT1JHQU5JQ09TINFBVkVaIE9TT1JJTyBTLkEgREUgQy5WIFNBIERFIENWMTMwMQYDVQQpFCpPUkdBTklDT1Mg0UFWRVogT1NPUklPIFMuQSBERSBDLlYgU0EgREUgQ1YxMzAxBgNVBAoUKk9SR0FOSUNPUyDRQVZFWiBPU09SSU8gUy5BIERFIEMuViBTQSBERSBDVjElMCMGA1UELRQcT9FPMTIwNzI2UlgzIC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlAF4PoRqITQAEjFBzzfiT/NSN2yvb7Iv1ZMe4qD7tBxBxazRCx+GnimfpR+eaM744RlRDUj+hZfWcsOMn+q65UEIP+Xq5V1NbO1LZDse9uG1fLLSmptfKjyfvTtmBNYBjC3G6YmRv5qVw81CIS4aQOSMXKD+lrxjmRUhV9EAtXVoqGxvyDKeeX4caKuRz8mlrnR8/SMbnpobe5BNoXPrpDbEypemiJXe40pjsltY0RV3b0W0JtJQABUwZ9xn0lPYHY2q7IxYfohibv+o9ldXOXY6tivBZFfbGQSUp7CevC55+Y6uqh35Pi1o0nt/vBVgUOVPNM8d4TvGbXsE0G2J7QIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFp52XykMXfFUtjQqA2zzLPrPIDSMEpkm1vWY0qfz2gC2TlVpbDCWH2vFHpP8D14OifXOmYkws2cvLyE0uBN6se4zXxVHBpTEq+93rvu/tjvMU6r7DISDwB0EX5kmKIFcOugET3/Eq1mxZ6mrI0K26RaEUz+HVyR0EQ2Ll5CLExDkPYV/am0gynhn6QPkxPNbcbm77PEIbH7zc+t7ZB5sgQ6LnubgnKNZDn8bNhkuM1jqFkh7h0owhlJrOvATgrDSLnrot8FoLFkrWQD4uA5udGRwXn5QWx0QM5ScNiSgSRilSFEyXn6rH/CJLO05Sx5OwJJTaxFbAyOXnoNdPMzbQAziaW78478nCNZVSrKWpjwWpScirtM2zcQ9fywd/a3CG66Ff29zasfhHJCp29TIjj1OURp6l1CKc16+UxjuVJ1z5Xh7v3s8S2gtmuYP1sUXPvAEYuVp9CFW87QVMtl3+nGlyJEzSAW/yaps9ua5RmyJK0Mjk1zyXjOJoIY75CIOMN8oqVAxmLJg5XftXJSekGpxybw9aq9qOJdmxVcZoAFaYg4MAdKViBoYxfWfEm4q/ihRz4asnzLp9NJWTXN1YH94rJrK7JSEq820flgr1kiL7z7n1rgWMvhJH9nHriG3yRkno/8OdLJxOSXd7MKZfZx0EWDX8toqWyE7zia8aPM=" + organicos_navez_osorio_base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS8AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucRFLOMmsAaFFEdAecnfgJf0IlyJpvyNOGiSwXgY6uZtS0QJmmupWTlQATxbN4xeN7csx7yCMYxMiWXLyTbjVIWzzsFVKHbsxCudz6UDqMZ3aXEEPDDbPECXJC4FxqzuUgifN4QQuIvxfPbk23m3Vtqu9lr/xMrDNqLZ4RiqY2062kgQzGzekq8CSC97qBAbb8SFMgakFjeHN0JiTGaTpYCpGbu4d+i3ZrQ0mlYkxesdvCLqlCwVM0RTMJsNQ8vpBpRDzH372iOTLCO/gXtV8pEsxpUzG9LSUBo7xSMd1/lcfdyqVgnScgUm8/+toxk6uwZkUMWWvp7tqrMYQFYdR5CjiZjgAWrNorgMmawBqkJU6KQO/CpXVn99U1fANPfQoeyQMgLt35k0JKynG8MuWsgb4EG9Z6sRmOsCQQDDMKwhBjqcbEwN2dL4f1HyN8wklFCyYy6j1NTKU2AjRMXVu4+OlAp5jpjgv08RQxEkW/tNMSSBcpvOzNr64u0M692VA2fThR3UMQ/MZ2yVM6yY3GgIu2tJmg08lhmkoLpWZIMy7bZjj/AEbi7B3wSF4vDYZJcr/Djeezm3MMSghoiOIRSqtBjwf7ZjhA2ymdCsrzy7XSMVekT0y1S+ew1WhnzUNKQSucb6V2yRwNbm0EyeEuvVyHgiGEzCrzNbNHCfoFr69YCUi8itiDfiV7/p7LJzD8J/w85nmOkI/9p+aZ2EyaOdThqBmN4CtoDi5ixz/1EElLn7KVI4d/DZsZ4ZMu76kLAy94o0m6ORSbHX5hw12+P5DgGaLu/Dxd9cctRCkvcUdagiECuKGLJpxTJvEBQoZqUB8AJFgwKcNLl3Z5KAWL5hV0t1h8i3N4HllygqpfUSQMLWCtlGwdI4XGlGI5CmnjrL2Uj8sj9C0zSNqZVnAXFMV9f2ND9W6YJqfU89BQ6Y4QQRMGjXcVF7c78bn5r6zI+Qv2QKm3YiGCfuIa64B+PB/BdithpOuBPn5X5Zxc8ju/kYjJk7sau7VtKJseGOJ1bqOq99VzaxoHjzoJgthLHtni9WtGAnnQy7GMWGW4Un2yObHCxvQxx/rIZEaQiCGfRXOcZIZuXBe5xeHJFGrekDxu3YyumEnLWvsirDF3qhpUtxqvbkTuZw2xT3vTR+oWZpSEnYTd3k/09Eb0ovOPLkbhvcvCEeoI91EJvU+KI4Lm7ZsuTUSpECrHiS3uPOjboCigOWGayKzUHUICNrGK0zxgZXhhl6V7y9pImRl34ID/tZhr3veW4pQKgscv6sQjGJzaph2oCP7uZC6arGWcFpc2pgfBcobmOXYPWKskU3eWKClHBJnJ8MoOru+ObOb+izPhINHOmzP26TnKzFxdZiL+onxjadPYslcLtqlmOYpb/5hHgGOvitLhCLHCp0gYNB2uzj0sVxNs3k7k43KrlO5L6gp1KVaIw2a1yZzOCqDWWcePfKM3Mii9JdVyfHZLRRjFCQiOYo41AltHU+9IcaoT4J/j7pKw5tnlu2VaMlnN0dISpoq/ak0m4YjTd3XdRQeH9ktWmclkc65LdLKf9hIqjVqvOhQUJYkuT7OPgr+o7Z9BnClXMz1/CYWftwQE=" + password = "12345678a" + payroll_invoice = Invoice( version_code="4.0", series="F", @@ -39,12 +42,12 @@ def main (): ), tax_credentials=[ TaxCredential( - base64_file=base64_cer, + base64_file=escuela_kemper_urgate_base64_cer, file_type=0, # Certificado password="12345678a" ), TaxCredential( - base64_file=base64_key, + base64_file=escuela_kemper_urgate_base64_key, file_type=1, # Llave privada password="12345678a" ) From 2e52cf98f499212e4037d3c1b03e72ad79e69041 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Tue, 20 Jan 2026 19:39:03 -0600 Subject: [PATCH 18/28] Add comprehensive payroll invoice examples for all CFDI nomina types Add 13 SDK examples covering different payroll scenarios: - Regular payroll (ordinaria) - Assimilated salaries (asimilados) - Bonuses, savings fund and deductions - Overtime (horas extra) - Disabilities (incapacidades) - SNCF for public organizations - Extraordinary payroll (aguinaldo) - Severance and indemnification - Retirement and pension - Payroll without deductions - Employment subsidy - Travel expenses (viaticos) - Basic payroll Co-Authored-By: Claude Opus 4.5 --- ejemplos-nomina.py | 1810 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 1653 insertions(+), 157 deletions(-) diff --git a/ejemplos-nomina.py b/ejemplos-nomina.py index 58427cf..23a33c6 100644 --- a/ejemplos-nomina.py +++ b/ejemplos-nomina.py @@ -1,188 +1,1684 @@ +""" +Ejemplos de Factura de Nomina usando el SDK de FiscalAPI para Python. + +Este archivo contiene ejemplos de diferentes tipos de facturas de nomina: +1. Nomina Ordinaria +2. Nomina Asimilados +3. Nomina Con Bonos, Fondo Ahorro y Deducciones +4. Nomina Con Horas Extra +5. Nomina Con Incapacidades +6. Nomina con SNCF +7. Nomina Extraordinaria +8. Nomina Separacion Indemnizacion +9. Nomina Jubilacion Pension Retiro +10. Nomina Sin Deducciones +11. Nomina Subsidio causado al empleo +12. Nomina Viaticos +13. Nomina basica +""" + from datetime import datetime from decimal import Decimal from fiscalapi.models.common_models import FiscalApiSettings -from fiscalapi.models.fiscalapi_models import Invoice, InvoiceComplement, InvoiceIssuer, InvoiceIssuerEmployerData, InvoiceRecipient, InvoiceRecipientEmployeeData, PayrollComplement, PayrollDeduction, PayrollEarning, PayrollEarningsComplement, PayrollOtherPayment, TaxCredential +from fiscalapi.models.fiscalapi_models import ( + Invoice, + InvoiceComplement, + InvoiceIssuer, + InvoiceIssuerEmployerData, + InvoiceRecipient, + InvoiceRecipientEmployeeData, + PayrollComplement, + PayrollDeduction, + PayrollEarning, + PayrollEarningsComplement, + PayrollOtherPayment, + PayrollOvertime, + PayrollDisability, + PayrollSeverance, + PayrollRetirement, + PayrollBalanceCompensation, + TaxCredential +) from fiscalapi.services.fiscalapi_client import FiscalApiClient -def main (): - print("Hello World!") - settings = FiscalApiSettings( - #api_url="https://test.fiscalapi.com", - #api_key="", - #tenant="", - api_url="http://localhost:5001", - api_key="sk_development_b470ea83_3c0f_4209_b933_85223b960d91", - tenant="102e5f13-e114-41dd-bea7-507fce177281" - ) +# ============================================================================ +# CONFIGURACION +# ============================================================================ + +# Configuracion del cliente +settings = FiscalApiSettings( + # api_url="https://test.fiscalapi.com", + # api_key="", + # tenant="", + api_url="http://localhost:5001", + api_key="sk_development_b470ea83_3c0f_4209_b933_85223b960d91", + tenant="102e5f13-e114-41dd-bea7-507fce177281" +) - client = FiscalApiClient(settings=settings) +client = FiscalApiClient(settings=settings) + +# Certificados en base64 +escuela_kemper_urgate_base64_cer = "MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" +escuela_kemper_urgate_base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS/AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucoZQObOaLUEm+I+QZ7Y8Giupo+F1XWkLvAsdk/uZlJcTfKLJyJbJwsQYbSpLOCLataZ4O5MVnnmMbfG//NKJn9kSMvJQZhSwAwoGLYDm1ESGezrvZabgFJnoQv8Si1nAhVGTk9FkFBesxRzq07dmZYwFCnFSX4xt2fDHs1PMpQbeq83aL/PzLCce3kxbYSB5kQlzGtUYayiYXcu0cVRu228VwBLCD+2wTDDoCmRXtPesgrLKUR4WWWb5N2AqAU1mNDC+UEYsENAerOFXWnmwrcTAu5qyZ7GsBMTpipW4Dbou2yqQ0lpA/aB06n1kz1aL6mNqGPaJ+OqoFuc8Ugdhadd+MmjHfFzoI20SZ3b2geCsUMNCsAd6oXMsZdWm8lzjqCGWHFeol0ik/xHMQvuQkkeCsQ28PBxdnUgf7ZGer+TN+2ZLd2kvTBOk6pIVgy5yC6cZ+o1Tloql9hYGa6rT3xcMbXlW+9e5jM2MWXZliVW3ZhaPjptJFDbIfWxJPjz4QvKyJk0zok4muv13Iiwj2bCyefUTRz6psqI4cGaYm9JpscKO2RCJN8UluYGbbWmYQU+Int6LtZj/lv8p6xnVjWxYI+rBPdtkpfFYRp+MJiXjgPw5B6UGuoruv7+vHjOLHOotRo+RdjZt7NqL9dAJnl1Qb2jfW6+d7NYQSI/bAwxO0sk4taQIT6Gsu/8kfZOPC2xk9rphGqCSS/4q3Os0MMjA1bcJLyoWLp13pqhK6bmiiHw0BBXH4fbEp4xjSbpPx4tHXzbdn8oDsHKZkWh3pPC2J/nVl0k/yF1KDVowVtMDXE47k6TGVcBoqe8PDXCG9+vjRpzIidqNo5qebaUZu6riWMWzldz8x3Z/jLWXuDiM7/Yscn0Z2GIlfoeyz+GwP2eTdOw9EUedHjEQuJY32bq8LICimJ4Ht+zMJKUyhwVQyAER8byzQBwTYmYP5U0wdsyIFitphw+/IH8+v08Ia1iBLPQAeAvRfTTIFLCs8foyUrj5Zv2B/wTYIZy6ioUM+qADeXyo45uBLLqkN90Rf6kiTqDld78NxwsfyR5MxtJLVDFkmf2IMMJHTqSfhbi+7QJaC11OOUJTD0v9wo0X/oO5GvZhe0ZaGHnm9zqTopALuFEAxcaQlc4R81wjC4wrIrqWnbcl2dxiBtD73KW+wcC9ymsLf4I8BEmiN25lx/OUc1IHNyXZJYSFkEfaxCEZWKcnbiyf5sqFSSlEqZLc4lUPJFAoP6s1FHVcyO0odWqdadhRZLZC9RCzQgPlMRtji/OXy5phh7diOBZv5UYp5nb+MZ2NAB/eFXm2JLguxjvEstuvTDmZDUb6Uqv++RdhO5gvKf/AcwU38ifaHQ9uvRuDocYwVxZS2nr9rOwZ8nAh+P2o4e0tEXjxFKQGhxXYkn75H3hhfnFYjik/2qunHBBZfcdG148MaNP6DjX33M238T9Zw/GyGx00JMogr2pdP4JAErv9a5yt4YR41KGf8guSOUbOXVARw6+ybh7+meb7w4BeTlj3aZkv8tVGdfIt3lrwVnlbzhLjeQY6PplKp3/a5Kr5yM0T4wJoKQQ6v3vSNmrhpbuAtKxpMILe8CQoo=" +organicos_navez_osorio_base64_cer = "MIIF1DCCA7ygAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MzkwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTI1NTE2WhcNMjcwNTE4MTI1NTE2WjCB+zEzMDEGA1UEAxQqT1JHQU5JQ09TINFBVkVaIE9TT1JJTyBTLkEgREUgQy5WIFNBIERFIENWMTMwMQYDVQQpFCpPUkdBTklDT1Mg0UFWRVogT1NPUklPIFMuQSBERSBDLlYgU0EgREUgQ1YxMzAxBgNVBAoUKk9SR0FOSUNPUyDRQVZFWiBPU09SSU8gUy5BIERFIEMuViBTQSBERSBDVjElMCMGA1UELRQcT9FPMTIwNzI2UlgzIC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlAF4PoRqITQAEjFBzzfiT/NSN2yvb7Iv1ZMe4qD7tBxBxazRCx+GnimfpR+eaM744RlRDUj+hZfWcsOMn+q65UEIP+Xq5V1NbO1LZDse9uG1fLLSmptfKjyfvTtmBNYBjC3G6YmRv5qVw81CIS4aQOSMXKD+lrxjmRUhV9EAtXVoqGxvyDKeeX4caKuRz8mlrnR8/SMbnpobe5BNoXPrpDbEypemiJXe40pjsltY0RV3b0W0JtJQABUwZ9xn0lPYHY2q7IxYfohibv+o9ldXOXY6tivBZFfbGQSUp7CevC55+Y6uqh35Pi1o0nt/vBVgUOVPNM8d4TvGbXsE0G2J7QIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFp52XykMXfFUtjQqA2zzLPrPIDSMEpkm1vWY0qfz2gC2TlVpbDCWH2vFHpP8D14OifXOmYkws2cvLyE0uBN6se4zXxVHBpTEq+93rvu/tjvMU6r7DISDwB0EX5kmKIFcOugET3/Eq1mxZ6mrI0K26RaEUz+HVyR0EQ2Ll5CLExDkPYV/am0gynhn6QPkxPNbcbm77PEIbH7zc+t7ZB5sgQ6LnubgnKNZDn8bNhkuM1jqFkh7h0owhlJrOvATgrDSLnrot8FoLFkrWQD4uA5udGRwXn5QWx0QM5ScNiSgSRilSFEyXn6rH/CJLO05Sx5OwJJTaxFbAyOXnoNdPMzbQAziaW78478nCNZVSrKWpjwWpScirtM2zcQ9fywd/a3CG66Ff29zasfhHJCp29TIjj1OURp6l1CKc16+UxjuVJ1z5Xh7v3s8S2gtmuYP1sUXPvAEYuVp9CFW87QVMtl3+nGlyJEzSAW/yaps9ua5RmyJK0Mjk1zyXjOJoIY75CIOMN8oqVAxmLJg5XftXJSekGpxybw9aq9qOJdmxVcZoAFaYg4MAdKViBoYxfWfEm4q/ihRz4asnzLp9NJWTXN1YH94rJrK7JSEq820flgr1kiL7z7n1rgWMvhJH9nHriG3yRkno/8OdLJxOSXd7MKZfZx0EWDX8toqWyE7zia8aPM=" +organicos_navez_osorio_base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS8AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucRFLOMmsAaFFEdAecnfgJf0IlyJpvyNOGiSwXgY6uZtS0QJmmupWTlQATxbN4xeN7csx7yCMYxMiWXLyTbjVIWzzsFVKHbsxCudz6UDqMZ3aXEEPDDbPECXJC4FxqzuUgifN4QQuIvxfPbk23m3Vtqu9lr/xMrDNqLZ4RiqY2062kgQzGzekq8CSC97qBAbb8SFMgakFjeHN0JiTGaTpYCpGbu4d+i3ZrQ0mlYkxesdvCLqlCwVM0RTMJsNQ8vpBpRDzH372iOTLCO/gXtV8pEsxpUzG9LSUBo7xSMd1/lcfdyqVgnScgUm8/+toxk6uwZkUMWWvp7tqrMYQFYdR5CjiZjgAWrNorgMmawBqkJU6KQO/CpXVn99U1fANPfQoeyQMgLt35k0JKynG8MuWsgb4EG9Z6sRmOsCQQDDMKwhBjqcbEwN2dL4f1HyN8wklFCyYy6j1NTKU2AjRMXVu4+OlAp5jpjgv08RQxEkW/tNMSSBcpvOzNr64u0M692VA2fThR3UMQ/MZ2yVM6yY3GgIu2tJmg08lhmkoLpWZIMy7bZjj/AEbi7B3wSF4vDYZJcr/Djeezm3MMSghoiOIRSqtBjwf7ZjhA2ymdCsrzy7XSMVekT0y1S+ew1WhnzUNKQSucb6V2yRwNbm0EyeEuvVyHgiGEzCrzNbNHCfoFr69YCUi8itiDfiV7/p7LJzD8J/w85nmOkI/9p+aZ2EyaOdThqBmN4CtoDi5ixz/1EElLn7KVI4d/DZsZ4ZMu76kLAy94o0m6ORSbHX5hw12+P5DgGaLu/Dxd9cctRCkvcUdagiECuKGLJpxTJvEBQoZqUB8AJFgwKcNLl3Z5KAWL5hV0t1h8i3N4HllygqpfUSQMLWCtlGwdI4XGlGI5CmnjrL2Uj8sj9C0zSNqZVnAXFMV9f2ND9W6YJqfU89BQ6Y4QQRMGjXcVF7c78bn5r6zI+Qv2QKm3YiGCfuIa64B+PB/BdithpOuBPn5X5Zxc8ju/kYjJk7sau7VtKJseGOJ1bqOq99VzaxoHjzoJgthLHtni9WtGAnnQy7GMWGW4Un2yObHCxvQxx/rIZEaQiCGfRXOcZIZuXBe5xeHJFGrekDxu3YyumEnLWvsirDF3qhpUtxqvbkTuZw2xT3vTR+oWZpSEnYTd3k/09Eb0ovOPLkbhvcvCEeoI91EJvU+KI4Lm7ZsuTUSpECrHiS3uPOjboCigOWGayKzUHUICNrGK0zxgZXhhl6V7y9pImRl34ID/tZhr3veW4pQKgscv6sQjGJzaph2oCP7uZC6arGWcFpc2pgfBcobmOXYPWKskU3eWKClHBJnJ8MoOru+ObOb+izPhINHOmzP26TnKzFxdZiL+onxjadPYslcLtqlmOYpb/5hHgGOvitLhCLHCp0gYNB2uzj0sVxNs3k7k43KrlO5L6gp1KVaIw2a1yZzOCqDWWcePfKM3Mii9JdVyfHZLRRjFCQiOYo41AltHU+9IcaoT4J/j7pKw5tnlu2VaMlnN0dISpoq/ak0m4YjTd3XdRQeH9ktWmclkc65LdLKf9hIqjVqvOhQUJYkuT7OPgr+o7Z9BnClXMz1/CYWftwQE=" +password = "12345678a" + + +# ============================================================================ +# 1. NOMINA ORDINARIA +# ============================================================================ +def create_nomina_ordinaria(): + """ + Crea una factura de nomina ordinaria con percepciones, deducciones y otros pagos. + """ + print("\n" + "="*60) + print("1. NOMINA ORDINARIA") + print("="*60) - escuela_kemper_urgate_base64_cer = "MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE=" - escuela_kemper_urgate_base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS/AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucoZQObOaLUEm+I+QZ7Y8Giupo+F1XWkLvAsdk/uZlJcTfKLJyJbJwsQYbSpLOCLataZ4O5MVnnmMbfG//NKJn9kSMvJQZhSwAwoGLYDm1ESGezrvZabgFJnoQv8Si1nAhVGTk9FkFBesxRzq07dmZYwFCnFSX4xt2fDHs1PMpQbeq83aL/PzLCce3kxbYSB5kQlzGtUYayiYXcu0cVRu228VwBLCD+2wTDDoCmRXtPesgrLKUR4WWWb5N2AqAU1mNDC+UEYsENAerOFXWnmwrcTAu5qyZ7GsBMTpipW4Dbou2yqQ0lpA/aB06n1kz1aL6mNqGPaJ+OqoFuc8Ugdhadd+MmjHfFzoI20SZ3b2geCsUMNCsAd6oXMsZdWm8lzjqCGWHFeol0ik/xHMQvuQkkeCsQ28PBxdnUgf7ZGer+TN+2ZLd2kvTBOk6pIVgy5yC6cZ+o1Tloql9hYGa6rT3xcMbXlW+9e5jM2MWXZliVW3ZhaPjptJFDbIfWxJPjz4QvKyJk0zok4muv13Iiwj2bCyefUTRz6psqI4cGaYm9JpscKO2RCJN8UluYGbbWmYQU+Int6LtZj/lv8p6xnVjWxYI+rBPdtkpfFYRp+MJiXjgPw5B6UGuoruv7+vHjOLHOotRo+RdjZt7NqL9dAJnl1Qb2jfW6+d7NYQSI/bAwxO0sk4taQIT6Gsu/8kfZOPC2xk9rphGqCSS/4q3Os0MMjA1bcJLyoWLp13pqhK6bmiiHw0BBXH4fbEp4xjSbpPx4tHXzbdn8oDsHKZkWh3pPC2J/nVl0k/yF1KDVowVtMDXE47k6TGVcBoqe8PDXCG9+vjRpzIidqNo5qebaUZu6riWMWzldz8x3Z/jLWXuDiM7/Yscn0Z2GIlfoeyz+GwP2eTdOw9EUedHjEQuJY32bq8LICimJ4Ht+zMJKUyhwVQyAER8byzQBwTYmYP5U0wdsyIFitphw+/IH8+v08Ia1iBLPQAeAvRfTTIFLCs8foyUrj5Zv2B/wTYIZy6ioUM+qADeXyo45uBLLqkN90Rf6kiTqDld78NxwsfyR5MxtJLVDFkmf2IMMJHTqSfhbi+7QJaC11OOUJTD0v9wo0X/oO5GvZhe0ZaGHnm9zqTopALuFEAxcaQlc4R81wjC4wrIrqWnbcl2dxiBtD73KW+wcC9ymsLf4I8BEmiN25lx/OUc1IHNyXZJYSFkEfaxCEZWKcnbiyf5sqFSSlEqZLc4lUPJFAoP6s1FHVcyO0odWqdadhRZLZC9RCzQgPlMRtji/OXy5phh7diOBZv5UYp5nb+MZ2NAB/eFXm2JLguxjvEstuvTDmZDUb6Uqv++RdhO5gvKf/AcwU38ifaHQ9uvRuDocYwVxZS2nr9rOwZ8nAh+P2o4e0tEXjxFKQGhxXYkn75H3hhfnFYjik/2qunHBBZfcdG148MaNP6DjX33M238T9Zw/GyGx00JMogr2pdP4JAErv9a5yt4YR41KGf8guSOUbOXVARw6+ybh7+meb7w4BeTlj3aZkv8tVGdfIt3lrwVnlbzhLjeQY6PplKp3/a5Kr5yM0T4wJoKQQ6v3vSNmrhpbuAtKxpMILe8CQoo=" - organicos_navez_osorio_base64_cer = "MIIF1DCCA7ygAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MzkwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTI1NTE2WhcNMjcwNTE4MTI1NTE2WjCB+zEzMDEGA1UEAxQqT1JHQU5JQ09TINFBVkVaIE9TT1JJTyBTLkEgREUgQy5WIFNBIERFIENWMTMwMQYDVQQpFCpPUkdBTklDT1Mg0UFWRVogT1NPUklPIFMuQSBERSBDLlYgU0EgREUgQ1YxMzAxBgNVBAoUKk9SR0FOSUNPUyDRQVZFWiBPU09SSU8gUy5BIERFIEMuViBTQSBERSBDVjElMCMGA1UELRQcT9FPMTIwNzI2UlgzIC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlAF4PoRqITQAEjFBzzfiT/NSN2yvb7Iv1ZMe4qD7tBxBxazRCx+GnimfpR+eaM744RlRDUj+hZfWcsOMn+q65UEIP+Xq5V1NbO1LZDse9uG1fLLSmptfKjyfvTtmBNYBjC3G6YmRv5qVw81CIS4aQOSMXKD+lrxjmRUhV9EAtXVoqGxvyDKeeX4caKuRz8mlrnR8/SMbnpobe5BNoXPrpDbEypemiJXe40pjsltY0RV3b0W0JtJQABUwZ9xn0lPYHY2q7IxYfohibv+o9ldXOXY6tivBZFfbGQSUp7CevC55+Y6uqh35Pi1o0nt/vBVgUOVPNM8d4TvGbXsE0G2J7QIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFp52XykMXfFUtjQqA2zzLPrPIDSMEpkm1vWY0qfz2gC2TlVpbDCWH2vFHpP8D14OifXOmYkws2cvLyE0uBN6se4zXxVHBpTEq+93rvu/tjvMU6r7DISDwB0EX5kmKIFcOugET3/Eq1mxZ6mrI0K26RaEUz+HVyR0EQ2Ll5CLExDkPYV/am0gynhn6QPkxPNbcbm77PEIbH7zc+t7ZB5sgQ6LnubgnKNZDn8bNhkuM1jqFkh7h0owhlJrOvATgrDSLnrot8FoLFkrWQD4uA5udGRwXn5QWx0QM5ScNiSgSRilSFEyXn6rH/CJLO05Sx5OwJJTaxFbAyOXnoNdPMzbQAziaW78478nCNZVSrKWpjwWpScirtM2zcQ9fywd/a3CG66Ff29zasfhHJCp29TIjj1OURp6l1CKc16+UxjuVJ1z5Xh7v3s8S2gtmuYP1sUXPvAEYuVp9CFW87QVMtl3+nGlyJEzSAW/yaps9ua5RmyJK0Mjk1zyXjOJoIY75CIOMN8oqVAxmLJg5XftXJSekGpxybw9aq9qOJdmxVcZoAFaYg4MAdKViBoYxfWfEm4q/ihRz4asnzLp9NJWTXN1YH94rJrK7JSEq820flgr1kiL7z7n1rgWMvhJH9nHriG3yRkno/8OdLJxOSXd7MKZfZx0EWDX8toqWyE7zia8aPM=" - organicos_navez_osorio_base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS8AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucRFLOMmsAaFFEdAecnfgJf0IlyJpvyNOGiSwXgY6uZtS0QJmmupWTlQATxbN4xeN7csx7yCMYxMiWXLyTbjVIWzzsFVKHbsxCudz6UDqMZ3aXEEPDDbPECXJC4FxqzuUgifN4QQuIvxfPbk23m3Vtqu9lr/xMrDNqLZ4RiqY2062kgQzGzekq8CSC97qBAbb8SFMgakFjeHN0JiTGaTpYCpGbu4d+i3ZrQ0mlYkxesdvCLqlCwVM0RTMJsNQ8vpBpRDzH372iOTLCO/gXtV8pEsxpUzG9LSUBo7xSMd1/lcfdyqVgnScgUm8/+toxk6uwZkUMWWvp7tqrMYQFYdR5CjiZjgAWrNorgMmawBqkJU6KQO/CpXVn99U1fANPfQoeyQMgLt35k0JKynG8MuWsgb4EG9Z6sRmOsCQQDDMKwhBjqcbEwN2dL4f1HyN8wklFCyYy6j1NTKU2AjRMXVu4+OlAp5jpjgv08RQxEkW/tNMSSBcpvOzNr64u0M692VA2fThR3UMQ/MZ2yVM6yY3GgIu2tJmg08lhmkoLpWZIMy7bZjj/AEbi7B3wSF4vDYZJcr/Djeezm3MMSghoiOIRSqtBjwf7ZjhA2ymdCsrzy7XSMVekT0y1S+ew1WhnzUNKQSucb6V2yRwNbm0EyeEuvVyHgiGEzCrzNbNHCfoFr69YCUi8itiDfiV7/p7LJzD8J/w85nmOkI/9p+aZ2EyaOdThqBmN4CtoDi5ixz/1EElLn7KVI4d/DZsZ4ZMu76kLAy94o0m6ORSbHX5hw12+P5DgGaLu/Dxd9cctRCkvcUdagiECuKGLJpxTJvEBQoZqUB8AJFgwKcNLl3Z5KAWL5hV0t1h8i3N4HllygqpfUSQMLWCtlGwdI4XGlGI5CmnjrL2Uj8sj9C0zSNqZVnAXFMV9f2ND9W6YJqfU89BQ6Y4QQRMGjXcVF7c78bn5r6zI+Qv2QKm3YiGCfuIa64B+PB/BdithpOuBPn5X5Zxc8ju/kYjJk7sau7VtKJseGOJ1bqOq99VzaxoHjzoJgthLHtni9WtGAnnQy7GMWGW4Un2yObHCxvQxx/rIZEaQiCGfRXOcZIZuXBe5xeHJFGrekDxu3YyumEnLWvsirDF3qhpUtxqvbkTuZw2xT3vTR+oWZpSEnYTd3k/09Eb0ovOPLkbhvcvCEeoI91EJvU+KI4Lm7ZsuTUSpECrHiS3uPOjboCigOWGayKzUHUICNrGK0zxgZXhhl6V7y9pImRl34ID/tZhr3veW4pQKgscv6sQjGJzaph2oCP7uZC6arGWcFpc2pgfBcobmOXYPWKskU3eWKClHBJnJ8MoOru+ObOb+izPhINHOmzP26TnKzFxdZiL+onxjadPYslcLtqlmOYpb/5hHgGOvitLhCLHCp0gYNB2uzj0sVxNs3k7k43KrlO5L6gp1KVaIw2a1yZzOCqDWWcePfKM3Mii9JdVyfHZLRRjFCQiOYo41AltHU+9IcaoT4J/j7pKw5tnlu2VaMlnN0dISpoq/ak0m4YjTd3XdRQeH9ktWmclkc65LdLKf9hIqjVqvOhQUJYkuT7OPgr+o7Z9BnClXMz1/CYWftwQE=" - password = "12345678a" - payroll_invoice = Invoice( - version_code="4.0", - series="F", - date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), - payment_method_code="PUE", - currency_code="MXN", - type_code="N", # N = Nomina - expedition_zip_code="20000", - export_code="01", - issuer=InvoiceIssuer( - tin="EKU9003173C9", - legal_name="ESCUELA KEMPER URGATE", - tax_regime_code="601", - employer_data=InvoiceIssuerEmployerData( - employer_registration="B5510768108" - ), - tax_credentials=[ - TaxCredential( - base64_file=escuela_kemper_urgate_base64_cer, - file_type=0, # Certificado - password="12345678a" - ), - TaxCredential( - base64_file=escuela_kemper_urgate_base64_key, - file_type=1, # Llave privada - password="12345678a" + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="FUNK671228PH6", + legal_name="KARLA FUENTE NOLASCO", + zip_code="01160", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101MNEXXXA8", + social_security_number="04078873454", + labor_relation_start_date="2024-08-18", + seniority="P54W", + sat_contract_type_id="01", + sat_tax_regime_type_id="02", + employee_number="123456789", + department="GenAI", + position="Sr Software Engineer", + sat_job_risk_id="1", + sat_payment_periodicity_id="05", + sat_bank_id="012", + base_salary_for_contributions=Decimal("2828.50"), + integrated_daily_salary=Decimal("0.00"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2025-08-30", + initial_payment_date="2025-07-31", + final_payment_date="2025-08-30", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="1003", + concept="Sueldo Nominal", + taxed_amount=Decimal("95030.00"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="005", + code="5913", + concept="Fondo de Ahorro Aportacion Patron", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("4412.46") + ), + PayrollEarning( + earning_type_code="038", + code="1885", + concept="Bono Ingles", + taxed_amount=Decimal("14254.50"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="029", + code="1941", + concept="Vales Despensa", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("3439.00") + ), + PayrollEarning( + earning_type_code="038", + code="1824", + concept="Herramientas Teletrabajo (telecom y prop. electri)", + taxed_amount=Decimal("273.00"), + exempt_amount=Decimal("0.00") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="002", + code="5050", + concept="Exceso de subsidio al empleo", + amount=Decimal("0.00"), + subsidy_caused=Decimal("0.00") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="002", + code="5003", + concept="ISR Causado", + amount=Decimal("27645.52") + ), + PayrollDeduction( + deduction_type_code="004", + code="5910", + concept="Fondo de ahorro Empleado Inversion", + amount=Decimal("4412.46") + ), + PayrollDeduction( + deduction_type_code="004", + code="5914", + concept="Fondo de Ahorro Patron Inversion", + amount=Decimal("4412.46") + ), + PayrollDeduction( + deduction_type_code="004", + code="1966", + concept="Contribucion poliza exceso GMM", + amount=Decimal("519.91") + ), + PayrollDeduction( + deduction_type_code="004", + code="1934", + concept="Descuento Vales Despensa", + amount=Decimal("1.00") + ), + PayrollDeduction( + deduction_type_code="004", + code="1942", + concept="Vales Despensa Electronico", + amount=Decimal("3439.00") + ), + PayrollDeduction( + deduction_type_code="001", + code="1895", + concept="IMSS", + amount=Decimal("2391.13") ) ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 2. NOMINA ASIMILADOS +# ============================================================================ +def create_nomina_asimilados(): + """ + Crea una factura de nomina para asimilados a salarios. + """ + print("\n" + "="*60) + print("2. NOMINA ASIMILADOS") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="06880", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + origin_employer_tin="EKU9003173C9" ), - recipient=InvoiceRecipient( - tin="FUNK671228PH6", - legal_name="KARLA FUENTE NOLASCO", - zip_code="01160", - tax_regime_code="605", - cfdi_use_code="CN01", # Nomina - employee_data=InvoiceRecipientEmployeeData( - curp="XEXX010101MNEXXXA8", - social_security_number="04078873454", - labor_relation_start_date="2024-08-18", - seniority="P54W", # ISO 8601 duration (54 weeks) - sat_contract_type_id="01", - sat_tax_regime_type_id="02", - employee_number="123456789", - department="GenAI", - position="Sr Software Engineer", - sat_job_risk_id="1", - sat_payment_periodicity_id="05", - sat_bank_id="012", - base_salary_for_contributions=Decimal("2828.50"), - integrated_daily_salary=Decimal("0.00"), - sat_payroll_state_id="JAL" + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password ) + ] + ), + recipient=InvoiceRecipient( + tin="CACX7605101P8", + legal_name="XOCHILT CASAS CHAVEZ", + zip_code="36257", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + sat_contract_type_id="09", + sat_unionized_status_id="No", + sat_tax_regime_type_id="09", + employee_number="00002", + department="ADMINISTRACION", + position="DIRECTOR DE ADMINISTRACION", + sat_payment_periodicity_id="99", + sat_bank_id="012", + bank_account="1111111111", + sat_payroll_state_id="CMX" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="E", + payment_date="2023-06-02T00:00:00", + initial_payment_date="2023-06-01T00:00:00", + final_payment_date="2023-06-02T00:00:00", + days_paid=Decimal("1"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="046", + code="010046", + concept="INGRESOS ASIMILADOS A SALARIOS", + taxed_amount=Decimal("111197.73"), + exempt_amount=Decimal("0.00") + ) + ], + other_payments=[] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="002", + code="020002", + concept="ISR", + amount=Decimal("36197.73") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 3. NOMINA CON BONOS, FONDO AHORRO Y DEDUCCIONES +# ============================================================================ +def create_nomina_bonos_fondo_ahorro(): + """ + Crea una factura de nomina con bonos, fondo de ahorro y multiples deducciones. + """ + print("\n" + "="*60) + print("3. NOMINA CON BONOS, FONDO AHORRO Y DEDUCCIONES") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="Z0000001234" ), - complement=InvoiceComplement( - payroll=PayrollComplement( - version="1.2", - payroll_type_code="O", # O = Ordinaria - payment_date="2025-08-30", - initial_payment_date="2025-07-31", - final_payment_date="2025-08-30", - days_paid=Decimal("30"), - earnings=PayrollEarningsComplement( - earnings=[ - PayrollEarning( - earning_type_code="001", - code="1003", - concept="Sueldo Nominal", - taxed_amount=Decimal("95030.00"), - exempt_amount=Decimal("0.00") - ), - PayrollEarning( - earning_type_code="005", - code="5913", - concept="Fondo de Ahorro Aportacion Patron", - taxed_amount=Decimal("0.00"), - exempt_amount=Decimal("4412.46") - ), - PayrollEarning( - earning_type_code="038", - code="1885", - concept="Bono Ingles", - taxed_amount=Decimal("14254.50"), - exempt_amount=Decimal("0.00") - ), - PayrollEarning( - earning_type_code="029", - code="1941", - concept="Vales Despensa", - taxed_amount=Decimal("0.00"), - exempt_amount=Decimal("3439.00") - ), - PayrollEarning( - earning_type_code="038", - code="1824", - concept="Herramientas Teletrabajo (telecom y prop. electri)", - taxed_amount=Decimal("273.00"), - exempt_amount=Decimal("0.00") - ) - ], - other_payments=[ - PayrollOtherPayment( - other_payment_type_code="002", - code="5050", - concept="Exceso de subsidio al empleo", - amount=Decimal("0.00"), - subsidy_caused=Decimal("0.00") + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101MNEXXXA8", + social_security_number="0000000000", + labor_relation_start_date="2022-03-02T00:00:00", + seniority="P66W", + sat_contract_type_id="01", + sat_unionized_status_id="No", + sat_tax_regime_type_id="02", + employee_number="111111", + sat_job_risk_id="4", + sat_payment_periodicity_id="02", + integrated_daily_salary=Decimal("180.96"), + sat_payroll_state_id="GUA" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-06-11T00:00:00", + initial_payment_date="2023-06-05T00:00:00", + final_payment_date="2023-06-11T00:00:00", + days_paid=Decimal("7"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="SP01", + concept="SUELDO", + taxed_amount=Decimal("1210.30"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="010", + code="SP02", + concept="PREMIO PUNTUALIDAD", + taxed_amount=Decimal("121.03"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="029", + code="SP03", + concept="MONEDERO ELECTRONICO", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("269.43") + ), + PayrollEarning( + earning_type_code="010", + code="SP04", + concept="PREMIO DE ASISTENCIA", + taxed_amount=Decimal("121.03"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="005", + code="SP54", + concept="APORTACION FONDO AHORRO", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("121.03") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="002", + code="ISRSUB", + concept="Subsidio ISR para empleo", + amount=Decimal("0.0"), + subsidy_caused=Decimal("0.0"), + balance_compensation=PayrollBalanceCompensation( + favorable_balance=Decimal("0.0"), + year=2022, + remaining_favorable_balance=Decimal("0.0") ) - ] - ), - deductions=[ - PayrollDeduction( - deduction_type_code="002", - code="5003", - concept="ISR Causado", - amount=Decimal("27645.52") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="004", + code="ZA09", + concept="APORTACION FONDO AHORRO", + amount=Decimal("121.03") + ), + PayrollDeduction( + deduction_type_code="002", + code="ISR", + concept="ISR", + amount=Decimal("36.57") + ), + PayrollDeduction( + deduction_type_code="001", + code="IMSS", + concept="Cuota de Seguridad Social EE", + amount=Decimal("30.08") + ), + PayrollDeduction( + deduction_type_code="004", + code="ZA68", + concept="DEDUCCION FDO AHORRO PAT", + amount=Decimal("121.03") + ), + PayrollDeduction( + deduction_type_code="018", + code="ZA11", + concept="APORTACION CAJA AHORRO", + amount=Decimal("300.00") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 4. NOMINA CON HORAS EXTRA +# ============================================================================ +def create_nomina_horas_extra(): + """ + Crea una factura de nomina con horas extra. + """ + print("\n" + "="*60) + print("4. NOMINA CON HORAS EXTRA") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01", + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ), + PayrollEarning( + earning_type_code="019", + code="00100", + concept="Horas Extra", + taxed_amount=Decimal("50.00"), + exempt_amount=Decimal("50.00"), + overtime=[ + PayrollOvertime( + days=1, + hours_type_code="01", + extra_hours=2, + amount_paid=Decimal("100.00") + ) + ] + ) + ], + other_payments=[] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="001", + code="00301", + concept="Seguridad Social", + amount=Decimal("200") + ), + PayrollDeduction( + deduction_type_code="002", + code="00302", + concept="ISR", + amount=Decimal("100") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 5. NOMINA CON INCAPACIDADES +# ============================================================================ +def create_nomina_incapacidades(): + """ + Crea una factura de nomina con incapacidades. + """ + print("\n" + "="*60) + print("5. NOMINA CON INCAPACIDADES") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01T00:00:00", + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="001", + code="00301", + concept="Seguridad Social", + amount=Decimal("200") + ), + PayrollDeduction( + deduction_type_code="002", + code="00302", + concept="ISR", + amount=Decimal("100") + ) + ], + disabilities=[ + PayrollDisability( + disability_days=1, + disability_type_code="01" + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 6. NOMINA CON SNCF (Sistema Nacional de Coordinacion Fiscal) +# ============================================================================ +def create_nomina_sncf(): + """ + Crea una factura de nomina con SNCF (para organismos publicos). + Usa los certificados de Organicos Navez Osorio. + """ + print("\n" + "="*60) + print("6. NOMINA CON SNCF") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="39074", + export_code="01", + issuer=InvoiceIssuer( + tin="OÑO120726RX3", + legal_name="ORGANICOS ÑAVEZ OSORIO", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="27112029", + sat_fund_source_id="IP" + ), + tax_credentials=[ + TaxCredential( + base64_file=organicos_navez_osorio_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=organicos_navez_osorio_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="CACX7605101P8", + legal_name="XOCHILT CASAS CHAVEZ", + zip_code="36257", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="80997742673", + labor_relation_start_date="2021-09-01", + seniority="P88W", + sat_contract_type_id="01", + sat_tax_regime_type_id="02", + employee_number="273", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + integrated_daily_salary=Decimal("221.48"), + sat_payroll_state_id="GRO" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-16T00:00:00", + initial_payment_date="2023-05-01T00:00:00", + final_payment_date="2023-05-16T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="P001", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("3322.20"), + exempt_amount=Decimal("0.0") ), - PayrollDeduction( - deduction_type_code="004", - code="5910", - concept="Fondo de ahorro Empleado Inversion", - amount=Decimal("4412.46") + PayrollEarning( + earning_type_code="038", + code="P540", + concept="Compensacion", + taxed_amount=Decimal("100.0"), + exempt_amount=Decimal("0.0") ), - PayrollDeduction( - deduction_type_code="004", - code="5914", - concept="Fondo de Ahorro Patron Inversion", - amount=Decimal("4412.46") + PayrollEarning( + earning_type_code="038", + code="P550", + concept="Compensacion Garantizada Extraordinaria", + taxed_amount=Decimal("2200.0"), + exempt_amount=Decimal("0.0") ), - PayrollDeduction( - deduction_type_code="004", - code="1966", - concept="Contribucion poliza exceso GMM", - amount=Decimal("519.91") + PayrollEarning( + earning_type_code="038", + code="P530", + concept="Servicio Extraordinario", + taxed_amount=Decimal("200.0"), + exempt_amount=Decimal("0.0") ), - PayrollDeduction( - deduction_type_code="004", - code="1934", - concept="Descuento Vales Despensa", - amount=Decimal("1.00") + PayrollEarning( + earning_type_code="001", + code="P506", + concept="Otras Prestaciones", + taxed_amount=Decimal("1500.0"), + exempt_amount=Decimal("0.0") ), - PayrollDeduction( - deduction_type_code="004", - code="1942", - concept="Vales Despensa Electronico", - amount=Decimal("3439.00") + PayrollEarning( + earning_type_code="001", + code="P505", + concept="Remuneracion al Desempeno Legislativo", + taxed_amount=Decimal("17500.0"), + exempt_amount=Decimal("0.0") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="002", + code="o002", + concept="Subsidio para el empleo efectivamente entregado al trabajador", + amount=Decimal("0.0"), + subsidy_caused=Decimal("0.0") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="002", + code="D002", + concept="ISR", + amount=Decimal("4716.61") + ), + PayrollDeduction( + deduction_type_code="004", + code="D525", + concept="Redondeo", + amount=Decimal("0.81") + ), + PayrollDeduction( + deduction_type_code="001", + code="D510", + concept="Cuota Trabajador ISSSTE", + amount=Decimal("126.78") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 7. NOMINA EXTRAORDINARIA +# ============================================================================ +def create_nomina_extraordinaria(): + """ + Crea una factura de nomina extraordinaria (ej. aguinaldo). + """ + print("\n" + "="*60) + print("7. NOMINA EXTRAORDINARIA") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01", + seniority="P439W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="99", + sat_bank_id="002", + bank_account="1111111111", + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="E", + payment_date="2023-06-04T00:00:00", + initial_payment_date="2023-06-04T00:00:00", + final_payment_date="2023-06-04T00:00:00", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="002", + code="00500", + concept="Gratificacion Anual (Aguinaldo)", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("10000.00") + ) + ], + other_payments=[] + ), + deductions=[] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 8. NOMINA SEPARACION INDEMNIZACION +# ============================================================================ +def create_nomina_separacion_indemnizacion(): + """ + Crea una factura de nomina por separacion e indemnizacion. + """ + print("\n" + "="*60) + print("8. NOMINA SEPARACION INDEMNIZACION") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01", + seniority="P439W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="99", + sat_bank_id="002", + bank_account="1111111111", + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="E", + payment_date="2023-06-04T00:00:00", + initial_payment_date="2023-05-05T00:00:00", + final_payment_date="2023-06-04T00:00:00", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="023", + code="00500", + concept="Pagos por separacion", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("10000.00") ), - PayrollDeduction( - deduction_type_code="001", - code="1895", - concept="IMSS", - amount=Decimal("2391.13") + PayrollEarning( + earning_type_code="025", + code="00900", + concept="Indemnizaciones", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("500.00") + ) + ], + other_payments=[], + severance=PayrollSeverance( + total_paid=Decimal("10500.00"), + years_of_service=1, + last_monthly_salary=Decimal("10000.00"), + accumulable_income=Decimal("10000.00"), + non_accumulable_income=Decimal("0.00") + ) + ), + deductions=[] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 9. NOMINA JUBILACION PENSION RETIRO +# ============================================================================ +def create_nomina_jubilacion_pension_retiro(): + """ + Crea una factura de nomina por jubilacion, pension o retiro. + """ + print("\n" + "="*60) + print("9. NOMINA JUBILACION PENSION RETIRO") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01", + seniority="P439W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="99", + sat_bank_id="002", + bank_account="1111111111", + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="E", + payment_date="2023-05-05T00:00:00", + initial_payment_date="2023-06-04T00:00:00", + final_payment_date="2023-06-04T00:00:00", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="039", + code="00500", + concept="Jubilaciones, pensiones o haberes de retiro", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("10000.00") + ) + ], + retirement=PayrollRetirement( + total_one_time=Decimal("10000.00"), + accumulable_income=Decimal("10000.00"), + non_accumulable_income=Decimal("0.00") + ) + ), + deductions=[] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 10. NOMINA SIN DEDUCCIONES +# ============================================================================ +def create_nomina_sin_deducciones(): + """ + Crea una factura de nomina sin deducciones. + """ + print("\n" + "="*60) + print("10. NOMINA SIN DEDUCCIONES") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01", + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ) + ], + other_payments=[] + ), + deductions=[] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 11. NOMINA SUBSIDIO CAUSADO AL EMPLEO +# ============================================================================ +def create_nomina_subsidio_causado(): + """ + Crea una factura de nomina con subsidio causado al empleo. + """ + print("\n" + "="*60) + print("11. NOMINA SUBSIDIO CAUSADO AL EMPLEO") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01T00:00:00", + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="02", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="007", + code="0002", + concept="ISR ajustado por subsidio", + amount=Decimal("145.80"), + subsidy_caused=Decimal("0.0") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="107", + code="D002", + concept="Ajuste al Subsidio Causado", + amount=Decimal("160.35") + ), + PayrollDeduction( + deduction_type_code="002", + code="D002", + concept="ISR", + amount=Decimal("145.80") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 12. NOMINA VIATICOS +# ============================================================================ +def create_nomina_viaticos(): + """ + Crea una factura de nomina con viaticos. + """ + print("\n" + "="*60) + print("12. NOMINA VIATICOS") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01T00:00:00", + seniority="P438W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-09-26T00:00:00", + initial_payment_date="2023-09-11T00:00:00", + final_payment_date="2023-09-26T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="050", + code="050", + concept="Viaticos", + taxed_amount=Decimal("0"), + exempt_amount=Decimal("3000") ) ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="081", + code="081", + concept="Ajuste en viaticos entregados al trabajador", + amount=Decimal("3000") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 13. NOMINA BASICA +# ============================================================================ +def create_nomina_basica(): + """ + Crea una factura de nomina basica con sueldo y deducciones de seguridad social e ISR. + """ + print("\n" + "="*60) + print("13. NOMINA BASICA") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-18T18:04:06", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + tax_regime_code="601", + employer_data=InvoiceIssuerEmployerData( + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ), + tax_credentials=[ + TaxCredential( + base64_file=escuela_kemper_urgate_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=escuela_kemper_urgate_base64_key, + file_type=1, + password=password ) + ] + ), + recipient=InvoiceRecipient( + tin="XOJI740919U48", + legal_name="INGRID XODAR JIMENEZ", + zip_code="76028", + tax_regime_code="605", + cfdi_use_code="CN01", + employee_data=InvoiceRecipientEmployeeData( + curp="XEXX010101HNEXXXA4", + social_security_number="000000", + labor_relation_start_date="2015-01-01T00:00:00", + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ) + ], + other_payments=[] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="001", + code="00301", + concept="Seguridad Social", + amount=Decimal("200") + ), + PayrollDeduction( + deduction_type_code="002", + code="00302", + concept="ISR", + amount=Decimal("100") + ) + ] ) ) + ) api_response = client.invoices.create(payroll_invoice) - print(api_response) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# FUNCION PRINCIPAL +# ============================================================================ +def main(): + """ + Funcion principal que ejecuta todos los ejemplos de factura de nomina. + Descomenta las funciones que desees ejecutar. + """ + print("="*60) + print("EJEMPLOS DE FACTURA DE NOMINA - FISCALAPI PYTHON SDK") + print("="*60) + + # Ejecutar todos los ejemplos uno por uno + examples = [ + create_nomina_ordinaria, + create_nomina_asimilados, + create_nomina_bonos_fondo_ahorro, + create_nomina_horas_extra, + create_nomina_incapacidades, + create_nomina_sncf, + create_nomina_extraordinaria, + create_nomina_separacion_indemnizacion, + create_nomina_jubilacion_pension_retiro, + create_nomina_sin_deducciones, + create_nomina_subsidio_causado, + create_nomina_viaticos, + create_nomina_basica, + ] + + results = [] + for example in examples: + try: + response = example() + success = response.succeeded if response else False + results.append((example.__name__, success, None)) + except Exception as e: + results.append((example.__name__, False, str(e))) + + # Resumen de resultados + print("\n" + "="*60) + print("RESUMEN DE RESULTADOS") + print("="*60) + for name, success, error in results: + status = "OK" if success else "FAILED" + print(f"{name}: {status}") + if error: + print(f" Error: {error}") + + print("\n" + "="*60) + print("FIN DE LOS EJEMPLOS") + print("="*60) + if __name__ == "__main__": main() From bbe688632aa3e83d8d943804b2be4f2fdc4b2379 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Tue, 20 Jan 2026 20:13:30 -0600 Subject: [PATCH 19/28] Add payment complement invoice examples and rename nomina examples file - Add facturas-de-complemento-pago.py with examples for payment complement invoices - Por valores: inline issuer/recipient data with tax credentials - Por referencias: using pre-created issuer/recipient IDs - Uses correct structure: complement.payment (PaymentComplement) instead of deprecated payments field - Rename ejemplos-nomina.py to facturas-de-nomina.py for consistency Co-Authored-By: Claude Opus 4.5 --- facturas-de-complemento-pago.py | 257 ++++++++++++++++++++ ejemplos-nomina.py => facturas-de-nomina.py | 4 +- 2 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 facturas-de-complemento-pago.py rename ejemplos-nomina.py => facturas-de-nomina.py (99%) diff --git a/facturas-de-complemento-pago.py b/facturas-de-complemento-pago.py new file mode 100644 index 0000000..9b67e51 --- /dev/null +++ b/facturas-de-complemento-pago.py @@ -0,0 +1,257 @@ +""" +Ejemplos de Factura de Complemento de Pago usando el SDK de FiscalAPI para Python. + +Este archivo contiene ejemplos de diferentes tipos de facturas de complemento de pago: +1. Complemento de Pago por Valores (todos los datos inline) +2. Complemento de Pago por Referencias (usando IDs de emisor y receptor) +""" + +from datetime import datetime +from decimal import Decimal +from fiscalapi.models.common_models import FiscalApiSettings +from fiscalapi.models.fiscalapi_models import ( + Invoice, + InvoiceItem, + InvoiceIssuer, + InvoiceRecipient, + InvoiceComplement, + PaymentComplement, + PaidInvoice, + PaidInvoiceTax, + TaxCredential +) +from fiscalapi.services.fiscalapi_client import FiscalApiClient + + +# ============================================================================ +# CONFIGURACION +# ============================================================================ + +# Configuracion del cliente +settings = FiscalApiSettings( + # api_url="https://test.fiscalapi.com", + # api_key="", + # tenant="", + +) + +client = FiscalApiClient(settings=settings) + +# Certificados en base64 +karla_fuente_nolasco_base64_cer = "MIIFgDCCA2igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0NDYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTQzNTM3WhcNMjcwNTE4MTQzNTM3WjCBpzEdMBsGA1UEAxMUS0FSTEEgRlVFTlRFIE5PTEFTQ08xHTAbBgNVBCkTFEtBUkxBIEZVRU5URSBOT0xBU0NPMR0wGwYDVQQKExRLQVJMQSBGVUVOVEUgTk9MQVNDTzEWMBQGA1UELRMNRlVOSzY3MTIyOFBINjEbMBkGA1UEBRMSRlVOSzY3MTIyOE1DTE5MUjA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhNXbTSqGX6+/3Urpemyy5vVG2IdP2v7v001+c4BoMxEDFDQ32cOFdDiRxy0Fq9aR+Ojrofq8VeftvN586iyA1A6a0QnA68i7JnQKI4uJy+u0qiixuHu6u3b3BhSpoaVHcUtqFWLLlzr0yBxfVLOqVna/1/tHbQJg9hx57mp97P0JmXO1WeIqi+Zqob/mVZh2lsPGdJ8iqgjYFaFn9QVOQ1Pq74o1PTqwfzqgJSfV0zOOlESDPWggaDAYE4VNyTBisOUjlNd0x7ppcTxSi3yenrJHqkq/pqJsRLKf6VJ/s9p6bsd2bj07hSDpjlDC2lB25eEfkEkeMkXoE7ErXQ5QCwIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAHwYpgbClHULXYhK4GNTgonvXh81oqfXwCSWAyDPiTYFDWVfWM9C4ApxMLyc0XvJte75Rla+bPC08oYN3OlhbbvP3twBL/w9SsfxvkbpFn2ZfGSTXZhyiq4vjmQHW1pnFvGelwgU4v3eeRE/MjoCnE7M/Q5thpuog6WGf7CbKERnWZn8QsUaJsZSEkg6Bv2jm69ye57ab5rrOUaeMlstTfdlaHAEkUgLX/NXq7RbGwv82hkHY5b2vYcXeh34tUMBL6os3OdRlooN9ZQGkVIISvxVZpSHkYC20DFNh1Bb0ovjfujlTcka81GnbUhFGZtRuoVQ1RVpMO8xtx3YKBLp4do3hPmnRCV5hCm43OIjYx9Ov2dqICV3AaNXSLV1dW39Bak/RBiIDGHzOIW2+VMPjvvypBjmPv/tmbqNHWPSAWOxTyMx6E1gFCZvi+5F+BgkdC3Lm7U0BU0NfvsXajZd8sXnIllvEMrikCLoI/yurvexNDcF1RW/FhMsoua0eerwczcNm66pGjHm05p9DR6lFeJZrtqeqZuojdxBWy4vH6ghyJaupergoX+nmdG3JYeRttCFF/ITI68TeCES5V3Y0C3psYAg1XxcGRLGd4chPo/4xwiLkijWtgt0/to5ljGBwfK7r62PHZfL1Dp+i7V3w7hmOlhbXzP+zhMZn1GCk7KY=" +karla_fuente_nolasco_base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS9AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucRBAKNQXH8t8gVCl/ItHMI2hMJ76QOECOqEi1Y89cDpegDvh/INXyMsXbzi87tfFzgq1O+9ID6aPWGg+bNGADXyXxDVdy7Nq/SCdoXvo66MTYwq8jyJeUHDHEGMVBcmZpD44VJCvLBxDcvByuevP4Wo2NKqJCwK+ecAdZc/8Rvd947SjbMHuS8BppfQWARVUqA5BLOkTAHNv6tEk/hncC7O2YOGSShart8fM8dokgGSyewHVFe08POuQ+WDHeVpvApH/SP29rwktSoiHRoL6dK+F2YeEB5SuFW9LQgYCutjapmUP/9TC3Byro9Li6UrvQHxNmgMFGQJSYjFdqlGjLibfuguLp7pueutbROoZaSxU8HqlfYxLkpJUxUwNI1ja/1t3wcivtWknVXBd13R06iVfU1HGe8Kb4u5il4a4yP4p7VT4RE3b1SBLJeG+BxHiE8gFaaKcX/Cl6JV14RPTvk/6VnAtEQ66qHJex21KKuiJo2JoOmDXVHmvGQlWXNjYgoPx28Xd5WsofL+n7HDR2Ku8XgwJw6IXBJGuoday9qWN9v/k7DGlNGB6Sm4gdVUmycMP6EGhB1vFTiDfOGQO42ywmcpKoMETPVQ5InYKE0xAOckgcminDgxWjtUHjBDPEKifEjYudPwKmR6Cf4ZdGvUWwY/zq9pPAC9bu423KeBCnSL8AQ4r5SVsW6XG0njamwfNjpegwh/YG7sS7sDtZ8gi7r6tZYjsOqZlCYU0j7QTBpuQn81Yof2nQRCFxhRJCeydmIA8+z0nXrcElk7NDPk4kYQS0VitJ2qeQYNENzGBglROkCl2y6GlxAG80IBtReCUp/xOSdlwDR0eim+SNkdStvmQM5IcWBuDKwGZc1A4v/UoLl7niV9fpl4X6bUX8lZzY4gidJOafoJ30VoY/lYGkrkEuz3GpbbT5v8fF3iXVRlEqhlpe8JSGu7Rd2cPcJSkQ1Cuj/QRhHPhFMF2KhTEf95c9ZBKI8H7SvBi7eLXfSW2Y0ve6vXBZKyjK9whgCU9iVOsJjqRXpAccaWOKi420CjmS0+uwj/Xr2wLZhPEjBA/G6Od30+eG9mICmbp/5wAGhK/ZxCT17ZETyFmOMo49jl9pxdKocJNuzMrLpSz7/g5Jwp8+y8Ck5YP7AX0R/dVA0t37DO7nAbQT5XVSYpMVh/yvpYJ9WR+tb8Yg1h2lERLR2fbuhQRcwmisZR2W3Sr2b7hX9MCMkMQw8y2fDJrzLrqKqkHcjvnI/TdzZW2MzeQDoBBb3fmgvjYg07l4kThS73wGX992w2Y+a1A2iirSmrYEm9dSh16JmXa8boGQAONQzQkHh7vpw0IBs9cnvqO1QLB1GtbBztUBXonA4TxMKLYZkVrrd2RhrYWMsDp7MpC4M0p/DA3E/qscYwq1OpwriewNdx6XXqMZbdUNqMP2viBY2VSGmNdHtVfbN/rnaeJetFGX7XgTVYD7wDq8TW9yseCK944jcT+y/o0YiT9j3OLQ2Ts0LDTQskpJSxRmXEQGy3NBDOYFTvRkcGJEQJItuol8NivJN1H9LoLIUAlAHBZxfHpUYx66YnP4PdTdMIWH+nxyekKPFfAT7olQ=" +password = "12345678a" + + +# ============================================================================ +# 1. COMPLEMENTO DE PAGO POR VALORES +# ============================================================================ +def create_complemento_pago_valores(): + """ + Crea una factura de complemento de pago (factura de pago) por valores. + Incluye todos los datos del emisor, receptor y certificados inline. + """ + print("\n" + "="*60) + print("1. COMPLEMENTO DE PAGO POR VALORES") + print("="*60) + + payment_invoice = Invoice( + version_code="4.0", + series="CP", + date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), + currency_code="XXX", + type_code="P", + expedition_zip_code="01160", + exchange_rate=1, + export_code="01", + issuer=InvoiceIssuer( + tin="FUNK671228PH6", + legal_name="KARLA FUENTE NOLASCO", + tax_regime_code="621", + tax_credentials=[ + TaxCredential( + base64_file=karla_fuente_nolasco_base64_cer, + file_type=0, + password=password + ), + TaxCredential( + base64_file=karla_fuente_nolasco_base64_key, + file_type=1, + password=password + ) + ] + ), + recipient=InvoiceRecipient( + tin="EKU9003173C9", + legal_name="ESCUELA KEMPER URGATE", + zip_code="42501", + tax_regime_code="601", + cfdi_use_code="CP01", + email="mail@domain.com", + ), + items=[ + InvoiceItem( + item_code="84111506", + quantity=1, + unit_of_measurement_code="ACT", + description="Pago", + unit_price=0, + tax_object_code="01" + ) + ], + complement=InvoiceComplement( + payment=PaymentComplement( + payment_date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), + payment_form_code="28", + currency_code="MXN", + exchange_rate=Decimal("1"), + amount=Decimal("11600.00"), + source_bank_tin="BSM970519DU8", + source_bank_account="1234567891012131", + target_bank_tin="BBA830831LJ2", + target_bank_account="1234567890", + paid_invoices=[ + PaidInvoice( + uuid="5C7B0622-01B4-4EB8-96D0-E0DEBD89FF0F", + series="F", + number="123", + currency_code="MXN", + partiality_number=1, + sub_total=Decimal("10000.00"), + previous_balance=Decimal("11600.00"), + payment_amount=Decimal("11600.00"), + remaining_balance=Decimal("0"), + tax_object_code="02", + paid_invoice_taxes=[ + PaidInvoiceTax( + tax_code="002", + tax_type_code="Tasa", + tax_rate=Decimal("0.160000"), + tax_flag_code="T" + ) + ] + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payment_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 2. COMPLEMENTO DE PAGO POR REFERENCIAS +# ============================================================================ +def create_complemento_pago_referencias(): + """ + Crea una factura de complemento de pago (factura de pago) por referencias. + Usa IDs de emisor y receptor previamente creados en el sistema. + No incluye conceptos (items) ya que no son necesarios en este modo. + """ + print("\n" + "="*60) + print("2. COMPLEMENTO DE PAGO POR REFERENCIAS") + print("="*60) + + payment_invoice = Invoice( + version_code="4.0", + series="CP", + date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), + currency_code="XXX", + type_code="P", + expedition_zip_code="01160", + exchange_rate=1, + export_code="01", + issuer=InvoiceIssuer( + id="109f4d94-63ea-4a21-ab15-20c8b87d8ee9" + ), + recipient=InvoiceRecipient( + id="2e7b988f-3a2a-4f67-86e9-3f931dd48581" + ), + complement=InvoiceComplement( + payment=PaymentComplement( + payment_date=datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), + payment_form_code="28", + currency_code="MXN", + exchange_rate=Decimal("1"), + amount=Decimal("11600.00"), + source_bank_tin="BSM970519DU8", + source_bank_account="1234567891012131", + target_bank_tin="BBA830831LJ2", + target_bank_account="1234567890", + paid_invoices=[ + PaidInvoice( + uuid="5C7B0622-01B4-4EB8-96D0-E0DEBD89FF0F", + series="F", + number="123", + currency_code="MXN", + partiality_number=1, + sub_total=Decimal("10000.00"), + previous_balance=Decimal("11600.00"), + payment_amount=Decimal("11600.00"), + remaining_balance=Decimal("0"), + tax_object_code="02", + paid_invoice_taxes=[ + PaidInvoiceTax( + tax_code="002", + tax_type_code="Tasa", + tax_rate=Decimal("0.160000"), + tax_flag_code="T" + ) + ] + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payment_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# FUNCION PRINCIPAL +# ============================================================================ +def main(): + """ + Funcion principal que ejecuta todos los ejemplos de factura de complemento de pago. + Descomenta las funciones que desees ejecutar. + """ + print("="*60) + print("EJEMPLOS DE FACTURA DE COMPLEMENTO DE PAGO - FISCALAPI PYTHON SDK") + print("="*60) + + # Ejecutar todos los ejemplos uno por uno + examples = [ + create_complemento_pago_valores, + create_complemento_pago_referencias, + ] + + results = [] + for example in examples: + try: + response = example() + success = response.succeeded if response else False + results.append((example.__name__, success, None)) + except Exception as e: + results.append((example.__name__, False, str(e))) + + # Resumen de resultados + print("\n" + "="*60) + print("RESUMEN DE RESULTADOS") + print("="*60) + for name, success, error in results: + status = "OK" if success else "FAILED" + print(f"{name}: {status}") + if error: + print(f" Error: {error}") + + print("\n" + "="*60) + print("FIN DE LOS EJEMPLOS") + print("="*60) + + +if __name__ == "__main__": + main() diff --git a/ejemplos-nomina.py b/facturas-de-nomina.py similarity index 99% rename from ejemplos-nomina.py rename to facturas-de-nomina.py index 23a33c6..0ac5a80 100644 --- a/ejemplos-nomina.py +++ b/facturas-de-nomina.py @@ -51,9 +51,7 @@ # api_url="https://test.fiscalapi.com", # api_key="", # tenant="", - api_url="http://localhost:5001", - api_key="sk_development_b470ea83_3c0f_4209_b933_85223b960d91", - tenant="102e5f13-e114-41dd-bea7-507fce177281" + ) client = FiscalApiClient(settings=settings) From b0142758a156f83f5c73e6e307aa601c0d5e990e Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Tue, 20 Jan 2026 20:14:49 -0600 Subject: [PATCH 20/28] payroll by values addded --- ...omplemento-pago.py => ejemplos-facturas-de-complemento-pago.py | 0 facturas-de-nomina.py => ejemplos-facturas-de-nomina.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename facturas-de-complemento-pago.py => ejemplos-facturas-de-complemento-pago.py (100%) rename facturas-de-nomina.py => ejemplos-facturas-de-nomina.py (100%) diff --git a/facturas-de-complemento-pago.py b/ejemplos-facturas-de-complemento-pago.py similarity index 100% rename from facturas-de-complemento-pago.py rename to ejemplos-facturas-de-complemento-pago.py diff --git a/facturas-de-nomina.py b/ejemplos-facturas-de-nomina.py similarity index 100% rename from facturas-de-nomina.py rename to ejemplos-facturas-de-nomina.py From 0ed43532b8a2cf2ff55b5fd841dbd133c95c177d Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 25 Jan 2026 15:38:51 -0600 Subject: [PATCH 21/28] Add payroll invoice examples using references mode for all 13 CFDI nomina types Adds 13 setup methods (_references_setup_data) that configure employee/employer data, and 12 new invoice methods (_references) that create payroll invoices using only person IDs instead of inline data. Also adds curp field to Person model. Co-Authored-By: Claude Opus 4.5 --- ejemplos-facturas-de-nomina.py | 2129 ++++++++++++++++++++++++-- fiscalapi/models/fiscalapi_models.py | 5 +- 2 files changed, 2045 insertions(+), 89 deletions(-) diff --git a/ejemplos-facturas-de-nomina.py b/ejemplos-facturas-de-nomina.py index 0ac5a80..1df5007 100644 --- a/ejemplos-facturas-de-nomina.py +++ b/ejemplos-facturas-de-nomina.py @@ -37,7 +37,10 @@ PayrollSeverance, PayrollRetirement, PayrollBalanceCompensation, - TaxCredential + TaxCredential, + EmployeeData, + EmployerData, + Person ) from fiscalapi.services.fiscalapi_client import FiscalApiClient @@ -51,7 +54,9 @@ # api_url="https://test.fiscalapi.com", # api_key="", # tenant="", - + api_url="http://localhost:5001", + api_key="sk_development_b470ea83_3c0f_4209_b933_85223b960d91", + tenant="102e5f13-e114-41dd-bea7-507fce177281" ) client = FiscalApiClient(settings=settings) @@ -63,11 +68,15 @@ organicos_navez_osorio_base64_key = "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIAgEAAoIBAQACAggAMBQGCCqGSIb3DQMHBAgwggS8AgEAMASCBMh4EHl7aNSCaMDA1VlRoXCZ5UUmqErAbucRFLOMmsAaFFEdAecnfgJf0IlyJpvyNOGiSwXgY6uZtS0QJmmupWTlQATxbN4xeN7csx7yCMYxMiWXLyTbjVIWzzsFVKHbsxCudz6UDqMZ3aXEEPDDbPECXJC4FxqzuUgifN4QQuIvxfPbk23m3Vtqu9lr/xMrDNqLZ4RiqY2062kgQzGzekq8CSC97qBAbb8SFMgakFjeHN0JiTGaTpYCpGbu4d+i3ZrQ0mlYkxesdvCLqlCwVM0RTMJsNQ8vpBpRDzH372iOTLCO/gXtV8pEsxpUzG9LSUBo7xSMd1/lcfdyqVgnScgUm8/+toxk6uwZkUMWWvp7tqrMYQFYdR5CjiZjgAWrNorgMmawBqkJU6KQO/CpXVn99U1fANPfQoeyQMgLt35k0JKynG8MuWsgb4EG9Z6sRmOsCQQDDMKwhBjqcbEwN2dL4f1HyN8wklFCyYy6j1NTKU2AjRMXVu4+OlAp5jpjgv08RQxEkW/tNMSSBcpvOzNr64u0M692VA2fThR3UMQ/MZ2yVM6yY3GgIu2tJmg08lhmkoLpWZIMy7bZjj/AEbi7B3wSF4vDYZJcr/Djeezm3MMSghoiOIRSqtBjwf7ZjhA2ymdCsrzy7XSMVekT0y1S+ew1WhnzUNKQSucb6V2yRwNbm0EyeEuvVyHgiGEzCrzNbNHCfoFr69YCUi8itiDfiV7/p7LJzD8J/w85nmOkI/9p+aZ2EyaOdThqBmN4CtoDi5ixz/1EElLn7KVI4d/DZsZ4ZMu76kLAy94o0m6ORSbHX5hw12+P5DgGaLu/Dxd9cctRCkvcUdagiECuKGLJpxTJvEBQoZqUB8AJFgwKcNLl3Z5KAWL5hV0t1h8i3N4HllygqpfUSQMLWCtlGwdI4XGlGI5CmnjrL2Uj8sj9C0zSNqZVnAXFMV9f2ND9W6YJqfU89BQ6Y4QQRMGjXcVF7c78bn5r6zI+Qv2QKm3YiGCfuIa64B+PB/BdithpOuBPn5X5Zxc8ju/kYjJk7sau7VtKJseGOJ1bqOq99VzaxoHjzoJgthLHtni9WtGAnnQy7GMWGW4Un2yObHCxvQxx/rIZEaQiCGfRXOcZIZuXBe5xeHJFGrekDxu3YyumEnLWvsirDF3qhpUtxqvbkTuZw2xT3vTR+oWZpSEnYTd3k/09Eb0ovOPLkbhvcvCEeoI91EJvU+KI4Lm7ZsuTUSpECrHiS3uPOjboCigOWGayKzUHUICNrGK0zxgZXhhl6V7y9pImRl34ID/tZhr3veW4pQKgscv6sQjGJzaph2oCP7uZC6arGWcFpc2pgfBcobmOXYPWKskU3eWKClHBJnJ8MoOru+ObOb+izPhINHOmzP26TnKzFxdZiL+onxjadPYslcLtqlmOYpb/5hHgGOvitLhCLHCp0gYNB2uzj0sVxNs3k7k43KrlO5L6gp1KVaIw2a1yZzOCqDWWcePfKM3Mii9JdVyfHZLRRjFCQiOYo41AltHU+9IcaoT4J/j7pKw5tnlu2VaMlnN0dISpoq/ak0m4YjTd3XdRQeH9ktWmclkc65LdLKf9hIqjVqvOhQUJYkuT7OPgr+o7Z9BnClXMz1/CYWftwQE=" password = "12345678a" - +escuela_kemper_urgate_id = "2e7b988f-3a2a-4f67-86e9-3f931dd48581" +karla_fuente_nolasco_id = "109f4d94-63ea-4a21-ab15-20c8b87d8ee9" +organicos_navez_osorio_id = "f645e146-f80e-40fa-953f-fd1bd06d4e9f" +xochilt_casas_chavez_id = "e3b4edaa-e4d9-4794-9c5b-3dd5b7e372aa" +ingrid_xodar_jimenez_id = "9367249f-f0ee-43f4-b771-da2fff3f185f" # ============================================================================ -# 1. NOMINA ORDINARIA +# 1. NOMINA ORDINARIA (Facturación por valores) # ============================================================================ -def create_nomina_ordinaria(): +def create_nomina_ordinaria_values(): """ Crea una factura de nomina ordinaria con percepciones, deducciones y otros pagos. """ @@ -78,7 +87,7 @@ def create_nomina_ordinaria(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -236,11 +245,10 @@ def create_nomina_ordinaria(): print(f"Response: {api_response}") return api_response - # ============================================================================ -# 2. NOMINA ASIMILADOS +# 2. NOMINA ASIMILADOS (Facturación por valores) # ============================================================================ -def create_nomina_asimilados(): +def create_nomina_asimilados_values(): """ Crea una factura de nomina para asimilados a salarios. """ @@ -251,7 +259,7 @@ def create_nomina_asimilados(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -333,11 +341,10 @@ def create_nomina_asimilados(): print(f"Response: {api_response}") return api_response - # ============================================================================ -# 3. NOMINA CON BONOS, FONDO AHORRO Y DEDUCCIONES +# 3. NOMINA CON BONOS, FONDO AHORRO Y DEDUCCIONES (Facturación por valores) # ============================================================================ -def create_nomina_bonos_fondo_ahorro(): +def create_nomina_bonos_fondo_ahorro_values(): """ Crea una factura de nomina con bonos, fondo de ahorro y multiples deducciones. """ @@ -348,7 +355,7 @@ def create_nomina_bonos_fondo_ahorro(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -496,11 +503,10 @@ def create_nomina_bonos_fondo_ahorro(): print(f"Response: {api_response}") return api_response - # ============================================================================ -# 4. NOMINA CON HORAS EXTRA +# 4. NOMINA CON HORAS EXTRA (Facturación por valores) # ============================================================================ -def create_nomina_horas_extra(): +def create_nomina_horas_extra_values(): """ Crea una factura de nomina con horas extra. """ @@ -511,7 +517,7 @@ def create_nomina_horas_extra(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -621,11 +627,10 @@ def create_nomina_horas_extra(): print(f"Response: {api_response}") return api_response - # ============================================================================ -# 5. NOMINA CON INCAPACIDADES +# 5. NOMINA CON INCAPACIDADES (Facturación por valores) # ============================================================================ -def create_nomina_incapacidades(): +def create_nomina_incapacidades_values(): """ Crea una factura de nomina con incapacidades. """ @@ -636,7 +641,7 @@ def create_nomina_incapacidades(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -736,11 +741,10 @@ def create_nomina_incapacidades(): print(f"Response: {api_response}") return api_response - # ============================================================================ -# 6. NOMINA CON SNCF (Sistema Nacional de Coordinacion Fiscal) +# 6. NOMINA CON SNCF (Sistema Nacional de Coordinacion Fiscal) (Facturación por valores) # ============================================================================ -def create_nomina_sncf(): +def create_nomina_sncf_values(): """ Crea una factura de nomina con SNCF (para organismos publicos). Usa los certificados de Organicos Navez Osorio. @@ -752,7 +756,7 @@ def create_nomina_sncf(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -892,9 +896,9 @@ def create_nomina_sncf(): # ============================================================================ -# 7. NOMINA EXTRAORDINARIA +# 7. NOMINA EXTRAORDINARIA (Facturación por valores) # ============================================================================ -def create_nomina_extraordinaria(): +def create_nomina_extraordinaria_values(): """ Crea una factura de nomina extraordinaria (ej. aguinaldo). """ @@ -905,7 +909,7 @@ def create_nomina_extraordinaria(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -986,11 +990,10 @@ def create_nomina_extraordinaria(): print(f"Response: {api_response}") return api_response - # ============================================================================ -# 8. NOMINA SEPARACION INDEMNIZACION +# 8. NOMINA SEPARACION INDEMNIZACION (Facturación por valores) # ============================================================================ -def create_nomina_separacion_indemnizacion(): +def create_nomina_separacion_indemnizacion_values(): """ Crea una factura de nomina por separacion e indemnizacion. """ @@ -1001,7 +1004,7 @@ def create_nomina_separacion_indemnizacion(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -1098,9 +1101,9 @@ def create_nomina_separacion_indemnizacion(): # ============================================================================ -# 9. NOMINA JUBILACION PENSION RETIRO +# 9. NOMINA JUBILACION PENSION RETIRO (Facturación por valores) # ============================================================================ -def create_nomina_jubilacion_pension_retiro(): +def create_nomina_jubilacion_pension_retiro_values(): """ Crea una factura de nomina por jubilacion, pension o retiro. """ @@ -1111,7 +1114,7 @@ def create_nomina_jubilacion_pension_retiro(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -1198,9 +1201,9 @@ def create_nomina_jubilacion_pension_retiro(): # ============================================================================ -# 10. NOMINA SIN DEDUCCIONES +# 10. NOMINA SIN DEDUCCIONES (Facturación por valores) # ============================================================================ -def create_nomina_sin_deducciones(): +def create_nomina_sin_deducciones_values(): """ Crea una factura de nomina sin deducciones. """ @@ -1211,7 +1214,7 @@ def create_nomina_sin_deducciones(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -1295,9 +1298,9 @@ def create_nomina_sin_deducciones(): # ============================================================================ -# 11. NOMINA SUBSIDIO CAUSADO AL EMPLEO +# 11. NOMINA SUBSIDIO CAUSADO AL EMPLEO (Facturación por valores) # ============================================================================ -def create_nomina_subsidio_causado(): +def create_nomina_subsidio_causado_values(): """ Crea una factura de nomina con subsidio causado al empleo. """ @@ -1308,7 +1311,7 @@ def create_nomina_subsidio_causado(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -1413,9 +1416,9 @@ def create_nomina_subsidio_causado(): # ============================================================================ -# 12. NOMINA VIATICOS +# 12. NOMINA VIATICOS (Facturación por valores) # ============================================================================ -def create_nomina_viaticos(): +def create_nomina_viaticos_values(): """ Crea una factura de nomina con viaticos. """ @@ -1426,7 +1429,7 @@ def create_nomina_viaticos(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -1514,11 +1517,10 @@ def create_nomina_viaticos(): print(f"Response: {api_response}") return api_response - # ============================================================================ -# 13. NOMINA BASICA +# 13. NOMINA BASICA (Facturación por valores) # ============================================================================ -def create_nomina_basica(): +def create_nomina_basica_values(): """ Crea una factura de nomina basica con sueldo y deducciones de seguridad social e ISR. """ @@ -1529,7 +1531,7 @@ def create_nomina_basica(): payroll_invoice = Invoice( version_code="4.0", series="F", - date="2026-01-18T18:04:06", + date="2026-01-25T10:00:00", payment_method_code="PUE", currency_code="MXN", type_code="N", @@ -1625,53 +1627,2006 @@ def create_nomina_basica(): return api_response + +#********************(Facturación por referencias)**************************** +#********************(Facturación por referencias)**************************** +#********************(Facturación por referencias)**************************** + # ============================================================================ -# FUNCION PRINCIPAL +# 1. NOMINA ORDINARIA (Facturación por referencias) # ============================================================================ -def main(): +def create_nomina_ordinaria_references_setup_data(): """ - Funcion principal que ejecuta todos los ejemplos de factura de nomina. - Descomenta las funciones que desees ejecutar. + Configura los datos de empleado/empleador para nomina ordinaria por referencias. """ + print("\n" + "="*60) + print("SETUP: 1. NOMINA ORDINARIA (Referencias)") print("="*60) - print("EJEMPLOS DE FACTURA DE NOMINA - FISCALAPI PYTHON SDK") + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(karla_fuente_nolasco_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(karla_fuente_nolasco_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "FUNO850618MJCNLR09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=karla_fuente_nolasco_id, + social_security_number="04078873454", + labor_relation_start_date=datetime(2024, 8, 18), + seniority="P54W", + sat_contract_type_id="01", + sat_tax_regime_type_id="02", + employee_number="123456789", + department="GenAI", + position="Sr Software Engineer", + sat_job_risk_id="1", + sat_payment_periodicity_id="05", + sat_bank_id="012", + base_salary_for_contributions=Decimal("2828.50"), + integrated_daily_salary=Decimal("0.00"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_ordinaria_references(): + """ + Crea una factura de nomina ordinaria con percepciones, deducciones y otros pagos. + """ + print("\n" + "="*60) + print("1. NOMINA ORDINARIA") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=karla_fuente_nolasco_id, + tax_regime_code="605", + cfdi_use_code="CN01" + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2025-08-30", + initial_payment_date="2025-07-31", + final_payment_date="2025-08-30", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="1003", + concept="Sueldo Nominal", + taxed_amount=Decimal("95030.00"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="005", + code="5913", + concept="Fondo de Ahorro Aportacion Patron", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("4412.46") + ), + PayrollEarning( + earning_type_code="038", + code="1885", + concept="Bono Ingles", + taxed_amount=Decimal("14254.50"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="029", + code="1941", + concept="Vales Despensa", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("3439.00") + ), + PayrollEarning( + earning_type_code="038", + code="1824", + concept="Herramientas Teletrabajo (telecom y prop. electri)", + taxed_amount=Decimal("273.00"), + exempt_amount=Decimal("0.00") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="002", + code="5050", + concept="Exceso de subsidio al empleo", + amount=Decimal("0.00"), + subsidy_caused=Decimal("0.00") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="002", + code="5003", + concept="ISR Causado", + amount=Decimal("27645.52") + ), + PayrollDeduction( + deduction_type_code="004", + code="5910", + concept="Fondo de ahorro Empleado Inversion", + amount=Decimal("4412.46") + ), + PayrollDeduction( + deduction_type_code="004", + code="5914", + concept="Fondo de Ahorro Patron Inversion", + amount=Decimal("4412.46") + ), + PayrollDeduction( + deduction_type_code="004", + code="1966", + concept="Contribucion poliza exceso GMM", + amount=Decimal("519.91") + ), + PayrollDeduction( + deduction_type_code="004", + code="1934", + concept="Descuento Vales Despensa", + amount=Decimal("1.00") + ), + PayrollDeduction( + deduction_type_code="004", + code="1942", + concept="Vales Despensa Electronico", + amount=Decimal("3439.00") + ), + PayrollDeduction( + deduction_type_code="001", + code="1895", + concept="IMSS", + amount=Decimal("2391.13") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + +# ============================================================================ +# 2. NOMINA ASIMILADOS (Facturación por referencias) +# ============================================================================ +def create_nomina_asimilados_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina asimilados por referencias. + """ + print("\n" + "="*60) + print("SETUP: 2. NOMINA ASIMILADOS (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(xochilt_casas_chavez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(xochilt_casas_chavez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "CACX850618MJCSHS09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=xochilt_casas_chavez_id, + sat_contract_type_id="09", + sat_unionized_status_id="No", + sat_tax_regime_type_id="09", + employee_number="00002", + department="ADMINISTRACION", + position="DIRECTOR DE ADMINISTRACION", + sat_payment_periodicity_id="99", + sat_bank_id="012", + bank_account="1111111111", + sat_payroll_state_id="CMX" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + origin_employer_tin="EKU9003173C9" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_asimilados_references(): + """ + Crea una factura de nomina para asimilados a salarios usando facturacion por referencias. + """ + print("\n" + "="*60) + print("2. NOMINA ASIMILADOS (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="06880", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=xochilt_casas_chavez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="E", + payment_date="2023-06-02T00:00:00", + initial_payment_date="2023-06-01T00:00:00", + final_payment_date="2023-06-02T00:00:00", + days_paid=Decimal("1"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="046", + code="010046", + concept="INGRESOS ASIMILADOS A SALARIOS", + taxed_amount=Decimal("111197.73"), + exempt_amount=Decimal("0.00") + ) + ], + other_payments=[] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="002", + code="020002", + concept="ISR", + amount=Decimal("36197.73") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + +# ============================================================================ +# 3. NOMINA CON BONOS, FONDO AHORRO Y DEDUCCIONES (Facturación por referencias) +# ============================================================================ +def create_nomina_bonos_fondo_ahorro_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina bonos por referencias. + """ + print("\n" + "="*60) + print("SETUP: 3. NOMINA CON BONOS, FONDO AHORRO Y DEDUCCIONES (Referencias)") print("="*60) - # Ejecutar todos los ejemplos uno por uno - examples = [ - create_nomina_ordinaria, - create_nomina_asimilados, - create_nomina_bonos_fondo_ahorro, - create_nomina_horas_extra, - create_nomina_incapacidades, - create_nomina_sncf, - create_nomina_extraordinaria, - create_nomina_separacion_indemnizacion, - create_nomina_jubilacion_pension_retiro, - create_nomina_sin_deducciones, - create_nomina_subsidio_causado, - create_nomina_viaticos, - create_nomina_basica, - ] - - results = [] - for example in examples: - try: - response = example() - success = response.succeeded if response else False - results.append((example.__name__, success, None)) - except Exception as e: - results.append((example.__name__, False, str(e))) - - # Resumen de resultados + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="0000000000", + labor_relation_start_date=datetime(2022, 3, 2), + seniority="P66W", + sat_contract_type_id="01", + sat_unionized_status_id="No", + sat_tax_regime_type_id="02", + employee_number="111111", + sat_job_risk_id="4", + sat_payment_periodicity_id="02", + integrated_daily_salary=Decimal("180.96"), + sat_payroll_state_id="GUA" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="Z0000001234" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_bonos_fondo_ahorro_references(): + """ + Crea una factura de nomina con bonos, fondo de ahorro usando facturacion por referencias. + """ print("\n" + "="*60) - print("RESUMEN DE RESULTADOS") + print("3. NOMINA CON BONOS, FONDO AHORRO Y DEDUCCIONES (Referencias)") print("="*60) - for name, success, error in results: - status = "OK" if success else "FAILED" - print(f"{name}: {status}") - if error: - print(f" Error: {error}") + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-06-11T00:00:00", + initial_payment_date="2023-06-05T00:00:00", + final_payment_date="2023-06-11T00:00:00", + days_paid=Decimal("7"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="SP01", + concept="SUELDO", + taxed_amount=Decimal("1210.30"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="010", + code="SP02", + concept="PREMIO PUNTUALIDAD", + taxed_amount=Decimal("121.03"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="029", + code="SP03", + concept="MONEDERO ELECTRONICO", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("269.43") + ), + PayrollEarning( + earning_type_code="010", + code="SP04", + concept="PREMIO DE ASISTENCIA", + taxed_amount=Decimal("121.03"), + exempt_amount=Decimal("0.00") + ), + PayrollEarning( + earning_type_code="005", + code="SP54", + concept="APORTACION FONDO AHORRO", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("121.03") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="002", + code="ISRSUB", + concept="Subsidio ISR para empleo", + amount=Decimal("0.0"), + subsidy_caused=Decimal("0.0"), + balance_compensation=PayrollBalanceCompensation( + favorable_balance=Decimal("0.0"), + year=2022, + remaining_favorable_balance=Decimal("0.0") + ) + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="004", + code="ZA09", + concept="APORTACION FONDO AHORRO", + amount=Decimal("121.03") + ), + PayrollDeduction( + deduction_type_code="002", + code="ISR", + concept="ISR", + amount=Decimal("36.57") + ), + PayrollDeduction( + deduction_type_code="001", + code="IMSS", + concept="Cuota de Seguridad Social EE", + amount=Decimal("30.08") + ), + PayrollDeduction( + deduction_type_code="004", + code="ZA68", + concept="DEDUCCION FDO AHORRO PAT", + amount=Decimal("121.03") + ), + PayrollDeduction( + deduction_type_code="018", + code="ZA11", + concept="APORTACION CAJA AHORRO", + amount=Decimal("300.00") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 4. NOMINA CON HORAS EXTRA (Facturación por referencias) +# ============================================================================ +def create_nomina_horas_extra_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina horas extra por referencias. + """ + print("\n" + "="*60) + print("SETUP: 4. NOMINA CON HORAS EXTRA (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_horas_extra_references(): + """ + Crea una factura de nomina con horas extra usando facturacion por referencias. + """ + print("\n" + "="*60) + print("4. NOMINA CON HORAS EXTRA (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ), + PayrollEarning( + earning_type_code="019", + code="00100", + concept="Horas Extra", + taxed_amount=Decimal("50.00"), + exempt_amount=Decimal("50.00"), + overtime=[ + PayrollOvertime( + days=1, + hours_type_code="01", + extra_hours=2, + amount_paid=Decimal("100.00") + ) + ] + ) + ], + other_payments=[] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="001", + code="00301", + concept="Seguridad Social", + amount=Decimal("200") + ), + PayrollDeduction( + deduction_type_code="002", + code="00302", + concept="ISR", + amount=Decimal("100") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 5. NOMINA CON INCAPACIDADES (Facturación por referencias) +# ============================================================================ +def create_nomina_incapacidades_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina incapacidades por referencias. + """ + print("\n" + "="*60) + print("SETUP: 5. NOMINA CON INCAPACIDADES (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_incapacidades_references(): + """ + Crea una factura de nomina con incapacidades usando facturacion por referencias. + """ + print("\n" + "="*60) + print("5. NOMINA CON INCAPACIDADES (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="001", + code="00301", + concept="Seguridad Social", + amount=Decimal("200") + ), + PayrollDeduction( + deduction_type_code="002", + code="00302", + concept="ISR", + amount=Decimal("100") + ) + ], + disabilities=[ + PayrollDisability( + disability_days=1, + disability_type_code="01" + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 6. NOMINA CON SNCF (Facturación por referencias) +# ============================================================================ +def create_nomina_sncf_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina SNCF por referencias. + """ + print("\n" + "="*60) + print("SETUP: 6. NOMINA CON SNCF (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(xochilt_casas_chavez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(organicos_navez_osorio_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(xochilt_casas_chavez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "CACX850618MJCSHS09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=organicos_navez_osorio_id, + employee_person_id=xochilt_casas_chavez_id, + social_security_number="80997742673", + labor_relation_start_date=datetime(2021, 9, 1), + seniority="P88W", + sat_contract_type_id="01", + sat_tax_regime_type_id="02", + employee_number="273", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + integrated_daily_salary=Decimal("221.48"), + sat_payroll_state_id="GRO" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=organicos_navez_osorio_id, + employer_registration="27112029", + sat_fund_source_id="IP" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_sncf_references(): + """ + Crea una factura de nomina con SNCF usando facturacion por referencias. + """ + print("\n" + "="*60) + print("6. NOMINA CON SNCF (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="39074", + export_code="01", + issuer=InvoiceIssuer( + id=organicos_navez_osorio_id + ), + recipient=InvoiceRecipient( + id=xochilt_casas_chavez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-16T00:00:00", + initial_payment_date="2023-05-01T00:00:00", + final_payment_date="2023-05-16T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="P001", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("3322.20"), + exempt_amount=Decimal("0.0") + ), + PayrollEarning( + earning_type_code="038", + code="P540", + concept="Compensacion", + taxed_amount=Decimal("100.0"), + exempt_amount=Decimal("0.0") + ), + PayrollEarning( + earning_type_code="038", + code="P550", + concept="Compensacion Garantizada Extraordinaria", + taxed_amount=Decimal("2200.0"), + exempt_amount=Decimal("0.0") + ), + PayrollEarning( + earning_type_code="038", + code="P530", + concept="Servicio Extraordinario", + taxed_amount=Decimal("200.0"), + exempt_amount=Decimal("0.0") + ), + PayrollEarning( + earning_type_code="001", + code="P506", + concept="Otras Prestaciones", + taxed_amount=Decimal("1500.0"), + exempt_amount=Decimal("0.0") + ), + PayrollEarning( + earning_type_code="001", + code="P505", + concept="Remuneracion al Desempeno Legislativo", + taxed_amount=Decimal("17500.0"), + exempt_amount=Decimal("0.0") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="002", + code="o002", + concept="Subsidio para el empleo efectivamente entregado al trabajador", + amount=Decimal("0.0"), + subsidy_caused=Decimal("0.0") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="002", + code="D002", + concept="ISR", + amount=Decimal("4716.61") + ), + PayrollDeduction( + deduction_type_code="004", + code="D525", + concept="Redondeo", + amount=Decimal("0.81") + ), + PayrollDeduction( + deduction_type_code="001", + code="D510", + concept="Cuota Trabajador ISSSTE", + amount=Decimal("126.78") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 7. NOMINA EXTRAORDINARIA (Facturación por referencias) +# ============================================================================ +def create_nomina_extraordinaria_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina extraordinaria por referencias. + """ + print("\n" + "="*60) + print("SETUP: 7. NOMINA EXTRAORDINARIA (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P439W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="99", + sat_bank_id="002", + bank_account="1111111111", + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_extraordinaria_references(): + """ + Crea una factura de nomina extraordinaria usando facturacion por referencias. + """ + print("\n" + "="*60) + print("7. NOMINA EXTRAORDINARIA (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="E", + payment_date="2023-06-04T00:00:00", + initial_payment_date="2023-06-04T00:00:00", + final_payment_date="2023-06-04T00:00:00", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="002", + code="00500", + concept="Gratificacion Anual (Aguinaldo)", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("10000.00") + ) + ], + other_payments=[] + ), + deductions=[] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 8. NOMINA SEPARACION INDEMNIZACION (Facturación por referencias) +# ============================================================================ +def create_nomina_separacion_indemnizacion_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina separacion indemnizacion por referencias. + """ + print("\n" + "="*60) + print("SETUP: 8. NOMINA SEPARACION INDEMNIZACION (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P439W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="99", + sat_bank_id="002", + bank_account="1111111111", + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_separacion_indemnizacion_references(): + """ + Crea una factura de nomina por separacion e indemnizacion usando facturacion por referencias. + """ + print("\n" + "="*60) + print("8. NOMINA SEPARACION INDEMNIZACION (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="E", + payment_date="2023-06-04T00:00:00", + initial_payment_date="2023-05-05T00:00:00", + final_payment_date="2023-06-04T00:00:00", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="023", + code="00500", + concept="Pagos por separacion", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("10000.00") + ), + PayrollEarning( + earning_type_code="025", + code="00900", + concept="Indemnizaciones", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("500.00") + ) + ], + other_payments=[], + severance=PayrollSeverance( + total_paid=Decimal("10500.00"), + years_of_service=1, + last_monthly_salary=Decimal("10000.00"), + accumulable_income=Decimal("10000.00"), + non_accumulable_income=Decimal("0.00") + ) + ), + deductions=[] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 9. NOMINA JUBILACION PENSION RETIRO (Facturación por referencias) +# ============================================================================ +def create_nomina_jubilacion_pension_retiro_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina jubilacion pension retiro por referencias. + """ + print("\n" + "="*60) + print("SETUP: 9. NOMINA JUBILACION PENSION RETIRO (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P439W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="99", + sat_bank_id="002", + bank_account="1111111111", + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_jubilacion_pension_retiro_references(): + """ + Crea una factura de nomina por jubilacion, pension o retiro usando facturacion por referencias. + """ + print("\n" + "="*60) + print("9. NOMINA JUBILACION PENSION RETIRO (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="E", + payment_date="2023-05-05T00:00:00", + initial_payment_date="2023-06-04T00:00:00", + final_payment_date="2023-06-04T00:00:00", + days_paid=Decimal("30"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="039", + code="00500", + concept="Jubilaciones, pensiones o haberes de retiro", + taxed_amount=Decimal("0.00"), + exempt_amount=Decimal("10000.00") + ) + ], + retirement=PayrollRetirement( + total_one_time=Decimal("10000.00"), + accumulable_income=Decimal("10000.00"), + non_accumulable_income=Decimal("0.00") + ) + ), + deductions=[] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 10. NOMINA SIN DEDUCCIONES (Facturación por referencias) +# ============================================================================ +def create_nomina_sin_deducciones_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina sin deducciones por referencias. + """ + print("\n" + "="*60) + print("SETUP: 10. NOMINA SIN DEDUCCIONES (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_sin_deducciones_references(): + """ + Crea una factura de nomina sin deducciones usando facturacion por referencias. + """ + print("\n" + "="*60) + print("10. NOMINA SIN DEDUCCIONES (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ) + ], + other_payments=[] + ), + deductions=[] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 11. NOMINA SUBSIDIO CAUSADO (Facturación por referencias) +# ============================================================================ +def create_nomina_subsidio_causado_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina subsidio causado por referencias. + """ + print("\n" + "="*60) + print("SETUP: 11. NOMINA SUBSIDIO CAUSADO (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="02", # Different from other types + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_subsidio_causado_references(): + """ + Crea una factura de nomina con subsidio causado usando facturacion por referencias. + """ + print("\n" + "="*60) + print("11. NOMINA SUBSIDIO CAUSADO (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ) + ], + other_payments=[ + PayrollOtherPayment( + other_payment_type_code="007", + code="0002", + concept="ISR ajustado por subsidio", + amount=Decimal("145.80"), + subsidy_caused=Decimal("0.0") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="107", + code="D002", + concept="Ajuste al Subsidio Causado", + amount=Decimal("160.35") + ), + PayrollDeduction( + deduction_type_code="002", + code="D002", + concept="ISR", + amount=Decimal("145.80") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 12. NOMINA VIATICOS (Facturación por referencias) +# ============================================================================ +def create_nomina_viaticos_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina viaticos por referencias. + """ + print("\n" + "="*60) + print("SETUP: 12. NOMINA VIATICOS (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P438W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_viaticos_references(): + """ + Crea una factura de nomina con viaticos usando facturacion por referencias. + """ + print("\n" + "="*60) + print("12. NOMINA VIATICOS (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-09-26T00:00:00", + initial_payment_date="2023-09-11T00:00:00", + final_payment_date="2023-09-26T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="050", + code="050", + concept="Viaticos", + taxed_amount=Decimal("0"), + exempt_amount=Decimal("3000") + ) + ] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="081", + code="081", + concept="Ajuste en viaticos entregados al trabajador", + amount=Decimal("3000") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 13. NOMINA BASICA (Facturación por referencias) +# ============================================================================ +def create_nomina_basica_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina basica por referencias. + """ + print("\n" + "="*60) + print("SETUP: 13. NOMINA BASICA (Referencias)") + print("="*60) + + # 1. Delete existing employee data + try: + response = client.people.employee.delete(ingrid_xodar_jimenez_id) + print(f" - Delete employee: {response.succeeded}") + except Exception as e: + print(f" - No existing employee data: {e}") + + # 2. Delete existing employer data + try: + response = client.people.employer.delete(escuela_kemper_urgate_id) + print(f" - Delete employer: {response.succeeded}") + except Exception as e: + print(f" - No existing employer data: {e}") + + # 3. Update person with curp and tax regime for payroll + person_response = client.people.get_by_id(ingrid_xodar_jimenez_id) + if person_response.succeeded and person_response.data: + person = person_response.data + person.curp = "XOJI850618MJCDNG09" + person.sat_tax_regime_id = "605" + person.sat_cfdi_use_id = "CN01" + response = client.people.update(person) + print(f" - Update person: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 4. Create employee data + employee_data = EmployeeData( + employer_person_id=escuela_kemper_urgate_id, + employee_person_id=ingrid_xodar_jimenez_id, + social_security_number="000000", + labor_relation_start_date=datetime(2015, 1, 1), + seniority="P437W", + sat_contract_type_id="01", + sat_workday_type_id="01", + sat_tax_regime_type_id="03", + employee_number="120", + department="Desarrollo", + position="Ingeniero de Software", + sat_job_risk_id="1", + sat_payment_periodicity_id="04", + sat_bank_id="002", + bank_account="1111111111", + base_salary_for_contributions=Decimal("490.22"), + integrated_daily_salary=Decimal("146.47"), + sat_payroll_state_id="JAL" + ) + response = client.people.employee.create(employee_data) + print(f" - Create employee: {response.succeeded}") + if not response.succeeded: + print(f" Error: {response.message} - {response.details}") + + # 5. Create employer data + employer_data = EmployerData( + person_id=escuela_kemper_urgate_id, + employer_registration="B5510768108", + origin_employer_tin="URE180429TM6" + ) + response = client.people.employer.create(employer_data) + print(f" - Create employer: {response.succeeded}") +def create_nomina_basica_references(): + """ + Crea una factura de nomina basica usando facturacion por referencias. + """ + print("\n" + "="*60) + print("13. NOMINA BASICA (Referencias)") + print("="*60) + + payroll_invoice = Invoice( + version_code="4.0", + series="F", + date="2026-01-25T10:00:00", + payment_method_code="PUE", + currency_code="MXN", + type_code="N", + expedition_zip_code="20000", + export_code="01", + issuer=InvoiceIssuer( + id=escuela_kemper_urgate_id + ), + recipient=InvoiceRecipient( + id=ingrid_xodar_jimenez_id + ), + complement=InvoiceComplement( + payroll=PayrollComplement( + version="1.2", + payroll_type_code="O", + payment_date="2023-05-24T00:00:00", + initial_payment_date="2023-05-09T00:00:00", + final_payment_date="2023-05-24T00:00:00", + days_paid=Decimal("15"), + earnings=PayrollEarningsComplement( + earnings=[ + PayrollEarning( + earning_type_code="001", + code="00500", + concept="Sueldos, Salarios Rayas y Jornales", + taxed_amount=Decimal("2808.8"), + exempt_amount=Decimal("2191.2") + ) + ], + other_payments=[] + ), + deductions=[ + PayrollDeduction( + deduction_type_code="001", + code="00301", + concept="Seguridad Social", + amount=Decimal("200") + ), + PayrollDeduction( + deduction_type_code="002", + code="00302", + concept="ISR", + amount=Decimal("100") + ) + ] + ) + ) + ) + + api_response = client.invoices.create(payroll_invoice) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# FUNCIONES DE INVOCACION +# ============================================================================ +def invoke_by_values_payrolls(): + """ + Invoca UN metodo de facturacion por valores. + Descomenta solo UNO a la vez para ejecutar el ejemplo. + """ + # create_nomina_ordinaria_values() + # create_nomina_asimilados_values() + # create_nomina_bonos_fondo_ahorro_values() + # create_nomina_horas_extra_values() + # create_nomina_incapacidades_values() + # create_nomina_sncf_values() + # create_nomina_extraordinaria_values() + # create_nomina_separacion_indemnizacion_values() + # create_nomina_jubilacion_pension_retiro_values() + # create_nomina_sin_deducciones_values() + # create_nomina_subsidio_causado_values() + # create_nomina_viaticos_values() + # create_nomina_basica_values() + pass + + +def invoke_by_references_payrolls(): + """ + Invoca UN metodo de facturacion por referencias con su setup. + Descomenta solo UN PAR a la vez para ejecutar el ejemplo. + """ + # create_nomina_ordinaria_references_setup_data() + # create_nomina_ordinaria_references() + + # create_nomina_asimilados_references_setup_data() + # create_nomina_asimilados_references() + + # create_nomina_bonos_fondo_ahorro_references_setup_data() + # create_nomina_bonos_fondo_ahorro_references() + + # create_nomina_horas_extra_references_setup_data() + # create_nomina_horas_extra_references() + + # create_nomina_incapacidades_references_setup_data() + # create_nomina_incapacidades_references() + + # create_nomina_sncf_references_setup_data() + # create_nomina_sncf_references() + + # create_nomina_extraordinaria_references_setup_data() + # create_nomina_extraordinaria_references() + + # create_nomina_separacion_indemnizacion_references_setup_data() + # create_nomina_separacion_indemnizacion_references() + + # create_nomina_jubilacion_pension_retiro_references_setup_data() + # create_nomina_jubilacion_pension_retiro_references() + + # create_nomina_sin_deducciones_references_setup_data() + # create_nomina_sin_deducciones_references() + + # create_nomina_subsidio_causado_references_setup_data() + # create_nomina_subsidio_causado_references() + + # create_nomina_viaticos_references_setup_data() + # create_nomina_viaticos_references() + + create_nomina_basica_references_setup_data() + create_nomina_basica_references() + pass + + +# ============================================================================ +# FUNCION PRINCIPAL +# ============================================================================ +def main(): + """ + Funcion principal que ejecuta los ejemplos de factura de nomina. + Descomenta las funciones que desees ejecutar. + """ + print("="*60) + print("EJEMPLOS DE FACTURA DE NOMINA - FISCALAPI PYTHON SDK") + print("="*60) + + # invoke_by_values_payrolls() + invoke_by_references_payrolls() print("\n" + "="*60) print("FIN DE LOS EJEMPLOS") diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index bb14f04..e8265d0 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -66,6 +66,7 @@ class Person(BaseDto): user_type: Optional[CatalogDto] = Field(default=None, alias="userType", description="Tipo de persona expandido.") tin: Optional[str] = Field(default=None, alias="tin", description="RFC del emisor (Tax Identification Number).") zip_code: Optional[str] = Field(default=None, alias="zipCode", description="Código postal del emisor.") + curp: Optional[str] = Field(default=None, alias="curp", description="CURP de la persona.") base64_photo: Optional[str] = Field(default=None, alias="base64Photo", description="Foto de perfil en formato base64.") tax_password: Optional[str] = Field(default=None, alias="taxPassword", description="Contraseña de los certificados CSD del emisor.") available_balance: Optional[Decimal] = Field(default=None, alias="availableBalance", description="Saldo disponible en la cuenta.") @@ -86,7 +87,7 @@ class EmployeeData(BaseDto): employee_person_id: Optional[str] = Field(default=None, alias="employeePersonId") social_security_number: Optional[str] = Field(default=None, alias="socialSecurityNumber") labor_relation_start_date: Optional[datetime] = Field(default=None, alias="laborRelationStartDate") - seniority: Optional[int] = Field(default=None, alias="seniority") + seniority: Optional[str] = Field(default=None, alias="seniority") sat_contract_type_id: Optional[str] = Field(default=None, alias="satContractTypeId") sat_contract_type: Optional[CatalogDto] = Field(default=None, alias="satContractType") sat_unionized_status_id: Optional[str] = Field(default=None, alias="satUnionizedStatusId") @@ -165,7 +166,7 @@ class InvoiceRecipientEmployeeData(BaseDto): """Datos del empleado para el receptor en CFDI de nómina.""" curp: Optional[str] = Field(default=None, alias="curp") social_security_number: Optional[str] = Field(default=None, alias="socialSecurityNumber") - labor_relation_start_date: Optional[str] = Field(default=None, alias="laborRelationStartDate") + labor_relation_start_date: Optional[datetime] = Field(default=None, alias="laborRelationStartDate") seniority: Optional[str] = Field(default=None, alias="seniority") sat_contract_type_id: Optional[str] = Field(default=None, alias="satContractTypeId") sat_unionized_status_id: Optional[str] = Field(default=None, alias="satUnionizedStatusId") From bf9c5fcc5e6050ffafeec587244893c50a75baea Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 25 Jan 2026 15:49:31 -0600 Subject: [PATCH 22/28] Add language-agnostic payroll requirements document Comprehensive documentation for implementing CFDI Nomina support in any FiscalAPI SDK, covering all 13 payroll types, models, services, and both operation modes (by values and by references). Co-Authored-By: Claude Opus 4.5 --- payroll-requirements.md | 721 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 721 insertions(+) create mode 100644 payroll-requirements.md diff --git a/payroll-requirements.md b/payroll-requirements.md new file mode 100644 index 0000000..6f6e28a --- /dev/null +++ b/payroll-requirements.md @@ -0,0 +1,721 @@ +# FiscalAPI SDK - Payroll (CFDI Nomina) Requirements + +This document describes the requirements for implementing payroll invoice (CFDI de Nomina) support in FiscalAPI SDKs. It is language-agnostic and focuses on the models, services, and functionality needed. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [New Models Required](#new-models-required) +3. [New Services Required](#new-services-required) +4. [Person Model Updates](#person-model-updates) +5. [Invoice Model Updates](#invoice-model-updates) +6. [Two Operation Modes](#two-operation-modes) +7. [Payroll Types Reference](#payroll-types-reference) +8. [API Endpoints](#api-endpoints) +9. [Field Mappings (JSON Aliases)](#field-mappings-json-aliases) +10. [Example Implementations](#example-implementations) + +--- + +## Overview + +Payroll invoices (CFDI de Nomina) in Mexico require: +- **Employer data** attached to the issuer (company paying wages) +- **Employee data** attached to the recipient (worker receiving wages) +- **Payroll complement** containing earnings, deductions, and other payment details + +The SDK must support **13 different payroll types** and **two operation modes**: +1. **By Values** - All data sent inline with the invoice request +2. **By References** - Only person IDs sent; employee/employer data pre-configured via API + +--- + +## New Models Required + +### 1. EmployeeData + +Data model for storing employee information as a sub-resource of Person. + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| employer_person_id | employerPersonId | string | ID of the employer person | +| employee_person_id | employeePersonId | string | ID of the employee person | +| social_security_number | socialSecurityNumber | string | NSS (Numero de Seguridad Social) | +| labor_relation_start_date | laborRelationStartDate | datetime | Employment start date | +| seniority | seniority | string | ISO 8601 duration (e.g., "P54W" = 54 weeks) | +| sat_contract_type_id | satContractTypeId | string | SAT contract type code | +| sat_unionized_status_id | satUnionizedStatusId | string | Unionized status code | +| sat_tax_regime_type_id | satTaxRegimeTypeId | string | Tax regime type code | +| sat_workday_type_id | satWorkdayTypeId | string | Workday type code | +| sat_job_risk_id | satJobRiskId | string | Job risk level code | +| sat_payment_periodicity_id | satPaymentPeriodicityId | string | Payment frequency code | +| employee_number | employeeNumber | string | Internal employee number | +| sat_bank_id | satBankId | string | Bank code | +| sat_payroll_state_id | satPayrollStateId | string | State code for payroll | +| department | department | string | Department name | +| position | position | string | Job position/title | +| bank_account | bankAccount | string | Bank account number | +| base_salary_for_contributions | baseSalaryForContributions | decimal | Base salary for social security | +| integrated_daily_salary | integratedDailySalary | decimal | Integrated daily salary | +| subcontractor_rfc | subcontractorRfc | string | RFC of subcontractor (if applicable) | +| time_percentage | timePercentage | decimal | Time percentage (for partial employment) | + +### 2. EmployerData + +Data model for storing employer information as a sub-resource of Person. + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| person_id | personId | string | ID of the employer person | +| employer_registration | employerRegistration | string | Registro Patronal (IMSS) | +| origin_employer_tin | originEmployerTin | string | RFC of origin employer | +| sat_fund_source_id | satFundSourceId | string | Fund source code | +| own_resource_amount | ownResourceAmount | decimal | Own resource amount | + +### 3. InvoiceIssuerEmployerData + +Employer data embedded in invoice issuer (for "by values" mode). + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| curp | curp | string | CURP of employer representative | +| employer_registration | employerRegistration | string | Registro Patronal | +| origin_employer_tin | originEmployerTin | string | RFC of origin employer | +| sat_fund_source_id | satFundSourceId | string | Fund source code | +| own_resource_amount | ownResourceAmount | decimal | Own resource amount | + +### 4. InvoiceRecipientEmployeeData + +Employee data embedded in invoice recipient (for "by values" mode). + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| curp | curp | string | CURP of employee | +| social_security_number | socialSecurityNumber | string | NSS | +| labor_relation_start_date | laborRelationStartDate | string | Start date (ISO format) | +| seniority | seniority | string | ISO 8601 duration | +| sat_contract_type_id | satContractTypeId | string | Contract type | +| sat_unionized_status_id | satUnionizedStatusId | string | Unionized status | +| sat_workday_type_id | satWorkdayTypeId | string | Workday type | +| sat_tax_regime_type_id | satTaxRegimeTypeId | string | Tax regime type | +| employee_number | employeeNumber | string | Employee number | +| department | department | string | Department | +| position | position | string | Position | +| sat_job_risk_id | satJobRiskId | string | Job risk code | +| sat_payment_periodicity_id | satPaymentPeriodicityId | string | Payment periodicity | +| sat_bank_id | satBankId | string | Bank code | +| bank_account | bankAccount | string | Bank account | +| base_salary_for_contributions | baseSalaryForContributions | decimal | Base salary | +| integrated_daily_salary | integratedDailySalary | decimal | Integrated salary | +| sat_payroll_state_id | satPayrollStateId | string | Payroll state | + +### 5. Payroll Complement Models + +#### PayrollComplement (main container) + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| version | version | string | Always "1.2" | +| payroll_type_code | payrollTypeCode | string | "O" (ordinary) or "E" (extraordinary) | +| payment_date | paymentDate | string | Payment date (ISO format) | +| initial_payment_date | initialPaymentDate | string | Period start date | +| final_payment_date | finalPaymentDate | string | Period end date | +| days_paid | daysPaid | decimal | Days paid in period | +| earnings | earnings | PayrollEarningsComplement | Earnings container | +| deductions | deductions | PayrollDeduction[] | List of deductions | +| disabilities | disabilities | PayrollDisability[] | List of disabilities | + +#### PayrollEarningsComplement + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| earnings | earnings | PayrollEarning[] | List of earnings | +| other_payments | otherPayments | PayrollOtherPayment[] | Other payments (subsidies, etc.) | +| retirement | retirement | PayrollRetirement | Retirement/pension data | +| severance | severance | PayrollSeverance | Severance/indemnization data | + +#### PayrollEarning + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| earning_type_code | earningTypeCode | string | SAT earning type code | +| code | code | string | Internal code | +| concept | concept | string | Description | +| taxed_amount | taxedAmount | decimal | Taxable amount | +| exempt_amount | exemptAmount | decimal | Exempt amount | +| stock_options | stockOptions | PayrollStockOptions | Stock options (if applicable) | +| overtime | overtime | PayrollOvertime[] | Overtime hours | + +#### PayrollDeduction + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| deduction_type_code | deductionTypeCode | string | SAT deduction type code | +| code | code | string | Internal code | +| concept | concept | string | Description | +| amount | amount | decimal | Deduction amount | + +#### PayrollOtherPayment + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| other_payment_type_code | otherPaymentTypeCode | string | SAT other payment type code | +| code | code | string | Internal code | +| concept | concept | string | Description | +| amount | amount | decimal | Payment amount | +| subsidy_caused | subsidyCaused | decimal | Employment subsidy caused | +| balance_compensation | balanceCompensation | PayrollBalanceCompensation | Balance compensation | + +#### PayrollOvertime + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| days | days | int | Days with overtime | +| hours_type_code | hoursTypeCode | string | "01" (double) or "02" (triple) | +| extra_hours | extraHours | int | Number of extra hours | +| amount_paid | amountPaid | decimal | Amount paid | + +#### PayrollDisability + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| disability_days | disabilityDays | int | Days of disability | +| disability_type_code | disabilityTypeCode | string | SAT disability type code | +| monetary_amount | monetaryAmount | decimal | Monetary amount | + +#### PayrollRetirement + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| total_one_time | totalOneTime | decimal | One-time payment total | +| total_installments | totalInstallments | decimal | Installments total | +| daily_amount | dailyAmount | decimal | Daily amount | +| accumulable_income | accumulableIncome | decimal | Accumulable income | +| non_accumulable_income | nonAccumulableIncome | decimal | Non-accumulable income | + +#### PayrollSeverance + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| total_paid | totalPaid | decimal | Total paid | +| years_of_service | yearsOfService | int | Years of service | +| last_monthly_salary | lastMonthlySalary | decimal | Last monthly salary | +| accumulable_income | accumulableIncome | decimal | Accumulable income | +| non_accumulable_income | nonAccumulableIncome | decimal | Non-accumulable income | + +--- + +## New Services Required + +### 1. EmployeeService + +Sub-service of PeopleService for managing employee data. + +``` +Endpoint pattern: people/{personId}/employee +``` + +| Method | HTTP | Endpoint | Description | +|--------|------|----------|-------------| +| get_by_id(person_id) | GET | people/{personId}/employee | Get employee data | +| create(employee) | POST | people/{employeePersonId}/employee | Create employee data | +| update(employee) | PUT | people/{employeePersonId}/employee | Update employee data | +| delete(person_id) | DELETE | people/{personId}/employee | Delete employee data | + +### 2. EmployerService + +Sub-service of PeopleService for managing employer data. + +``` +Endpoint pattern: people/{personId}/employer +``` + +| Method | HTTP | Endpoint | Description | +|--------|------|----------|-------------| +| get_by_id(person_id) | GET | people/{personId}/employer | Get employer data | +| create(employer) | POST | people/{personId}/employer | Create employer data | +| update(employer) | PUT | people/{personId}/employer | Update employer data | +| delete(person_id) | DELETE | people/{personId}/employer | Delete employer data | + +### 3. PeopleService Updates + +Add employee and employer sub-services as properties: + +``` +client.people.employee -> EmployeeService +client.people.employer -> EmployerService +``` + +--- + +## Person Model Updates + +Add the following field to the Person model: + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| curp | curp | string | CURP (Clave Unica de Registro de Poblacion) | + +**Important**: For payroll invoices, the recipient (employee) must have: +- `curp` field populated +- `sat_tax_regime_id` = "605" (Sueldos y Salarios) +- `sat_cfdi_use_id` = "CN01" (Nomina) + +--- + +## Invoice Model Updates + +### InvoiceIssuer + +Add field: + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| employer_data | employerData | InvoiceIssuerEmployerData | Employer data for payroll | + +### InvoiceRecipient + +Add field: + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| employee_data | employeeData | InvoiceRecipientEmployeeData | Employee data for payroll | + +### InvoiceComplement + +Add field: + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| payroll | payroll | PayrollComplement | Payroll complement data | + +--- + +## Two Operation Modes + +### Mode 1: By Values + +All employee/employer data is sent inline with the invoice request. + +**Characteristics:** +- Self-contained request +- No prior setup required +- Larger payload size +- Employee data goes in `recipient.employee_data` +- Employer data goes in `issuer.employer_data` + +**Example structure:** +``` +Invoice { + issuer: { + tin: "EKU9003173C9", + legal_name: "ESCUELA KEMPER URGATE", + tax_regime_code: "601", + employer_data: { + employer_registration: "B5510768108", + origin_employer_tin: "URE180429TM6" + } + }, + recipient: { + tin: "XOJI740919U48", + legal_name: "INGRID XODAR JIMENEZ", + tax_regime_code: "605", + cfdi_use_code: "CN01", + employee_data: { + curp: "XOJI850618MJCDNG09", + social_security_number: "000000", + seniority: "P54W", + ... + } + }, + complement: { + payroll: { ... } + } +} +``` + +### Mode 2: By References + +Only person IDs are sent; employee/employer data must be pre-configured. + +**Characteristics:** +- Smaller payload +- Requires prior setup of employee/employer data via API +- Person must have CURP and tax regime configured +- Uses only `id` field in issuer/recipient + +**Setup steps:** +1. Update person with CURP and tax regime (605 for employee, appropriate for employer) +2. Create EmployeeData via `client.people.employee.create()` +3. Create EmployerData via `client.people.employer.create()` + +**Example structure:** +``` +Invoice { + issuer: { + id: "2e7b988f-3a2a-4f67-86e9-3f931dd48581" + }, + recipient: { + id: "9367249f-f0ee-43f4-b771-da2fff3f185f" + }, + complement: { + payroll: { ... } + } +} +``` + +--- + +## Payroll Types Reference + +### 13 Standard Payroll Types + +| # | Name | payroll_type_code | Key Characteristics | +|---|------|-------------------|---------------------| +| 1 | Ordinaria | O | Regular salary payment | +| 2 | Asimilados | O | Similar to salary (honorarios asimilados) | +| 3 | Bonos y Fondo de Ahorro | O | Bonuses and savings fund | +| 4 | Horas Extra | O | Overtime payment | +| 5 | Incapacidades | O | Disability/sick leave | +| 6 | SNCF | O | Federal government (SNCF) | +| 7 | Extraordinaria | E | Extraordinary payment | +| 8 | Separacion e Indemnizacion | E | Severance and indemnization | +| 9 | Jubilacion, Pension, Retiro | E | Retirement/pension | +| 10 | Sin Deducciones | O | Without deductions | +| 11 | Subsidio Causado | O | Employment subsidy adjustment | +| 12 | Viaticos | O | Travel expenses | +| 13 | Basica | O | Basic payroll | + +### Common Invoice Fields for All Payroll Types + +``` +version_code: "4.0" +payment_method_code: "PUE" +currency_code: "MXN" +type_code: "N" +expedition_zip_code: +export_code: "01" +``` + +### SAT Earning Type Codes (TipoPercepcion) + +| Code | Description | Common Use | +|------|-------------|------------| +| 001 | Sueldos, Salarios Rayas y Jornales | Regular salary | +| 002 | Gratificación Anual (Aguinaldo) | Christmas bonus | +| 003 | Participación de los Trabajadores en las Utilidades PTU | Profit sharing | +| 019 | Horas extra | Overtime | +| 022 | Prima vacacional | Vacation bonus | +| 023 | Pagos por separación | Severance pay | +| 025 | Indemnizaciones | Indemnization | +| 028 | Comisiones | Commissions | +| 029 | Vales de despensa | Food vouchers | +| 039 | Jubilaciones, pensiones o haberes de retiro | Retirement | +| 044 | Jubilaciones, pensiones o haberes de retiro parcial | Partial retirement | +| 045 | Ingresos en acciones | Stock income | +| 046 | Ingresos asimilados a salarios | Income similar to salary | +| 047 | Alimentación | Food | +| 050 | Viáticos | Travel expenses | + +### SAT Deduction Type Codes (TipoDeduccion) + +| Code | Description | Common Use | +|------|-------------|------------| +| 001 | Seguridad social | Social security | +| 002 | ISR | Income tax | +| 003 | Aportaciones a retiro | Retirement contributions | +| 004 | Otros | Other deductions | +| 006 | Descuento por incapacidad | Disability discount | +| 010 | Pensión alimenticia | Alimony | +| 020 | Fondo de ahorro | Savings fund | +| 081 | Ajuste en viáticos | Travel expense adjustment | +| 107 | Ajuste al Subsidio Causado | Subsidy adjustment | + +### SAT Other Payment Type Codes (TipoOtroPago) + +| Code | Description | +|------|-------------| +| 001 | Reintegro de ISR pagado en exceso | +| 002 | Subsidio para el empleo | +| 003 | Viáticos | +| 004 | Aplicación de saldo a favor por compensación anual | +| 007 | ISR ajustado por subsidio | + +--- + +## API Endpoints + +### Employee Endpoints + +``` +GET /api/{version}/people/{personId}/employee +POST /api/{version}/people/{personId}/employee +PUT /api/{version}/people/{personId}/employee +DELETE /api/{version}/people/{personId}/employee +``` + +### Employer Endpoints + +``` +GET /api/{version}/people/{personId}/employer +POST /api/{version}/people/{personId}/employer +PUT /api/{version}/people/{personId}/employer +DELETE /api/{version}/people/{personId}/employer +``` + +### Invoice Endpoints + +Payroll invoices use the standard invoice endpoint: + +``` +POST /api/{version}/invoices +``` + +With `type_code: "N"` for payroll invoices. + +--- + +## Field Mappings (JSON Aliases) + +All models must serialize using camelCase JSON aliases when communicating with the API. + +**Serialization rules:** +- Use camelCase for JSON property names +- Exclude null/None values from JSON +- Decimal values should serialize as strings +- Dates should serialize as ISO 8601 strings + +--- + +## Example Implementations + +### Example 1: Create Payroll Invoice (By Values) + +``` +// Pseudocode - adapt to target language + +invoice = Invoice( + version_code: "4.0", + series: "F", + date: "2026-01-25T10:00:00", + payment_method_code: "PUE", + currency_code: "MXN", + type_code: "N", + expedition_zip_code: "20000", + export_code: "01", + + issuer: InvoiceIssuer( + tin: "EKU9003173C9", + legal_name: "ESCUELA KEMPER URGATE", + tax_regime_code: "601", + employer_data: InvoiceIssuerEmployerData( + employer_registration: "B5510768108", + origin_employer_tin: "URE180429TM6" + ) + ), + + recipient: InvoiceRecipient( + tin: "XOJI740919U48", + legal_name: "INGRID XODAR JIMENEZ", + zip_code: "76028", + tax_regime_code: "605", + cfdi_use_code: "CN01", + employee_data: InvoiceRecipientEmployeeData( + curp: "XOJI850618MJCDNG09", + social_security_number: "000000", + labor_relation_start_date: "2015-01-01", + seniority: "P437W", + sat_contract_type_id: "01", + sat_workday_type_id: "01", + sat_tax_regime_type_id: "02", + employee_number: "120", + department: "Desarrollo", + position: "Ingeniero de Software", + sat_job_risk_id: "1", + sat_payment_periodicity_id: "04", + sat_bank_id: "002", + bank_account: "1111111111", + base_salary_for_contributions: 490.22, + integrated_daily_salary: 146.47, + sat_payroll_state_id: "JAL" + ) + ), + + complement: InvoiceComplement( + payroll: PayrollComplement( + version: "1.2", + payroll_type_code: "O", + payment_date: "2023-05-24", + initial_payment_date: "2023-05-09", + final_payment_date: "2023-05-24", + days_paid: 15, + earnings: PayrollEarningsComplement( + earnings: [ + PayrollEarning( + earning_type_code: "001", + code: "00500", + concept: "Sueldos, Salarios Rayas y Jornales", + taxed_amount: 2808.80, + exempt_amount: 2191.20 + ) + ] + ), + deductions: [ + PayrollDeduction( + deduction_type_code: "001", + code: "00301", + concept: "Seguridad Social", + amount: 200.00 + ), + PayrollDeduction( + deduction_type_code: "002", + code: "00302", + concept: "ISR", + amount: 100.00 + ) + ] + ) + ) +) + +response = client.invoices.create(invoice) +``` + +### Example 2: Setup for By References Mode + +``` +// Step 1: Update person with CURP and tax regime +employee_person = Person( + id: "9367249f-f0ee-43f4-b771-da2fff3f185f", + curp: "XOJI850618MJCDNG09", + sat_tax_regime_id: "605", + sat_cfdi_use_id: "CN01" +) +client.people.update(employee_person) + +// Step 2: Create employee data +employee_data = EmployeeData( + employer_person_id: "2e7b988f-3a2a-4f67-86e9-3f931dd48581", + employee_person_id: "9367249f-f0ee-43f4-b771-da2fff3f185f", + social_security_number: "000000", + labor_relation_start_date: datetime(2015, 1, 1), + seniority: "P437W", + sat_contract_type_id: "01", + sat_workday_type_id: "01", + sat_tax_regime_type_id: "02", + employee_number: "120", + department: "Desarrollo", + position: "Ingeniero de Software", + sat_job_risk_id: "1", + sat_payment_periodicity_id: "04", + sat_bank_id: "002", + bank_account: "1111111111", + integrated_daily_salary: 146.47, + sat_payroll_state_id: "JAL" +) +client.people.employee.create(employee_data) + +// Step 3: Create employer data +employer_data = EmployerData( + person_id: "2e7b988f-3a2a-4f67-86e9-3f931dd48581", + employer_registration: "B5510768108", + origin_employer_tin: "URE180429TM6" +) +client.people.employer.create(employer_data) +``` + +### Example 3: Create Payroll Invoice (By References) + +``` +invoice = Invoice( + version_code: "4.0", + series: "F", + date: "2026-01-25T10:00:00", + payment_method_code: "PUE", + currency_code: "MXN", + type_code: "N", + expedition_zip_code: "20000", + export_code: "01", + + // Only IDs - data comes from pre-configured employee/employer + issuer: InvoiceIssuer( + id: "2e7b988f-3a2a-4f67-86e9-3f931dd48581" + ), + recipient: InvoiceRecipient( + id: "9367249f-f0ee-43f4-b771-da2fff3f185f" + ), + + complement: InvoiceComplement( + payroll: PayrollComplement( + // Same payroll data as by-values mode + version: "1.2", + payroll_type_code: "O", + payment_date: "2023-05-24", + initial_payment_date: "2023-05-09", + final_payment_date: "2023-05-24", + days_paid: 15, + earnings: PayrollEarningsComplement( + earnings: [ + PayrollEarning( + earning_type_code: "001", + code: "00500", + concept: "Sueldos, Salarios Rayas y Jornales", + taxed_amount: 2808.80, + exempt_amount: 2191.20 + ) + ] + ), + deductions: [ + PayrollDeduction( + deduction_type_code: "001", + code: "00301", + concept: "Seguridad Social", + amount: 200.00 + ), + PayrollDeduction( + deduction_type_code: "002", + code: "00302", + concept: "ISR", + amount: 100.00 + ) + ] + ) + ) +) + +response = client.invoices.create(invoice) +``` + +--- + +## Implementation Checklist + +- [ ] Add `curp` field to Person model +- [ ] Create EmployeeData model +- [ ] Create EmployerData model +- [ ] Create InvoiceIssuerEmployerData model +- [ ] Create InvoiceRecipientEmployeeData model +- [ ] Create PayrollComplement and all sub-models +- [ ] Add employer_data to InvoiceIssuer model +- [ ] Add employee_data to InvoiceRecipient model +- [ ] Add payroll to InvoiceComplement model +- [ ] Create EmployeeService with CRUD operations +- [ ] Create EmployerService with CRUD operations +- [ ] Add employee and employer properties to PeopleService +- [ ] Create examples for all 13 payroll types (by values) +- [ ] Create examples for all 13 payroll types (by references) +- [ ] Create setup data methods for by-references mode +- [ ] Test all payroll types successfully + +--- + +## Notes + +1. **Seniority format**: Use ISO 8601 duration format (e.g., "P437W" for 437 weeks) +2. **Tax regime for employees**: Always "605" (Sueldos y Salarios) +3. **CFDI use for payroll**: Always "CN01" (Nomina) +4. **Invoice type**: Always "N" for payroll invoices +5. **Payment method**: Typically "PUE" (Pago en Una sola Exhibicion) +6. **Payroll complement version**: Always "1.2" From c05b1ba217d291cb0e71ba65b68fa9eecd5d6448 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 25 Jan 2026 16:26:08 -0600 Subject: [PATCH 23/28] Add StampService for stamp transactions (timbres) management - Add UserLookupDto, StampTransaction, StampTransactionParams models - Create StampService with get_list, get_by_id, transfer_stamps, withdraw_stamps methods - Add stamps property to FiscalApiClient facade - Add ejemplos-timbres.py with usage examples Co-Authored-By: Claude Opus 4.5 --- ejemplos-timbres.py | 189 +++++++++++++++++++++++++ fiscalapi/__init__.py | 10 ++ fiscalapi/models/__init__.py | 8 ++ fiscalapi/models/fiscalapi_models.py | 38 ++++- fiscalapi/services/fiscalapi_client.py | 2 + fiscalapi/services/stamp_service.py | 65 +++++++++ 6 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 ejemplos-timbres.py create mode 100644 fiscalapi/services/stamp_service.py diff --git a/ejemplos-timbres.py b/ejemplos-timbres.py new file mode 100644 index 0000000..3d87fdf --- /dev/null +++ b/ejemplos-timbres.py @@ -0,0 +1,189 @@ +""" +Ejemplos de uso del servicio de timbres (StampService) en FiscalAPI. + +Este archivo contiene ejemplos para: +- Listar transacciones de timbres +- Obtener una transaccion por ID +- Transferir timbres entre personas +- Retirar timbres +""" + +from fiscalapi import FiscalApiClient, FiscalApiSettings, StampTransactionParams + +# IDs de personas para los ejemplos +escuela_kemper_urgate_id = "2e7b988f-3a2a-4f67-86e9-3f931dd48581" +karla_fuente_nolasco_id = "109f4d94-63ea-4a21-ab15-20c8b87d8ee9" +organicos_navez_osorio_id = "f645e146-f80e-40fa-953f-fd1bd06d4e9f" +xochilt_casas_chavez_id = "e3b4edaa-e4d9-4794-9c5b-3dd5b7e372aa" +ingrid_xodar_jimenez_id = "9367249f-f0ee-43f4-b771-da2fff3f185f" +OSCAR_KALA_HAAK = "5fd9f48c-a6a2-474f-944b-88a01751d432" + +# Configuracion del cliente +settings = FiscalApiSettings( + # api_url="https://test.fiscalapi.com", + # api_key="", + # tenant="", + api_url="http://localhost:5001", + api_key="sk_development_b470ea83_3c0f_4209_b933_85223b960d91", + tenant="102e5f13-e114-41dd-bea7-507fce177281" +) + +client = FiscalApiClient(settings=settings) + + +# ============================================================================ +# 1. LISTAR TRANSACCIONES DE TIMBRES +# ============================================================================ +def listar_transacciones(): + """ + Lista las transacciones de timbres con paginacion. + """ + print("\n" + "=" * 60) + print("1. LISTAR TRANSACCIONES DE TIMBRES") + print("=" * 60) + + api_response = client.stamps.get_list(page_number=1, page_size=5) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 2. OBTENER TRANSACCION POR ID +# ============================================================================ +def obtener_transaccion_por_id(): + """ + Obtiene una transaccion de timbres por su ID. + """ + print("\n" + "=" * 60) + print("2. OBTENER TRANSACCION POR ID") + print("=" * 60) + + transaction_id = "77678d6d-94b1-4635-aa91-15cdd7423aab" + + api_response = client.stamps.get_by_id(transaction_id) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 3. TRANSFERIR TIMBRES +# ============================================================================ +def transferir_timbres(): + """ + Transfiere timbres de una persona a otra. + """ + print("\n" + "=" * 60) + print("3. TRANSFERIR TIMBRES") + print("=" * 60) + + params = StampTransactionParams( + from_person_id=escuela_kemper_urgate_id, + to_person_id=OSCAR_KALA_HAAK, + amount=10, + comments="Transferencia de prueba desde SDK Python" + ) + + api_response = client.stamps.transfer_stamps(params) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 4. RETIRAR TIMBRES +# ============================================================================ +def retirar_timbres(): + """ + Retira timbres de una persona (wrapper de transfer_stamps). + """ + print("\n" + "=" * 60) + print("4. RETIRAR TIMBRES") + print("=" * 60) + + params = StampTransactionParams( + from_person_id=karla_fuente_nolasco_id, + to_person_id=xochilt_casas_chavez_id, + amount=5, + comments="Retiro de timbres - devolucion" + ) + + api_response = client.stamps.withdraw_stamps(params) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 5. TRANSFERIR TIMBRES (ADMIN - OSCAR_KALA_HAAK) +# ============================================================================ +def transferir_timbres_admin(): + """ + Transfiere timbres desde el usuario admin (OSCAR_KALA_HAAK) a otra persona. + El admin puede transferir desde/hacia cualquier persona. + """ + print("\n" + "=" * 60) + print("5. TRANSFERIR TIMBRES (ADMIN)") + print("=" * 60) + + params = StampTransactionParams( + from_person_id=OSCAR_KALA_HAAK, + to_person_id=karla_fuente_nolasco_id, + amount=1, + comments="Transferencia admin -> Karla desde SDK Python" + ) + + api_response = client.stamps.transfer_stamps(params) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# 6. RETIRAR TIMBRES (ADMIN - OSCAR_KALA_HAAK) +# ============================================================================ +def retirar_timbres_admin(): + """ + Retira timbres usando el usuario admin (OSCAR_KALA_HAAK). + El admin puede retirar desde cualquier persona. + """ + print("\n" + "=" * 60) + print("6. RETIRAR TIMBRES (ADMIN)") + print("=" * 60) + + params = StampTransactionParams( + from_person_id=OSCAR_KALA_HAAK, + to_person_id=xochilt_casas_chavez_id, + amount=1, + comments="Retiro admin -> Xochilt desde SDK Python" + ) + + api_response = client.stamps.withdraw_stamps(params) + print(f"Response: {api_response}") + return api_response + + +# ============================================================================ +# MAIN +# ============================================================================ +def main(): + """ + Ejecuta todos los ejemplos de timbres. + """ + # 1. Listar transacciones + listar_transacciones() + + # 2. Obtener transaccion por ID + obtener_transaccion_por_id() + + # 3. Transferir timbres (usuario normal - puede fallar por permisos) + transferir_timbres() + + # 4. Retirar timbres (usuario normal - puede fallar por permisos) + retirar_timbres() + + # 5. Transferir timbres (admin - OSCAR_KALA_HAAK) + transferir_timbres_admin() + + # 6. Retirar timbres (admin - OSCAR_KALA_HAAK) + retirar_timbres_admin() + + +if __name__ == "__main__": + main() diff --git a/fiscalapi/__init__.py b/fiscalapi/__init__.py index 4543c4f..6800484 100644 --- a/fiscalapi/__init__.py +++ b/fiscalapi/__init__.py @@ -95,6 +95,10 @@ XmlItem, XmlComplement, Xml, + # Stamp models + UserLookupDto, + StampTransaction, + StampTransactionParams, ) # Servicios @@ -110,6 +114,7 @@ from .services.people_service import PeopleService from .services.product_service import ProductService from .services.tax_file_service import TaxFileService +from .services.stamp_service import StampService # Cliente principal from .services.fiscalapi_client import FiscalApiClient @@ -194,6 +199,10 @@ "XmlItem", "XmlComplement", "Xml", + # Stamp models + "UserLookupDto", + "StampTransaction", + "StampTransactionParams", # Servicios "BaseService", "ApiKeyService", @@ -207,6 +216,7 @@ "PeopleService", "ProductService", "TaxFileService", + "StampService", # Cliente principal "FiscalApiClient", ] diff --git a/fiscalapi/models/__init__.py b/fiscalapi/models/__init__.py index d4ba103..26498df 100644 --- a/fiscalapi/models/__init__.py +++ b/fiscalapi/models/__init__.py @@ -81,6 +81,10 @@ XmlItem, XmlComplement, Xml, + # Stamp models + UserLookupDto, + StampTransaction, + StampTransactionParams, ) __all__ = [ @@ -163,4 +167,8 @@ "XmlItem", "XmlComplement", "Xml", + # Stamp models + "UserLookupDto", + "StampTransaction", + "StampTransactionParams", ] diff --git a/fiscalapi/models/fiscalapi_models.py b/fiscalapi/models/fiscalapi_models.py index e8265d0..9ce1776 100644 --- a/fiscalapi/models/fiscalapi_models.py +++ b/fiscalapi/models/fiscalapi_models.py @@ -1,5 +1,5 @@ from decimal import Decimal -from pydantic import ConfigDict, EmailStr, Field +from pydantic import BaseModel, ConfigDict, EmailStr, Field from fiscalapi.models.common_models import BaseDto, CatalogDto from typing import Literal, Optional from datetime import datetime @@ -1033,4 +1033,38 @@ class Xml(BaseDto): datetime: lambda v: v.isoformat(), Decimal: str } - ) \ No newline at end of file + ) + + +# Stamp models + +class UserLookupDto(BaseDto): + """Lookup DTO for user/person references in stamp transactions.""" + tin: Optional[str] = Field(default=None, alias="tin", description="RFC del usuario.") + legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del usuario.") + + model_config = ConfigDict(populate_by_name=True) + + +class StampTransaction(BaseDto): + """Stamp transaction model representing stamp transfers/withdrawals.""" + consecutive: Optional[int] = Field(default=None, alias="consecutive", description="Consecutivo de la transacción.") + from_person: Optional[UserLookupDto] = Field(default=None, alias="fromPerson", description="Persona origen de la transferencia.") + to_person: Optional[UserLookupDto] = Field(default=None, alias="toPerson", description="Persona destino de la transferencia.") + amount: Optional[int] = Field(default=None, alias="amount", description="Cantidad de timbres transferidos.") + transaction_type: Optional[int] = Field(default=None, alias="transactionType", description="Tipo de transacción.") + transaction_status: Optional[int] = Field(default=None, alias="transactionStatus", description="Estado de la transacción.") + reference_id: Optional[str] = Field(default=None, alias="referenceId", description="ID de referencia de la transacción.") + comments: Optional[str] = Field(default=None, alias="comments", description="Comentarios de la transacción.") + + model_config = ConfigDict(populate_by_name=True) + + +class StampTransactionParams(BaseModel): + """Request parameters for stamp transfer/withdraw operations.""" + from_person_id: str = Field(default=..., alias="fromPersonId", description="ID de la persona origen.") + to_person_id: str = Field(default=..., alias="toPersonId", description="ID de la persona destino.") + amount: int = Field(default=..., alias="amount", description="Cantidad de timbres a transferir.") + comments: Optional[str] = Field(default=None, alias="comments", description="Comentarios de la transferencia.") + + model_config = ConfigDict(populate_by_name=True) \ No newline at end of file diff --git a/fiscalapi/services/fiscalapi_client.py b/fiscalapi/services/fiscalapi_client.py index faba9e4..f0adcc3 100644 --- a/fiscalapi/services/fiscalapi_client.py +++ b/fiscalapi/services/fiscalapi_client.py @@ -8,6 +8,7 @@ from fiscalapi.services.download_catalog_service import DownloadCatalogService from fiscalapi.services.download_rule_service import DownloadRuleService from fiscalapi.services.download_request_service import DownloadRequestService +from fiscalapi.services.stamp_service import StampService @@ -23,4 +24,5 @@ def __init__(self, settings: FiscalApiSettings): self.download_catalogs = DownloadCatalogService(settings) self.download_rules = DownloadRuleService(settings) self.download_requests = DownloadRequestService(settings) + self.stamps = StampService(settings) self.settings = settings \ No newline at end of file diff --git a/fiscalapi/services/stamp_service.py b/fiscalapi/services/stamp_service.py new file mode 100644 index 0000000..7671876 --- /dev/null +++ b/fiscalapi/services/stamp_service.py @@ -0,0 +1,65 @@ +"""Servicio para gestionar transacciones de timbres (stamps).""" + +from fiscalapi.models import ( + ApiResponse, + FiscalApiSettings, + PagedList, + StampTransaction, + StampTransactionParams, +) +from fiscalapi.services.base_service import BaseService + + +class StampService(BaseService): + """Service for managing stamp transactions (timbres).""" + + def __init__(self, settings: FiscalApiSettings): + super().__init__(settings) + + def get_list(self, page_number: int, page_size: int) -> ApiResponse[PagedList[StampTransaction]]: + """List stamp transactions with pagination. + + Args: + page_number: Page number (1-based). + page_size: Number of items per page. + + Returns: + ApiResponse containing a PagedList of StampTransaction objects. + """ + endpoint = f"stamps?pageNumber={page_number}&pageSize={page_size}" + return self.send_request("GET", endpoint, PagedList[StampTransaction]) + + def get_by_id(self, transaction_id: str) -> ApiResponse[StampTransaction]: + """Get a stamp transaction by ID. + + Args: + transaction_id: The unique identifier of the stamp transaction. + + Returns: + ApiResponse containing the StampTransaction object. + """ + endpoint = f"stamps/{transaction_id}" + return self.send_request("GET", endpoint, StampTransaction) + + def transfer_stamps(self, request: StampTransactionParams) -> ApiResponse[bool]: + """Transfer stamps from one person to another. + + Args: + request: StampTransactionParams containing transfer details. + + Returns: + ApiResponse containing a boolean indicating success. + """ + endpoint = "stamps" + return self.send_request("POST", endpoint, bool, payload=request) + + def withdraw_stamps(self, request: StampTransactionParams) -> ApiResponse[bool]: + """Withdraw stamps from a person (convenience wrapper for transfer_stamps). + + Args: + request: StampTransactionParams containing withdrawal details. + + Returns: + ApiResponse containing a boolean indicating success. + """ + return self.transfer_stamps(request) From 74af3a275810178ab80f07eec462e2a31c907eb3 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 25 Jan 2026 16:28:28 -0600 Subject: [PATCH 24/28] Simplify stamp examples to 4 core use cases Co-Authored-By: Claude Opus 4.5 --- ejemplos-timbres.py | 68 +++++---------------------------------------- 1 file changed, 7 insertions(+), 61 deletions(-) diff --git a/ejemplos-timbres.py b/ejemplos-timbres.py index 3d87fdf..2bfb9d7 100644 --- a/ejemplos-timbres.py +++ b/ejemplos-timbres.py @@ -77,9 +77,9 @@ def transferir_timbres(): print("=" * 60) params = StampTransactionParams( - from_person_id=escuela_kemper_urgate_id, - to_person_id=OSCAR_KALA_HAAK, - amount=10, + from_person_id=OSCAR_KALA_HAAK, + to_person_id=karla_fuente_nolasco_id, + amount=1, comments="Transferencia de prueba desde SDK Python" ) @@ -93,65 +93,17 @@ def transferir_timbres(): # ============================================================================ def retirar_timbres(): """ - Retira timbres de una persona (wrapper de transfer_stamps). + Retira timbres de una persona. """ print("\n" + "=" * 60) print("4. RETIRAR TIMBRES") print("=" * 60) - params = StampTransactionParams( - from_person_id=karla_fuente_nolasco_id, - to_person_id=xochilt_casas_chavez_id, - amount=5, - comments="Retiro de timbres - devolucion" - ) - - api_response = client.stamps.withdraw_stamps(params) - print(f"Response: {api_response}") - return api_response - - -# ============================================================================ -# 5. TRANSFERIR TIMBRES (ADMIN - OSCAR_KALA_HAAK) -# ============================================================================ -def transferir_timbres_admin(): - """ - Transfiere timbres desde el usuario admin (OSCAR_KALA_HAAK) a otra persona. - El admin puede transferir desde/hacia cualquier persona. - """ - print("\n" + "=" * 60) - print("5. TRANSFERIR TIMBRES (ADMIN)") - print("=" * 60) - - params = StampTransactionParams( - from_person_id=OSCAR_KALA_HAAK, - to_person_id=karla_fuente_nolasco_id, - amount=1, - comments="Transferencia admin -> Karla desde SDK Python" - ) - - api_response = client.stamps.transfer_stamps(params) - print(f"Response: {api_response}") - return api_response - - -# ============================================================================ -# 6. RETIRAR TIMBRES (ADMIN - OSCAR_KALA_HAAK) -# ============================================================================ -def retirar_timbres_admin(): - """ - Retira timbres usando el usuario admin (OSCAR_KALA_HAAK). - El admin puede retirar desde cualquier persona. - """ - print("\n" + "=" * 60) - print("6. RETIRAR TIMBRES (ADMIN)") - print("=" * 60) - params = StampTransactionParams( from_person_id=OSCAR_KALA_HAAK, to_person_id=xochilt_casas_chavez_id, amount=1, - comments="Retiro admin -> Xochilt desde SDK Python" + comments="Retiro de timbres desde SDK Python" ) api_response = client.stamps.withdraw_stamps(params) @@ -172,18 +124,12 @@ def main(): # 2. Obtener transaccion por ID obtener_transaccion_por_id() - # 3. Transferir timbres (usuario normal - puede fallar por permisos) + # 3. Transferir timbres transferir_timbres() - # 4. Retirar timbres (usuario normal - puede fallar por permisos) + # 4. Retirar timbres retirar_timbres() - # 5. Transferir timbres (admin - OSCAR_KALA_HAAK) - transferir_timbres_admin() - - # 6. Retirar timbres (admin - OSCAR_KALA_HAAK) - retirar_timbres_admin() - if __name__ == "__main__": main() From 7a3e1671a61957f2c364561caf171e4d162681d2 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 25 Jan 2026 16:29:33 -0600 Subject: [PATCH 25/28] Add OSCAR_KALA_HAAK constant to nomina examples Co-Authored-By: Claude Opus 4.5 --- ejemplos-facturas-de-nomina.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ejemplos-facturas-de-nomina.py b/ejemplos-facturas-de-nomina.py index 1df5007..f78a8c2 100644 --- a/ejemplos-facturas-de-nomina.py +++ b/ejemplos-facturas-de-nomina.py @@ -73,6 +73,7 @@ organicos_navez_osorio_id = "f645e146-f80e-40fa-953f-fd1bd06d4e9f" xochilt_casas_chavez_id = "e3b4edaa-e4d9-4794-9c5b-3dd5b7e372aa" ingrid_xodar_jimenez_id = "9367249f-f0ee-43f4-b771-da2fff3f185f" + # ============================================================================ # 1. NOMINA ORDINARIA (Facturación por valores) # ============================================================================ From b55f30d49846230fc4610909107088327a971107 Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 25 Jan 2026 16:40:54 -0600 Subject: [PATCH 26/28] Update README with new example files and stamp service documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add payroll invoice support (13 CFDI types) to features section - Add new "Gestión de Timbres" section documenting StampService - Add direct links to example files (nomina, timbres, complemento pago) - Remove local development credentials from example files Co-Authored-By: Claude Opus 4.5 --- README.md | 28 ++++++++++++++++++------ ejemplos-facturas-de-complemento-pago.py | 5 ----- ejemplos-facturas-de-nomina.py | 5 +---- ejemplos-timbres.py | 5 +---- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a33d2f6..c005d45 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,15 @@ - **Soporte completo para CFDI 4.0** con todas las especificaciones oficiales - **Timbrado de facturas de ingreso** con validación automática - **Timbrado de notas de crédito** (facturas de egreso) -- **Timbrado de complementos de pago** en MXN, USD y EUR. +- **Timbrado de complementos de pago** en MXN, USD y EUR +- **Timbrado de facturas de nómina** - Soporte para los 13 tipos de CFDI de nómina - **Consulta del estatus de facturas** en el SAT en tiempo real -- **Cancelación de facturas** +- **Cancelación de facturas** - **Generación de archivos PDF** de las facturas con formato profesional - **Personalización de logos y colores** en los PDF generados - **Envío de facturas por correo electrónico** automatizado - **Descarga de archivos XML** con estructura completa -- **Almacenamiento y recuperación** de facturas por 5 años. +- **Almacenamiento y recuperación** de facturas por 5 años - Dos [modos de operación](https://docs.fiscalapi.com/modes-of-operation): **Por valores** o **Por referencias** - [Ejemplos en Python](https://github.com/FiscalAPI/fiscalapi-samples-python) @@ -33,6 +34,7 @@ - **Administración de personas** (emisores, receptores, clientes, usuarios, etc.) - **Gestión de certificados CSD y FIEL** (subir archivos .cer y .key a FiscalAPI) - **Configuración de datos fiscales** (RFC, domicilio fiscal, régimen fiscal) +- **Datos de empleado y empleador** (Para facturas de nómina) ## 🛍️ Gestión de Productos/Servicios - **Gestión de productos y servicios** con catálogo personalizable @@ -43,7 +45,13 @@ - **Consulta en catálogos oficiales de Descarga masiva del SAT** actualizados - **Búsqueda de información** en catálogos del SAT con filtros avanzados - **Acceso y búsqueda** en catálogos completos - + +## 🎫 Gestión de Timbres +- **Listar transacciones de timbres** con paginación +- **Consultar transacciones** por ID +- **Transferir timbres** entre personas +- **Retirar timbres** de una persona + ## 📖 Recursos Adicionales - **Cientos de ejemplos de código** disponibles en múltiples lenguajes de programación - Documentación completa con guías paso a paso @@ -306,12 +314,15 @@ else: ## 📋 Operaciones Principales -- **Facturas (CFDI)** +- **Facturas (CFDI)** Crear facturas de ingreso, notas de crédito, complementos de pago, cancelaciones, generación de PDF/XML. -- **Personas (Clientes/Emisores)** +- **Personas (Clientes/Emisores)** Alta y administración de personas, gestión de certificados (CSD). -- **Productos y Servicios** +- **Productos y Servicios** Administración de catálogos de productos, búsqueda en catálogos SAT. +- **Timbres** + Listar transacciones, transferir y retirar timbres entre personas. + ## 🤝 Contribuir @@ -338,6 +349,9 @@ Este proyecto está licenciado bajo la Licencia **MPL**. Consulta el archivo [LI - [Como obtener mis credenciales](https://docs.fiscalapi.com/credentials-info) - [Portal de FiscalAPI](https://fiscalapi.com) - [Ejemplos en Python](https://github.com/FiscalAPI/fiscalapi-samples-python) +- [Ejemplos de Nómina](https://github.com/FiscalAPI/fiscalapi-python/blob/main/ejemplos-facturas-de-nomina.py) +- [Ejemplos de Timbres](https://github.com/FiscalAPI/fiscalapi-python/blob/main/ejemplos-timbres.py) +- [Ejemplos de Complementos de Pago](https://github.com/FiscalAPI/fiscalapi-python/blob/main/ejemplos-facturas-de-complemento-pago.py) - [Soporte técnico](https://fiscalapi.com/contact-us) - [Certificados prueba](https://docs.fiscalapi.com/tax-files-info) - [Postman Collection](https://documenter.getpostman.com/view/4346593/2sB2j4eqXr) diff --git a/ejemplos-facturas-de-complemento-pago.py b/ejemplos-facturas-de-complemento-pago.py index 9b67e51..13fb233 100644 --- a/ejemplos-facturas-de-complemento-pago.py +++ b/ejemplos-facturas-de-complemento-pago.py @@ -22,11 +22,6 @@ ) from fiscalapi.services.fiscalapi_client import FiscalApiClient - -# ============================================================================ -# CONFIGURACION -# ============================================================================ - # Configuracion del cliente settings = FiscalApiSettings( # api_url="https://test.fiscalapi.com", diff --git a/ejemplos-facturas-de-nomina.py b/ejemplos-facturas-de-nomina.py index f78a8c2..727f8f2 100644 --- a/ejemplos-facturas-de-nomina.py +++ b/ejemplos-facturas-de-nomina.py @@ -53,10 +53,7 @@ settings = FiscalApiSettings( # api_url="https://test.fiscalapi.com", # api_key="", - # tenant="", - api_url="http://localhost:5001", - api_key="sk_development_b470ea83_3c0f_4209_b933_85223b960d91", - tenant="102e5f13-e114-41dd-bea7-507fce177281" + # tenant="" ) client = FiscalApiClient(settings=settings) diff --git a/ejemplos-timbres.py b/ejemplos-timbres.py index 2bfb9d7..480a10d 100644 --- a/ejemplos-timbres.py +++ b/ejemplos-timbres.py @@ -22,10 +22,7 @@ settings = FiscalApiSettings( # api_url="https://test.fiscalapi.com", # api_key="", - # tenant="", - api_url="http://localhost:5001", - api_key="sk_development_b470ea83_3c0f_4209_b933_85223b960d91", - tenant="102e5f13-e114-41dd-bea7-507fce177281" + # tenant="" ) client = FiscalApiClient(settings=settings) From ee87487648d33ae6111798a3f1c48d02212498ea Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 25 Jan 2026 17:24:54 -0600 Subject: [PATCH 27/28] Add StampService export and update documentation - Add StampService to services/__init__.py exports - Update CLAUDE.md with complete service list, example files, and new sections - Add StampService documentation to payroll-requirements.md - Update implementation checklist with completed items - Bump version to 4.0.360 Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 67 +++++++++++++----- fiscalapi/services/__init__.py | 2 + payroll-requirements.md | 123 +++++++++++++++++++++++++++++---- setup.py | 2 +- 4 files changed, 165 insertions(+), 29 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d83ea15..2f240c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -FiscalAPI Python SDK - Official SDK for integrating with FiscalAPI, Mexico's electronic invoicing (CFDI 4.0) and fiscal services platform. Simplifies integration with SAT (Mexico's tax authority) for invoice creation, tax certificate management, and bulk downloads. +FiscalAPI Python SDK - Official SDK for integrating with FiscalAPI, Mexico's electronic invoicing (CFDI 4.0) and fiscal services platform. Simplifies integration with SAT (Mexico's tax authority) for invoice creation, tax certificate management, payroll invoices (CFDI de Nomina), and bulk downloads. ## Build and Publish Commands @@ -21,9 +21,12 @@ twine check dist/* # Publish to PyPI (requires PYPI_API_TOKEN) twine upload --username __token__ --password $PYPI_API_TOKEN dist/* + +# Clean build artifacts +rm -rf dist build fiscalapi.egg-info ``` -**Version management:** Version is defined in `setup.py`. CI/CD requires the version to match the git tag (vX.Y.Z format). +**Version management:** Version is defined in `setup.py` (line 5: `VERSION = "X.Y.Z"`). CI/CD is manually triggered via `workflow_dispatch`. ## Architecture @@ -35,13 +38,16 @@ FiscalApiClient (Facade) ├── FiscalApiSettings (Configuration) │ └── Services (all inherit from BaseService) - ├── invoice_service.py → Invoice CRUD, PDF, XML, cancellation, SAT status - ├── people_service.py → Person/entity management - ├── product_service.py → Product/service catalog - ├── tax_file_servive.py → CSD/FIEL certificate uploads - ├── api_key_service.py → API key management - ├── catalog_service.py → SAT catalog searches - └── download_*_service.py → Bulk download management + ├── invoice_service.py → Invoice CRUD, PDF, XML, cancellation, SAT status + ├── people_service.py → Person/entity management + │ ├── employee_service.py → Employee data (sub-service for payroll) + │ └── employer_service.py → Employer data (sub-service for payroll) + ├── product_service.py → Product/service catalog + ├── tax_file_service.py → CSD/FIEL certificate uploads + ├── api_key_service.py → API key management + ├── catalog_service.py → SAT catalog searches + ├── stamp_service.py → Stamp (timbres) transactions + └── download_*_service.py → Bulk download management ``` **Entry Point Pattern:** @@ -54,23 +60,35 @@ client = FiscalApiClient(settings=settings) # Access services through client client.invoices.create(invoice) client.people.get_list(page_num, page_size) +client.stamps.get_list(page_num, page_size) ``` ### Models (Pydantic v2) Located in `fiscalapi/models/`: - **common_models.py** - Base DTOs: `ApiResponse[T]`, `PagedList[T]`, `ValidationFailure`, `FiscalApiSettings` -- **fiscalapi_models.py** - Domain models: `Invoice`, `Person`, `Product`, `TaxFile`, and related entities +- **fiscalapi_models.py** - Domain models: `Invoice`, `Person`, `Product`, `TaxFile`, payroll complements, stamp transactions **Key Pattern - Field Aliasing:** Models use Pydantic `Field(alias="...")` for API JSON field mapping. When serializing, use `by_alias=True` and `exclude_none=True`. +### Public API Exports + +All types are exported from the main package (`fiscalapi/__init__.py`): +```python +from fiscalapi import Invoice, Person, Product, FiscalApiClient, ApiResponse +``` + +Also available via submodules: +```python +from fiscalapi.models import Invoice, Person +from fiscalapi.services import InvoiceService, StampService +``` + ### Two Operation Modes 1. **By References** - Use pre-created object IDs (faster, less data transfer) 2. **By Values** - Send all field data directly (self-contained, no prior setup) -See `examples.py` and README.md for detailed examples of both modes. - ### Request/Response Flow 1. Service method receives domain object @@ -86,12 +104,22 @@ See `examples.py` and README.md for detailed examples of both modes. ## Key Files -- `fiscalapi/__init__.py` - Central exports for all models and services +- `fiscalapi/__init__.py` - Central exports for all 85 public types (models + services) - `fiscalapi/services/base_service.py` - HTTP client, serialization, response handling - `fiscalapi/services/fiscalapi_client.py` - Main client facade -- `examples.py` - 3600+ lines of usage examples (commented out) - `setup.py` - Package metadata, version, and dependencies +## Example Files + +- `examples.py` - General usage examples (all invoice types) +- `ejemplos-facturas-de-nomina.py` - Payroll invoice examples (13 types) +- `ejemplos-facturas-de-complemento-pago.py` - Payment complement examples +- `ejemplos-timbres.py` - Stamp service examples + +## Reference Documentation + +- `payroll-requirements.md` - Detailed payroll implementation spec with all models, services, and SAT codes + ## Dependencies - Python >= 3.9 (CI/CD uses Python 3.9.13) @@ -102,7 +130,7 @@ See `examples.py` and README.md for detailed examples of both modes. ## Development Setup ```bash -# Create virtual environment with Python 3.9.13 +# Create virtual environment with Python 3.9+ python -m venv venv # Activate (Windows) @@ -130,8 +158,15 @@ pip install -r requirements.txt - Use `Optional[T]` only for truly optional fields - `ApiResponse[T]` supports any type T (not just BaseModel subclasses) +### Adding New Services + +1. Create service class inheriting from `BaseService` in `fiscalapi/services/` +2. Export from `fiscalapi/services/__init__.py` +3. Export from `fiscalapi/__init__.py` +4. Add property to `FiscalApiClient` class + ## External Resources - API Documentation: https://docs.fiscalapi.com - Test Certificates: https://docs.fiscalapi.com/recursos/descargas -- Postman Collection: Available in docs +- Postman Collection: https://documenter.getpostman.com/view/4346593/2sB2j4eqXr diff --git a/fiscalapi/services/__init__.py b/fiscalapi/services/__init__.py index 2b82b6e..79c75c5 100644 --- a/fiscalapi/services/__init__.py +++ b/fiscalapi/services/__init__.py @@ -12,6 +12,7 @@ from .invoice_service import InvoiceService from .people_service import PeopleService from .product_service import ProductService +from .stamp_service import StampService from .tax_file_service import TaxFileService __all__ = [ @@ -27,5 +28,6 @@ "InvoiceService", "PeopleService", "ProductService", + "StampService", "TaxFileService", ] diff --git a/payroll-requirements.md b/payroll-requirements.md index 6f6e28a..1f23733 100644 --- a/payroll-requirements.md +++ b/payroll-requirements.md @@ -9,6 +9,10 @@ This document describes the requirements for implementing payroll invoice (CFDI 1. [Overview](#overview) 2. [New Models Required](#new-models-required) 3. [New Services Required](#new-services-required) + - [EmployeeService](#1-employeeservice) + - [EmployerService](#2-employerservice) + - [PeopleService Updates](#3-peopleservice-updates) + - [StampService](#4-stampservice) 4. [Person Model Updates](#person-model-updates) 5. [Invoice Model Updates](#invoice-model-updates) 6. [Two Operation Modes](#two-operation-modes) @@ -248,6 +252,82 @@ client.people.employee -> EmployeeService client.people.employer -> EmployerService ``` +### 4. StampService + +Service for managing stamp transactions (timbres fiscales). Stamps are the digital fiscal tokens required to certify CFDI invoices. + +``` +Endpoint pattern: stamps +``` + +| Method | HTTP | Endpoint | Description | +|--------|------|----------|-------------| +| get_list(page_number, page_size) | GET | stamps?pageNumber={n}&pageSize={s} | List stamp transactions with pagination | +| get_by_id(transaction_id) | GET | stamps/{transactionId} | Get a stamp transaction by ID | +| transfer_stamps(request) | POST | stamps | Transfer stamps from one person to another | +| withdraw_stamps(request) | POST | stamps | Withdraw stamps (alias for transfer_stamps) | + +#### Stamp Models + +##### UserLookupDto + +Lookup DTO for user/person references in stamp transactions. + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| tin | tin | string | RFC of the user | +| legal_name | legalName | string | Legal name of the user | + +##### StampTransaction + +Represents a stamp transfer/withdrawal transaction. + +| Field | JSON Alias | Type | Description | +|-------|------------|------|-------------| +| consecutive | consecutive | int | Transaction consecutive number | +| from_person | fromPerson | UserLookupDto | Source person of the transfer | +| to_person | toPerson | UserLookupDto | Destination person of the transfer | +| amount | amount | int | Number of stamps transferred | +| transaction_type | transactionType | int | Type of transaction | +| transaction_status | transactionStatus | int | Status of the transaction | +| reference_id | referenceId | string | Transaction reference ID | +| comments | comments | string | Transaction comments | + +##### StampTransactionParams + +Request parameters for stamp transfer/withdraw operations. + +| Field | JSON Alias | Type | Required | Description | +|-------|------------|------|----------|-------------| +| from_person_id | fromPersonId | string | Yes | ID of the source person | +| to_person_id | toPersonId | string | Yes | ID of the destination person | +| amount | amount | int | Yes | Number of stamps to transfer | +| comments | comments | string | No | Transfer comments | + +#### Example: Transfer Stamps + +``` +// Transfer 100 stamps from master account to sub-account +params = StampTransactionParams( + from_person_id: "master-account-id", + to_person_id: "sub-account-id", + amount: 100, + comments: "Monthly stamp allocation" +) + +response = client.stamps.transfer_stamps(params) +``` + +#### Example: List Stamp Transactions + +``` +// Get paginated list of stamp transactions +response = client.stamps.get_list(page_number=1, page_size=10) + +for transaction in response.data.items: + print(f"Transfer: {transaction.amount} stamps from {transaction.from_person.legal_name}") +``` + --- ## Person Model Updates @@ -475,6 +555,14 @@ POST /api/{version}/invoices With `type_code: "N"` for payroll invoices. +### Stamp Endpoints + +``` +GET /api/{version}/stamps?pageNumber={n}&pageSize={s} +GET /api/{version}/stamps/{transactionId} +POST /api/{version}/stamps +``` + --- ## Field Mappings (JSON Aliases) @@ -692,22 +780,33 @@ response = client.invoices.create(invoice) ## Implementation Checklist -- [ ] Add `curp` field to Person model -- [ ] Create EmployeeData model -- [ ] Create EmployerData model -- [ ] Create InvoiceIssuerEmployerData model -- [ ] Create InvoiceRecipientEmployeeData model -- [ ] Create PayrollComplement and all sub-models -- [ ] Add employer_data to InvoiceIssuer model -- [ ] Add employee_data to InvoiceRecipient model -- [ ] Add payroll to InvoiceComplement model -- [ ] Create EmployeeService with CRUD operations -- [ ] Create EmployerService with CRUD operations -- [ ] Add employee and employer properties to PeopleService +### Models +- [x] Add `curp` field to Person model +- [x] Create EmployeeData model +- [x] Create EmployerData model +- [x] Create InvoiceIssuerEmployerData model +- [x] Create InvoiceRecipientEmployeeData model +- [x] Create PayrollComplement and all sub-models +- [x] Add employer_data to InvoiceIssuer model +- [x] Add employee_data to InvoiceRecipient model +- [x] Add payroll to InvoiceComplement model +- [x] Create StampTransaction model +- [x] Create StampTransactionParams model +- [x] Create UserLookupDto model + +### Services +- [x] Create EmployeeService with CRUD operations +- [x] Create EmployerService with CRUD operations +- [x] Add employee and employer properties to PeopleService +- [x] Create StampService with get_list, get_by_id, transfer_stamps, withdraw_stamps + +### Examples & Testing - [ ] Create examples for all 13 payroll types (by values) - [ ] Create examples for all 13 payroll types (by references) - [ ] Create setup data methods for by-references mode +- [ ] Create stamp service examples - [ ] Test all payroll types successfully +- [ ] Test stamp transactions successfully --- diff --git a/setup.py b/setup.py index e7b160d..e32dff1 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ import os from setuptools import setup, find_packages -VERSION = "4.0.270" +VERSION = "4.0.360" DESCRIPTION = "Genera facturas CFDI válidas ante el SAT consumiendo el API de https://www.fiscalapi.com" From ed178b6d151050af9a92abca490ecbe9299cf22e Mon Sep 17 00:00:00 2001 From: JesusMendoza Date: Sun, 25 Jan 2026 17:25:59 -0600 Subject: [PATCH 28/28] all samples tested and docs updated --- payroll-requirements.md | 820 ---------------------------------------- 1 file changed, 820 deletions(-) delete mode 100644 payroll-requirements.md diff --git a/payroll-requirements.md b/payroll-requirements.md deleted file mode 100644 index 1f23733..0000000 --- a/payroll-requirements.md +++ /dev/null @@ -1,820 +0,0 @@ -# FiscalAPI SDK - Payroll (CFDI Nomina) Requirements - -This document describes the requirements for implementing payroll invoice (CFDI de Nomina) support in FiscalAPI SDKs. It is language-agnostic and focuses on the models, services, and functionality needed. - ---- - -## Table of Contents - -1. [Overview](#overview) -2. [New Models Required](#new-models-required) -3. [New Services Required](#new-services-required) - - [EmployeeService](#1-employeeservice) - - [EmployerService](#2-employerservice) - - [PeopleService Updates](#3-peopleservice-updates) - - [StampService](#4-stampservice) -4. [Person Model Updates](#person-model-updates) -5. [Invoice Model Updates](#invoice-model-updates) -6. [Two Operation Modes](#two-operation-modes) -7. [Payroll Types Reference](#payroll-types-reference) -8. [API Endpoints](#api-endpoints) -9. [Field Mappings (JSON Aliases)](#field-mappings-json-aliases) -10. [Example Implementations](#example-implementations) - ---- - -## Overview - -Payroll invoices (CFDI de Nomina) in Mexico require: -- **Employer data** attached to the issuer (company paying wages) -- **Employee data** attached to the recipient (worker receiving wages) -- **Payroll complement** containing earnings, deductions, and other payment details - -The SDK must support **13 different payroll types** and **two operation modes**: -1. **By Values** - All data sent inline with the invoice request -2. **By References** - Only person IDs sent; employee/employer data pre-configured via API - ---- - -## New Models Required - -### 1. EmployeeData - -Data model for storing employee information as a sub-resource of Person. - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| employer_person_id | employerPersonId | string | ID of the employer person | -| employee_person_id | employeePersonId | string | ID of the employee person | -| social_security_number | socialSecurityNumber | string | NSS (Numero de Seguridad Social) | -| labor_relation_start_date | laborRelationStartDate | datetime | Employment start date | -| seniority | seniority | string | ISO 8601 duration (e.g., "P54W" = 54 weeks) | -| sat_contract_type_id | satContractTypeId | string | SAT contract type code | -| sat_unionized_status_id | satUnionizedStatusId | string | Unionized status code | -| sat_tax_regime_type_id | satTaxRegimeTypeId | string | Tax regime type code | -| sat_workday_type_id | satWorkdayTypeId | string | Workday type code | -| sat_job_risk_id | satJobRiskId | string | Job risk level code | -| sat_payment_periodicity_id | satPaymentPeriodicityId | string | Payment frequency code | -| employee_number | employeeNumber | string | Internal employee number | -| sat_bank_id | satBankId | string | Bank code | -| sat_payroll_state_id | satPayrollStateId | string | State code for payroll | -| department | department | string | Department name | -| position | position | string | Job position/title | -| bank_account | bankAccount | string | Bank account number | -| base_salary_for_contributions | baseSalaryForContributions | decimal | Base salary for social security | -| integrated_daily_salary | integratedDailySalary | decimal | Integrated daily salary | -| subcontractor_rfc | subcontractorRfc | string | RFC of subcontractor (if applicable) | -| time_percentage | timePercentage | decimal | Time percentage (for partial employment) | - -### 2. EmployerData - -Data model for storing employer information as a sub-resource of Person. - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| person_id | personId | string | ID of the employer person | -| employer_registration | employerRegistration | string | Registro Patronal (IMSS) | -| origin_employer_tin | originEmployerTin | string | RFC of origin employer | -| sat_fund_source_id | satFundSourceId | string | Fund source code | -| own_resource_amount | ownResourceAmount | decimal | Own resource amount | - -### 3. InvoiceIssuerEmployerData - -Employer data embedded in invoice issuer (for "by values" mode). - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| curp | curp | string | CURP of employer representative | -| employer_registration | employerRegistration | string | Registro Patronal | -| origin_employer_tin | originEmployerTin | string | RFC of origin employer | -| sat_fund_source_id | satFundSourceId | string | Fund source code | -| own_resource_amount | ownResourceAmount | decimal | Own resource amount | - -### 4. InvoiceRecipientEmployeeData - -Employee data embedded in invoice recipient (for "by values" mode). - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| curp | curp | string | CURP of employee | -| social_security_number | socialSecurityNumber | string | NSS | -| labor_relation_start_date | laborRelationStartDate | string | Start date (ISO format) | -| seniority | seniority | string | ISO 8601 duration | -| sat_contract_type_id | satContractTypeId | string | Contract type | -| sat_unionized_status_id | satUnionizedStatusId | string | Unionized status | -| sat_workday_type_id | satWorkdayTypeId | string | Workday type | -| sat_tax_regime_type_id | satTaxRegimeTypeId | string | Tax regime type | -| employee_number | employeeNumber | string | Employee number | -| department | department | string | Department | -| position | position | string | Position | -| sat_job_risk_id | satJobRiskId | string | Job risk code | -| sat_payment_periodicity_id | satPaymentPeriodicityId | string | Payment periodicity | -| sat_bank_id | satBankId | string | Bank code | -| bank_account | bankAccount | string | Bank account | -| base_salary_for_contributions | baseSalaryForContributions | decimal | Base salary | -| integrated_daily_salary | integratedDailySalary | decimal | Integrated salary | -| sat_payroll_state_id | satPayrollStateId | string | Payroll state | - -### 5. Payroll Complement Models - -#### PayrollComplement (main container) - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| version | version | string | Always "1.2" | -| payroll_type_code | payrollTypeCode | string | "O" (ordinary) or "E" (extraordinary) | -| payment_date | paymentDate | string | Payment date (ISO format) | -| initial_payment_date | initialPaymentDate | string | Period start date | -| final_payment_date | finalPaymentDate | string | Period end date | -| days_paid | daysPaid | decimal | Days paid in period | -| earnings | earnings | PayrollEarningsComplement | Earnings container | -| deductions | deductions | PayrollDeduction[] | List of deductions | -| disabilities | disabilities | PayrollDisability[] | List of disabilities | - -#### PayrollEarningsComplement - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| earnings | earnings | PayrollEarning[] | List of earnings | -| other_payments | otherPayments | PayrollOtherPayment[] | Other payments (subsidies, etc.) | -| retirement | retirement | PayrollRetirement | Retirement/pension data | -| severance | severance | PayrollSeverance | Severance/indemnization data | - -#### PayrollEarning - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| earning_type_code | earningTypeCode | string | SAT earning type code | -| code | code | string | Internal code | -| concept | concept | string | Description | -| taxed_amount | taxedAmount | decimal | Taxable amount | -| exempt_amount | exemptAmount | decimal | Exempt amount | -| stock_options | stockOptions | PayrollStockOptions | Stock options (if applicable) | -| overtime | overtime | PayrollOvertime[] | Overtime hours | - -#### PayrollDeduction - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| deduction_type_code | deductionTypeCode | string | SAT deduction type code | -| code | code | string | Internal code | -| concept | concept | string | Description | -| amount | amount | decimal | Deduction amount | - -#### PayrollOtherPayment - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| other_payment_type_code | otherPaymentTypeCode | string | SAT other payment type code | -| code | code | string | Internal code | -| concept | concept | string | Description | -| amount | amount | decimal | Payment amount | -| subsidy_caused | subsidyCaused | decimal | Employment subsidy caused | -| balance_compensation | balanceCompensation | PayrollBalanceCompensation | Balance compensation | - -#### PayrollOvertime - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| days | days | int | Days with overtime | -| hours_type_code | hoursTypeCode | string | "01" (double) or "02" (triple) | -| extra_hours | extraHours | int | Number of extra hours | -| amount_paid | amountPaid | decimal | Amount paid | - -#### PayrollDisability - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| disability_days | disabilityDays | int | Days of disability | -| disability_type_code | disabilityTypeCode | string | SAT disability type code | -| monetary_amount | monetaryAmount | decimal | Monetary amount | - -#### PayrollRetirement - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| total_one_time | totalOneTime | decimal | One-time payment total | -| total_installments | totalInstallments | decimal | Installments total | -| daily_amount | dailyAmount | decimal | Daily amount | -| accumulable_income | accumulableIncome | decimal | Accumulable income | -| non_accumulable_income | nonAccumulableIncome | decimal | Non-accumulable income | - -#### PayrollSeverance - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| total_paid | totalPaid | decimal | Total paid | -| years_of_service | yearsOfService | int | Years of service | -| last_monthly_salary | lastMonthlySalary | decimal | Last monthly salary | -| accumulable_income | accumulableIncome | decimal | Accumulable income | -| non_accumulable_income | nonAccumulableIncome | decimal | Non-accumulable income | - ---- - -## New Services Required - -### 1. EmployeeService - -Sub-service of PeopleService for managing employee data. - -``` -Endpoint pattern: people/{personId}/employee -``` - -| Method | HTTP | Endpoint | Description | -|--------|------|----------|-------------| -| get_by_id(person_id) | GET | people/{personId}/employee | Get employee data | -| create(employee) | POST | people/{employeePersonId}/employee | Create employee data | -| update(employee) | PUT | people/{employeePersonId}/employee | Update employee data | -| delete(person_id) | DELETE | people/{personId}/employee | Delete employee data | - -### 2. EmployerService - -Sub-service of PeopleService for managing employer data. - -``` -Endpoint pattern: people/{personId}/employer -``` - -| Method | HTTP | Endpoint | Description | -|--------|------|----------|-------------| -| get_by_id(person_id) | GET | people/{personId}/employer | Get employer data | -| create(employer) | POST | people/{personId}/employer | Create employer data | -| update(employer) | PUT | people/{personId}/employer | Update employer data | -| delete(person_id) | DELETE | people/{personId}/employer | Delete employer data | - -### 3. PeopleService Updates - -Add employee and employer sub-services as properties: - -``` -client.people.employee -> EmployeeService -client.people.employer -> EmployerService -``` - -### 4. StampService - -Service for managing stamp transactions (timbres fiscales). Stamps are the digital fiscal tokens required to certify CFDI invoices. - -``` -Endpoint pattern: stamps -``` - -| Method | HTTP | Endpoint | Description | -|--------|------|----------|-------------| -| get_list(page_number, page_size) | GET | stamps?pageNumber={n}&pageSize={s} | List stamp transactions with pagination | -| get_by_id(transaction_id) | GET | stamps/{transactionId} | Get a stamp transaction by ID | -| transfer_stamps(request) | POST | stamps | Transfer stamps from one person to another | -| withdraw_stamps(request) | POST | stamps | Withdraw stamps (alias for transfer_stamps) | - -#### Stamp Models - -##### UserLookupDto - -Lookup DTO for user/person references in stamp transactions. - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| tin | tin | string | RFC of the user | -| legal_name | legalName | string | Legal name of the user | - -##### StampTransaction - -Represents a stamp transfer/withdrawal transaction. - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| consecutive | consecutive | int | Transaction consecutive number | -| from_person | fromPerson | UserLookupDto | Source person of the transfer | -| to_person | toPerson | UserLookupDto | Destination person of the transfer | -| amount | amount | int | Number of stamps transferred | -| transaction_type | transactionType | int | Type of transaction | -| transaction_status | transactionStatus | int | Status of the transaction | -| reference_id | referenceId | string | Transaction reference ID | -| comments | comments | string | Transaction comments | - -##### StampTransactionParams - -Request parameters for stamp transfer/withdraw operations. - -| Field | JSON Alias | Type | Required | Description | -|-------|------------|------|----------|-------------| -| from_person_id | fromPersonId | string | Yes | ID of the source person | -| to_person_id | toPersonId | string | Yes | ID of the destination person | -| amount | amount | int | Yes | Number of stamps to transfer | -| comments | comments | string | No | Transfer comments | - -#### Example: Transfer Stamps - -``` -// Transfer 100 stamps from master account to sub-account -params = StampTransactionParams( - from_person_id: "master-account-id", - to_person_id: "sub-account-id", - amount: 100, - comments: "Monthly stamp allocation" -) - -response = client.stamps.transfer_stamps(params) -``` - -#### Example: List Stamp Transactions - -``` -// Get paginated list of stamp transactions -response = client.stamps.get_list(page_number=1, page_size=10) - -for transaction in response.data.items: - print(f"Transfer: {transaction.amount} stamps from {transaction.from_person.legal_name}") -``` - ---- - -## Person Model Updates - -Add the following field to the Person model: - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| curp | curp | string | CURP (Clave Unica de Registro de Poblacion) | - -**Important**: For payroll invoices, the recipient (employee) must have: -- `curp` field populated -- `sat_tax_regime_id` = "605" (Sueldos y Salarios) -- `sat_cfdi_use_id` = "CN01" (Nomina) - ---- - -## Invoice Model Updates - -### InvoiceIssuer - -Add field: - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| employer_data | employerData | InvoiceIssuerEmployerData | Employer data for payroll | - -### InvoiceRecipient - -Add field: - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| employee_data | employeeData | InvoiceRecipientEmployeeData | Employee data for payroll | - -### InvoiceComplement - -Add field: - -| Field | JSON Alias | Type | Description | -|-------|------------|------|-------------| -| payroll | payroll | PayrollComplement | Payroll complement data | - ---- - -## Two Operation Modes - -### Mode 1: By Values - -All employee/employer data is sent inline with the invoice request. - -**Characteristics:** -- Self-contained request -- No prior setup required -- Larger payload size -- Employee data goes in `recipient.employee_data` -- Employer data goes in `issuer.employer_data` - -**Example structure:** -``` -Invoice { - issuer: { - tin: "EKU9003173C9", - legal_name: "ESCUELA KEMPER URGATE", - tax_regime_code: "601", - employer_data: { - employer_registration: "B5510768108", - origin_employer_tin: "URE180429TM6" - } - }, - recipient: { - tin: "XOJI740919U48", - legal_name: "INGRID XODAR JIMENEZ", - tax_regime_code: "605", - cfdi_use_code: "CN01", - employee_data: { - curp: "XOJI850618MJCDNG09", - social_security_number: "000000", - seniority: "P54W", - ... - } - }, - complement: { - payroll: { ... } - } -} -``` - -### Mode 2: By References - -Only person IDs are sent; employee/employer data must be pre-configured. - -**Characteristics:** -- Smaller payload -- Requires prior setup of employee/employer data via API -- Person must have CURP and tax regime configured -- Uses only `id` field in issuer/recipient - -**Setup steps:** -1. Update person with CURP and tax regime (605 for employee, appropriate for employer) -2. Create EmployeeData via `client.people.employee.create()` -3. Create EmployerData via `client.people.employer.create()` - -**Example structure:** -``` -Invoice { - issuer: { - id: "2e7b988f-3a2a-4f67-86e9-3f931dd48581" - }, - recipient: { - id: "9367249f-f0ee-43f4-b771-da2fff3f185f" - }, - complement: { - payroll: { ... } - } -} -``` - ---- - -## Payroll Types Reference - -### 13 Standard Payroll Types - -| # | Name | payroll_type_code | Key Characteristics | -|---|------|-------------------|---------------------| -| 1 | Ordinaria | O | Regular salary payment | -| 2 | Asimilados | O | Similar to salary (honorarios asimilados) | -| 3 | Bonos y Fondo de Ahorro | O | Bonuses and savings fund | -| 4 | Horas Extra | O | Overtime payment | -| 5 | Incapacidades | O | Disability/sick leave | -| 6 | SNCF | O | Federal government (SNCF) | -| 7 | Extraordinaria | E | Extraordinary payment | -| 8 | Separacion e Indemnizacion | E | Severance and indemnization | -| 9 | Jubilacion, Pension, Retiro | E | Retirement/pension | -| 10 | Sin Deducciones | O | Without deductions | -| 11 | Subsidio Causado | O | Employment subsidy adjustment | -| 12 | Viaticos | O | Travel expenses | -| 13 | Basica | O | Basic payroll | - -### Common Invoice Fields for All Payroll Types - -``` -version_code: "4.0" -payment_method_code: "PUE" -currency_code: "MXN" -type_code: "N" -expedition_zip_code: -export_code: "01" -``` - -### SAT Earning Type Codes (TipoPercepcion) - -| Code | Description | Common Use | -|------|-------------|------------| -| 001 | Sueldos, Salarios Rayas y Jornales | Regular salary | -| 002 | Gratificación Anual (Aguinaldo) | Christmas bonus | -| 003 | Participación de los Trabajadores en las Utilidades PTU | Profit sharing | -| 019 | Horas extra | Overtime | -| 022 | Prima vacacional | Vacation bonus | -| 023 | Pagos por separación | Severance pay | -| 025 | Indemnizaciones | Indemnization | -| 028 | Comisiones | Commissions | -| 029 | Vales de despensa | Food vouchers | -| 039 | Jubilaciones, pensiones o haberes de retiro | Retirement | -| 044 | Jubilaciones, pensiones o haberes de retiro parcial | Partial retirement | -| 045 | Ingresos en acciones | Stock income | -| 046 | Ingresos asimilados a salarios | Income similar to salary | -| 047 | Alimentación | Food | -| 050 | Viáticos | Travel expenses | - -### SAT Deduction Type Codes (TipoDeduccion) - -| Code | Description | Common Use | -|------|-------------|------------| -| 001 | Seguridad social | Social security | -| 002 | ISR | Income tax | -| 003 | Aportaciones a retiro | Retirement contributions | -| 004 | Otros | Other deductions | -| 006 | Descuento por incapacidad | Disability discount | -| 010 | Pensión alimenticia | Alimony | -| 020 | Fondo de ahorro | Savings fund | -| 081 | Ajuste en viáticos | Travel expense adjustment | -| 107 | Ajuste al Subsidio Causado | Subsidy adjustment | - -### SAT Other Payment Type Codes (TipoOtroPago) - -| Code | Description | -|------|-------------| -| 001 | Reintegro de ISR pagado en exceso | -| 002 | Subsidio para el empleo | -| 003 | Viáticos | -| 004 | Aplicación de saldo a favor por compensación anual | -| 007 | ISR ajustado por subsidio | - ---- - -## API Endpoints - -### Employee Endpoints - -``` -GET /api/{version}/people/{personId}/employee -POST /api/{version}/people/{personId}/employee -PUT /api/{version}/people/{personId}/employee -DELETE /api/{version}/people/{personId}/employee -``` - -### Employer Endpoints - -``` -GET /api/{version}/people/{personId}/employer -POST /api/{version}/people/{personId}/employer -PUT /api/{version}/people/{personId}/employer -DELETE /api/{version}/people/{personId}/employer -``` - -### Invoice Endpoints - -Payroll invoices use the standard invoice endpoint: - -``` -POST /api/{version}/invoices -``` - -With `type_code: "N"` for payroll invoices. - -### Stamp Endpoints - -``` -GET /api/{version}/stamps?pageNumber={n}&pageSize={s} -GET /api/{version}/stamps/{transactionId} -POST /api/{version}/stamps -``` - ---- - -## Field Mappings (JSON Aliases) - -All models must serialize using camelCase JSON aliases when communicating with the API. - -**Serialization rules:** -- Use camelCase for JSON property names -- Exclude null/None values from JSON -- Decimal values should serialize as strings -- Dates should serialize as ISO 8601 strings - ---- - -## Example Implementations - -### Example 1: Create Payroll Invoice (By Values) - -``` -// Pseudocode - adapt to target language - -invoice = Invoice( - version_code: "4.0", - series: "F", - date: "2026-01-25T10:00:00", - payment_method_code: "PUE", - currency_code: "MXN", - type_code: "N", - expedition_zip_code: "20000", - export_code: "01", - - issuer: InvoiceIssuer( - tin: "EKU9003173C9", - legal_name: "ESCUELA KEMPER URGATE", - tax_regime_code: "601", - employer_data: InvoiceIssuerEmployerData( - employer_registration: "B5510768108", - origin_employer_tin: "URE180429TM6" - ) - ), - - recipient: InvoiceRecipient( - tin: "XOJI740919U48", - legal_name: "INGRID XODAR JIMENEZ", - zip_code: "76028", - tax_regime_code: "605", - cfdi_use_code: "CN01", - employee_data: InvoiceRecipientEmployeeData( - curp: "XOJI850618MJCDNG09", - social_security_number: "000000", - labor_relation_start_date: "2015-01-01", - seniority: "P437W", - sat_contract_type_id: "01", - sat_workday_type_id: "01", - sat_tax_regime_type_id: "02", - employee_number: "120", - department: "Desarrollo", - position: "Ingeniero de Software", - sat_job_risk_id: "1", - sat_payment_periodicity_id: "04", - sat_bank_id: "002", - bank_account: "1111111111", - base_salary_for_contributions: 490.22, - integrated_daily_salary: 146.47, - sat_payroll_state_id: "JAL" - ) - ), - - complement: InvoiceComplement( - payroll: PayrollComplement( - version: "1.2", - payroll_type_code: "O", - payment_date: "2023-05-24", - initial_payment_date: "2023-05-09", - final_payment_date: "2023-05-24", - days_paid: 15, - earnings: PayrollEarningsComplement( - earnings: [ - PayrollEarning( - earning_type_code: "001", - code: "00500", - concept: "Sueldos, Salarios Rayas y Jornales", - taxed_amount: 2808.80, - exempt_amount: 2191.20 - ) - ] - ), - deductions: [ - PayrollDeduction( - deduction_type_code: "001", - code: "00301", - concept: "Seguridad Social", - amount: 200.00 - ), - PayrollDeduction( - deduction_type_code: "002", - code: "00302", - concept: "ISR", - amount: 100.00 - ) - ] - ) - ) -) - -response = client.invoices.create(invoice) -``` - -### Example 2: Setup for By References Mode - -``` -// Step 1: Update person with CURP and tax regime -employee_person = Person( - id: "9367249f-f0ee-43f4-b771-da2fff3f185f", - curp: "XOJI850618MJCDNG09", - sat_tax_regime_id: "605", - sat_cfdi_use_id: "CN01" -) -client.people.update(employee_person) - -// Step 2: Create employee data -employee_data = EmployeeData( - employer_person_id: "2e7b988f-3a2a-4f67-86e9-3f931dd48581", - employee_person_id: "9367249f-f0ee-43f4-b771-da2fff3f185f", - social_security_number: "000000", - labor_relation_start_date: datetime(2015, 1, 1), - seniority: "P437W", - sat_contract_type_id: "01", - sat_workday_type_id: "01", - sat_tax_regime_type_id: "02", - employee_number: "120", - department: "Desarrollo", - position: "Ingeniero de Software", - sat_job_risk_id: "1", - sat_payment_periodicity_id: "04", - sat_bank_id: "002", - bank_account: "1111111111", - integrated_daily_salary: 146.47, - sat_payroll_state_id: "JAL" -) -client.people.employee.create(employee_data) - -// Step 3: Create employer data -employer_data = EmployerData( - person_id: "2e7b988f-3a2a-4f67-86e9-3f931dd48581", - employer_registration: "B5510768108", - origin_employer_tin: "URE180429TM6" -) -client.people.employer.create(employer_data) -``` - -### Example 3: Create Payroll Invoice (By References) - -``` -invoice = Invoice( - version_code: "4.0", - series: "F", - date: "2026-01-25T10:00:00", - payment_method_code: "PUE", - currency_code: "MXN", - type_code: "N", - expedition_zip_code: "20000", - export_code: "01", - - // Only IDs - data comes from pre-configured employee/employer - issuer: InvoiceIssuer( - id: "2e7b988f-3a2a-4f67-86e9-3f931dd48581" - ), - recipient: InvoiceRecipient( - id: "9367249f-f0ee-43f4-b771-da2fff3f185f" - ), - - complement: InvoiceComplement( - payroll: PayrollComplement( - // Same payroll data as by-values mode - version: "1.2", - payroll_type_code: "O", - payment_date: "2023-05-24", - initial_payment_date: "2023-05-09", - final_payment_date: "2023-05-24", - days_paid: 15, - earnings: PayrollEarningsComplement( - earnings: [ - PayrollEarning( - earning_type_code: "001", - code: "00500", - concept: "Sueldos, Salarios Rayas y Jornales", - taxed_amount: 2808.80, - exempt_amount: 2191.20 - ) - ] - ), - deductions: [ - PayrollDeduction( - deduction_type_code: "001", - code: "00301", - concept: "Seguridad Social", - amount: 200.00 - ), - PayrollDeduction( - deduction_type_code: "002", - code: "00302", - concept: "ISR", - amount: 100.00 - ) - ] - ) - ) -) - -response = client.invoices.create(invoice) -``` - ---- - -## Implementation Checklist - -### Models -- [x] Add `curp` field to Person model -- [x] Create EmployeeData model -- [x] Create EmployerData model -- [x] Create InvoiceIssuerEmployerData model -- [x] Create InvoiceRecipientEmployeeData model -- [x] Create PayrollComplement and all sub-models -- [x] Add employer_data to InvoiceIssuer model -- [x] Add employee_data to InvoiceRecipient model -- [x] Add payroll to InvoiceComplement model -- [x] Create StampTransaction model -- [x] Create StampTransactionParams model -- [x] Create UserLookupDto model - -### Services -- [x] Create EmployeeService with CRUD operations -- [x] Create EmployerService with CRUD operations -- [x] Add employee and employer properties to PeopleService -- [x] Create StampService with get_list, get_by_id, transfer_stamps, withdraw_stamps - -### Examples & Testing -- [ ] Create examples for all 13 payroll types (by values) -- [ ] Create examples for all 13 payroll types (by references) -- [ ] Create setup data methods for by-references mode -- [ ] Create stamp service examples -- [ ] Test all payroll types successfully -- [ ] Test stamp transactions successfully - ---- - -## Notes - -1. **Seniority format**: Use ISO 8601 duration format (e.g., "P437W" for 437 weeks) -2. **Tax regime for employees**: Always "605" (Sueldos y Salarios) -3. **CFDI use for payroll**: Always "CN01" (Nomina) -4. **Invoice type**: Always "N" for payroll invoices -5. **Payment method**: Typically "PUE" (Pago en Una sola Exhibicion) -6. **Payroll complement version**: Always "1.2"