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: | diff --git a/CLAUDE.md b/CLAUDE.md index 6c8560b..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,21 +104,69 @@ 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.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+ +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) + +### 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/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/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 new file mode 100644 index 0000000..13fb233 --- /dev/null +++ b/ejemplos-facturas-de-complemento-pago.py @@ -0,0 +1,252 @@ +""" +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 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-facturas-de-nomina.py b/ejemplos-facturas-de-nomina.py new file mode 100644 index 0000000..727f8f2 --- /dev/null +++ b/ejemplos-facturas-de-nomina.py @@ -0,0 +1,3635 @@ +""" +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, + PayrollOvertime, + PayrollDisability, + PayrollSeverance, + PayrollRetirement, + PayrollBalanceCompensation, + TaxCredential, + EmployeeData, + EmployerData, + Person +) +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 +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" + +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 (Facturación por valores) +# ============================================================================ +def create_nomina_ordinaria_values(): + """ + 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( + 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 (Facturación por valores) +# ============================================================================ +def create_nomina_asimilados_values(): + """ + 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-25T10:00:00", + 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" + ), + 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 (Facturación por valores) +# ============================================================================ +def create_nomina_bonos_fondo_ahorro_values(): + """ + 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-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="Z0000001234" + ), + 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="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 valores) +# ============================================================================ +def create_nomina_horas_extra_values(): + """ + 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-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" + ), + 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 (Facturación por valores) +# ============================================================================ +def create_nomina_incapacidades_values(): + """ + 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-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" + ), + 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) (Facturación por valores) +# ============================================================================ +def create_nomina_sncf_values(): + """ + 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-25T10:00:00", + 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") + ), + 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 valores) +# ============================================================================ +def create_nomina_extraordinaria_values(): + """ + 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-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" + ), + 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 (Facturación por valores) +# ============================================================================ +def create_nomina_separacion_indemnizacion_values(): + """ + 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-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" + ), + 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") + ), + 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 valores) +# ============================================================================ +def create_nomina_jubilacion_pension_retiro_values(): + """ + 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-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" + ), + 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 (Facturación por valores) +# ============================================================================ +def create_nomina_sin_deducciones_values(): + """ + 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-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" + ), + 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 (Facturación por valores) +# ============================================================================ +def create_nomina_subsidio_causado_values(): + """ + 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-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" + ), + 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 (Facturación por valores) +# ============================================================================ +def create_nomina_viaticos_values(): + """ + 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-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" + ), + 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 (Facturación por valores) +# ============================================================================ +def create_nomina_basica_values(): + """ + 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-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" + ), + 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(f"Response: {api_response}") + return api_response + + + +#********************(Facturación por referencias)**************************** +#********************(Facturación por referencias)**************************** +#********************(Facturación por referencias)**************************** + +# ============================================================================ +# 1. NOMINA ORDINARIA (Facturación por referencias) +# ============================================================================ +def create_nomina_ordinaria_references_setup_data(): + """ + Configura los datos de empleado/empleador para nomina ordinaria por referencias. + """ + print("\n" + "="*60) + print("SETUP: 1. NOMINA ORDINARIA (Referencias)") + print("="*60) + + # 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) + + # 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("3. NOMINA CON BONOS, FONDO AHORRO Y 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-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") + print("="*60) + + +if __name__ == "__main__": + main() diff --git a/ejemplos-timbres.py b/ejemplos-timbres.py new file mode 100644 index 0000000..480a10d --- /dev/null +++ b/ejemplos-timbres.py @@ -0,0 +1,132 @@ +""" +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="" +) + +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=OSCAR_KALA_HAAK, + to_person_id=karla_fuente_nolasco_id, + amount=1, + 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. + """ + print("\n" + "=" * 60) + print("4. RETIRAR TIMBRES") + print("=" * 60) + + params = StampTransactionParams( + from_person_id=OSCAR_KALA_HAAK, + to_person_id=xochilt_casas_chavez_id, + amount=1, + comments="Retiro de timbres 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 + transferir_timbres() + + # 4. Retirar timbres + retirar_timbres() + + +if __name__ == "__main__": + main() diff --git a/examples.py b/examples.py index a872a23..d54a5d3 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 (): @@ -10,15 +8,12 @@ def main (): settings = FiscalApiSettings( api_url="https://test.fiscalapi.com", - api_key="", - tenant="", + #api_key="", + #tenant="", ) client = FiscalApiClient(settings=settings) - - - - + # listar api-keys # api_response = client.api_keys.get_list(1, 10) # print(api_response) @@ -1357,8 +1352,27 @@ 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 + + + + # ======================================== # EJEMPLOS DE DESCARGA MASIVA # ======================================== @@ -1466,14 +1480,11 @@ 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) if __name__ == "__main__": main() - - -if __name__ == "__main__": - main() \ No newline at end of file + \ No newline at end of file diff --git a/fiscalapi/__init__.py b/fiscalapi/__init__.py index 402dd6d..6800484 100644 --- a/fiscalapi/__init__.py +++ b/fiscalapi/__init__.py @@ -1,31 +1,73 @@ -# 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 ( + # Product models ProductTax, Product, + # Person models Person, + EmployeeData, + EmployerData, 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, + # Complement models + LocalTax, + LocalTaxesComplement, + PaymentComplement, + PayrollStockOptions, + PayrollOvertime, + PayrollEarning, + PayrollBalanceCompensation, + PayrollOtherPayment, + PayrollRetirement, + PayrollSeverance, + PayrollEarningsComplement, + PayrollDeduction, + PayrollDisability, + PayrollComplement, + LadingComplement, + InvoiceComplement, + # Invoice models InvoiceResponse, Invoice, CancelInvoiceRequest, @@ -35,10 +77,13 @@ SendInvoiceRequest, InvoiceStatusRequest, InvoiceStatusResponse, + # API Key models ApiKey, + # Download models DownloadRule, DownloadRequest, MetadataItem, + # XML models XmlGlobalInformation, XmlIssuer, XmlRecipient, @@ -50,45 +95,83 @@ XmlItem, XmlComplement, Xml, + # Stamp models + UserLookupDto, + StampTransaction, + StampTransactionParams, ) -# 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 +from .services.stamp_service import StampService -# 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", + "PagedList", + "ValidationFailure", + # Product models "ProductTax", "Product", + # Person models "Person", + "EmployeeData", + "EmployerData", "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", + # Complement models + "LocalTax", + "LocalTaxesComplement", + "PaymentComplement", + "PayrollStockOptions", + "PayrollOvertime", + "PayrollEarning", + "PayrollBalanceCompensation", + "PayrollOtherPayment", + "PayrollRetirement", + "PayrollSeverance", + "PayrollEarningsComplement", + "PayrollDeduction", + "PayrollDisability", + "PayrollComplement", + "LadingComplement", + "InvoiceComplement", + # Invoice models "InvoiceResponse", "Invoice", "CancelInvoiceRequest", @@ -98,10 +181,13 @@ "SendInvoiceRequest", "InvoiceStatusRequest", "InvoiceStatusResponse", + # API Key models "ApiKey", + # Download models "DownloadRule", "DownloadRequest", "MetadataItem", + # XML models "XmlGlobalInformation", "XmlIssuer", "XmlRecipient", @@ -113,18 +199,24 @@ "XmlItem", "XmlComplement", "Xml", - + # Stamp models + "UserLookupDto", + "StampTransaction", + "StampTransactionParams", # Servicios + "BaseService", + "ApiKeyService", "CatalogService", + "DownloadCatalogService", + "DownloadRequestService", + "DownloadRuleService", + "EmployeeService", + "EmployerService", "InvoiceService", "PeopleService", "ProductService", "TaxFileService", - "ApiKeyService", - "DownloadCatalogService", - "DownloadRuleService", - "DownloadRequestService", - + "StampService", # Cliente principal "FiscalApiClient", ] diff --git a/fiscalapi/models/__init__.py b/fiscalapi/models/__init__.py index e69de29..26498df 100644 --- a/fiscalapi/models/__init__.py +++ b/fiscalapi/models/__init__.py @@ -0,0 +1,174 @@ +"""Modelos de FiscalAPI.""" + +from .common_models import ( + ApiResponse, + BaseDto, + CatalogDto, + FiscalApiSettings, + PagedList, + ValidationFailure, +) +from .fiscalapi_models import ( + # Product models + ProductTax, + Product, + # Person models + Person, + EmployeeData, + EmployerData, + 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, + # 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, + # API Key models + ApiKey, + # Download models + DownloadRule, + DownloadRequest, + MetadataItem, + # XML models + XmlGlobalInformation, + XmlIssuer, + XmlRecipient, + XmlRelated, + XmlTax, + XmlItemCustomsInformation, + XmlItemPropertyAccount, + XmlItemTax, + XmlItem, + XmlComplement, + Xml, + # Stamp models + UserLookupDto, + StampTransaction, + StampTransactionParams, +) + +__all__ = [ + # common_models + "ApiResponse", + "BaseDto", + "CatalogDto", + "FiscalApiSettings", + "PagedList", + "ValidationFailure", + # Product models + "ProductTax", + "Product", + # Person models + "Person", + "EmployeeData", + "EmployerData", + "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", + # 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", + # API Key models + "ApiKey", + # Download models + "DownloadRule", + "DownloadRequest", + "MetadataItem", + # XML models + "XmlGlobalInformation", + "XmlIssuer", + "XmlRecipient", + "XmlRelated", + "XmlTax", + "XmlItemCustomsInformation", + "XmlItemPropertyAccount", + "XmlItemTax", + "XmlItem", + "XmlComplement", + "Xml", + # Stamp models + "UserLookupDto", + "StampTransaction", + "StampTransactionParams", +] diff --git a/fiscalapi/models/common_models.py b/fiscalapi/models/common_models.py index ae12225..4d11bcf 100644 --- a/fiscalapi/models/common_models.py +++ b/fiscalapi/models/common_models.py @@ -1,15 +1,16 @@ 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") - message: Optional[str] = Field(alias="message") - details: Optional[str] = Field(alias="details") - data: Optional[T] = Field(None, alias="data") + 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") + http_status_code: Optional[int] = Field(default=None, alias="httpStatusCode") model_config = ConfigDict( populate_by_name=True, @@ -19,23 +20,23 @@ 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") - 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") + items: list[T] = Field(default_factory=list, alias="items", description="Lista de elementos paginados") + 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) 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] = 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) class BaseDto(BaseModel): @@ -48,22 +49,21 @@ class BaseDto(BaseModel): class CatalogDto(BaseDto): """Modelo para catálogos.""" - description: str = Field(alias="description") - - model_config = ConfigDict(populate_by_name=True) + description: str = Field(default=..., alias="description") 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.") - 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..9ce1776 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 pydantic import BaseModel, 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 @@ -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") @@ -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.") @@ -77,15 +78,64 @@ 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[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") + 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).""" 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.") @@ -99,11 +149,51 @@ 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[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") + 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(..., 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) + class InvoiceIssuer(BaseDto): """Modelo para el emisor de la factura.""" @@ -111,65 +201,118 @@ 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).") + 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) + 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.") 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(..., 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: 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(..., 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.") + 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.""" - 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( @@ -179,20 +322,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.") - 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.") + tax_object_code: str = Field(default=..., alias="taxObjectCode", description="Código de obligaciones de impuesto.") + 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( populate_by_name=True, @@ -203,17 +346,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.") + 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)") + 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, @@ -222,6 +365,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.") @@ -244,36 +560,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(..., 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.") + # 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(..., 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: 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).") + 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.") - + related_invoices: Optional[list[RelatedInvoice]] = Field(default=None, alias="relatedInvoices", description="Facturas relacionadas.") + 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}) @@ -282,31 +605,28 @@ 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.") + 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.") + 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.") - class Config: - populate_by_name = True + model_config = ConfigDict(populate_by_name=True) 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.") - 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,20 +634,18 @@ 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.") - 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.") - class Config: - populate_by_name = True + model_config = ConfigDict(populate_by_name=True) class InvoiceStatusRequest(BaseDto): @@ -339,22 +657,20 @@ 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.""" - 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 = { - "populate_by_name": True - } + model_config = ConfigDict(populate_by_name=True) @@ -448,7 +764,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 +912,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 +1007,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 +1019,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.") @@ -717,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/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..79c75c5 100644 --- a/fiscalapi/services/__init__.py +++ b/fiscalapi/services/__init__.py @@ -0,0 +1,33 @@ +"""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 .stamp_service import StampService +from .tax_file_service import TaxFileService + +__all__ = [ + "BaseService", + "ApiKeyService", + "CatalogService", + "DownloadCatalogService", + "DownloadRequestService", + "DownloadRuleService", + "EmployeeService", + "EmployerService", + "FiscalApiClient", + "InvoiceService", + "PeopleService", + "ProductService", + "StampService", + "TaxFileService", +] diff --git a/fiscalapi/services/base_service.py b/fiscalapi/services/base_service.py index 4172b89..9405820 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 @@ -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, @@ -25,102 +25,31 @@ def _get_headers(self) -> dict: "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() 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,10 +105,10 @@ 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=400, + http_status_code=status_code, message=generic_error.message, details=details_str, data=None @@ -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) -> 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" - + 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/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/download_request_service.py b/fiscalapi/services/download_request_service.py index 7fee886..93f3a50 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 @@ -39,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) @@ -134,7 +125,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 +133,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 +142,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,23 +182,16 @@ 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. - + 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 + ApiResponse[list[DownloadRequest]]: Lista de solicitudes encontradas """ - 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 + return self.send_request("GET", endpoint, list[DownloadRequest]) \ No newline at end of file diff --git a/fiscalapi/services/download_rule_service.py b/fiscalapi/services/download_rule_service.py 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 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..f0adcc3 100644 --- a/fiscalapi/services/fiscalapi_client.py +++ b/fiscalapi/services/fiscalapi_client.py @@ -3,11 +3,12 @@ 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 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/invoice_service.py b/fiscalapi/services/invoice_service.py index 9f5a14d..860ae40 100644 --- a/fiscalapi/services/invoice_service.py +++ b/fiscalapi/services/invoice_service.py @@ -1,91 +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 is None: - raise ValueError("request_model cannot be null") + """ + 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 - endpoint = self._get_endpoint_by_type(invoice.type_code) + 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]: - if cancel_invoice_request is None: - raise ValueError("request_model cannot be null") - + """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]: - if create_pdf_request is None: - raise ValueError("request_model cannot be null") - + + 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]: - if invoice_id is None: - raise ValueError("invoice_id cannot be null") - + + 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): - if not send_invoice_request: - raise ValueError("Invalid request") - + + 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 - """ - if request is None: - raise ValueError("request cannot be null") - + """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 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/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) 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/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"] diff --git a/setup.py b/setup.py index a02b494..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" @@ -30,9 +30,13 @@ 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.7", + python_requires=">=3.9", install_requires=[ "pydantic>=2.0.0", @@ -46,7 +50,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", ], )