Skip to content

Commit f2c338c

Browse files
committed
invoice service list and get by id added
1 parent 69b94b0 commit f2c338c

File tree

5 files changed

+186
-10
lines changed

5 files changed

+186
-10
lines changed

fiscalapi/models/common_models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import datetime
1+
from datetime import datetime
22
from typing import Any, Generic, List, Optional, TypeVar
33
from pydantic import BaseModel, ConfigDict, Field
44
from pydantic.alias_generators import to_snake
@@ -41,8 +41,8 @@ class ValidationFailure(BaseModel):
4141
class BaseDto(BaseModel):
4242
"""Modelo base para DTOs."""
4343
id: Optional[str] = Field(default=None, alias="id")
44-
created_at: Optional[datetime.datetime] = Field(default=None, alias="createdAt")
45-
updated_at: Optional[datetime.datetime] = Field(default=None, alias="updatedAt")
44+
created_at: Optional[datetime] = Field(default=None, alias="createdAt")
45+
updated_at: Optional[datetime] = Field(default=None, alias="updatedAt")
4646

4747
model_config = ConfigDict(populate_by_name=True)
4848

fiscalapi/models/fiscalapi_models.py

Lines changed: 151 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import datetime
21
from decimal import Decimal
32
from pydantic import ConfigDict, EmailStr, Field
43
from fiscalapi.models.common_models import BaseDto, CatalogDto
5-
from typing import Literal, Optional
4+
from typing import List, Literal, Optional
5+
from datetime import datetime
6+
7+
# products models
68

79
class ProductTax(BaseDto):
810
"""Modelo impuesto de producto."""
@@ -47,7 +49,7 @@ class Product(BaseDto):
4749
json_encoders={Decimal: str}
4850
)
4951

50-
52+
# people models
5153

5254
class Person(BaseDto):
5355
"""Modelo persona en FiscalAPI."""
@@ -85,10 +87,153 @@ class TaxFile(BaseDto):
8587
base64_file: Optional[str] = Field(default=None, alias="base64File", description="Archivo certificado o llave privada en formato base64.")
8688
file_type: Literal[0, 1] = Field(default=None, alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.")
8789
password: Optional[str] = Field(default=None, alias="password", description="Contraseña de la llave privada.")
88-
valid_from: Optional[datetime.datetime] = Field(default=None, alias="validFrom", description="Fecha de inicio de vigencia del certificado o llave privada.")
89-
valid_to: Optional[datetime.datetime] = Field(default=None, alias="validTo", description="Fecha de fin de vigencia del certificado o llave privada.")
90+
valid_from: Optional[datetime] = Field(default=None, alias="validFrom", description="Fecha de inicio de vigencia del certificado o llave privada.")
91+
valid_to: Optional[datetime] = Field(default=None, alias="validTo", description="Fecha de fin de vigencia del certificado o llave privada.")
9092
sequence: Optional[int] = Field(default=None, alias="sequence", description="Numero de secuencia que identifica el par entre certificado y llave privada.")
9193

9294
model_config = ConfigDict(
9395
populate_by_name=True
94-
)
96+
)
97+
98+
99+
# invoices models
100+
101+
102+
class TaxCredential(BaseDto):
103+
"""Modelo para los sellos del emisor (archivos .cer y .key)."""
104+
base64_file: str = Field(..., alias="base64File", description="Archivo en formato base64.")
105+
file_type: Literal[0, 1] = Field(..., alias="fileType", description="Tipo de archivo: 0 para certificado, 1 para llave privada.")
106+
password: str = Field(..., alias="password", description="Contraseña del archivo .key independientemente de si es un archivo .cer o .key.")
107+
108+
class InvoiceIssuer(BaseDto):
109+
"""Modelo para el emisor de la factura."""
110+
id: Optional[str] = Field(default=None, alias="id", description="ID de la persona (emisora) en fiscalapi.")
111+
tin: Optional[str] = Field(default=None, alias="tin", description="RFC del emisor (Tax Identification Number).")
112+
legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del emisor sin regimen de capital.")
113+
tax_regime_code: Optional[str] = Field(default=None, alias="taxRegimeCode", description="Código del régimen fiscal del emisor.")
114+
tax_credentials: Optional[List[TaxCredential]] = Field(default=None, alias="taxCredentials", description="Sellos del emisor (archivos .cer y .key).")
115+
116+
class InvoiceRecipient(BaseDto):
117+
"""Modelo para el receptor de la factura."""
118+
id: Optional[str] = Field(default=None, alias="id", description="ID de la persona (receptora) en fiscalapi.")
119+
tin: Optional[str] = Field(default=None, alias="tin", description="RFC del receptor (Tax Identification Number).")
120+
legal_name: Optional[str] = Field(default=None, alias="legalName", description="Razón social del receptor sin regimen de capital.")
121+
tax_regime_code: Optional[str] = Field(default=None, alias="taxRegimeCode", description="Código del régimen fiscal del receptor.")
122+
cfdi_use_code: Optional[str] = Field(default=None, alias="cfdiUseCode", description="Código del uso CFDI.")
123+
email: Optional[str] = Field(default=None, description="Correo electrónico del receptor.")
124+
125+
class ItemTax(BaseDto):
126+
"""Modelo para los impuestos aplicables a un producto o servicio."""
127+
tax_code: str = Field(..., alias="taxCode", description="Código del impuesto.")
128+
tax_type_code: str = Field(..., alias="taxTypeCode", description="Tipo de factor.")
129+
tax_rate: Decimal = Field(..., alias="taxRate", description="Tasa del impuesto.")
130+
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.")
131+
132+
133+
class InvoiceItem(BaseDto):
134+
"""Modelo para los conceptos de la factura (productos o servicios)."""
135+
id: Optional[str] = Field(default=None, alias="id", description="ID del producto en fiscalapi.")
136+
item_code: Optional[str] = Field(default=None, alias="itemCode", description="Código SAT del producto o servicio.")
137+
quantity: Decimal = Field(..., alias="quantity", description="Cantidad del producto o servicio.")
138+
discount: Optional[Decimal] = Field(default=None, alias="discount", description="Cantidad monetaria del descuento aplicado.")
139+
unit_of_measurement_code: Optional[str] = Field(default=None, alias="unitOfMeasurementCode", description="Código SAT de la unidad de medida.")
140+
description: str = Field(..., alias="description", description="Descripción del producto o servicio.")
141+
unit_price: Optional[Decimal] = Field(default=None, alias="unitPrice", description="Precio unitario del producto o servicio.")
142+
tax_object_code: Optional[str] = Field(default=None, alias="taxObjectCode", description="Código SAT de obligaciones de impuesto.")
143+
item_sku: Optional[str] = Field(default=None, alias="itemSku", description="SKU o clave del sistema externo.")
144+
item_taxes: Optional[List[ItemTax]] = Field(default=None, alias="itemTaxes", description="Impuestos aplicables al producto o servicio.")
145+
146+
class GlobalInformation(BaseDto):
147+
"""Modelo para la información global de la factura global."""
148+
periodicity_code: str = Field(..., alias="periodicityCode", description="Código SAT de la periodicidad de la factura global.")
149+
month_code: str = Field(..., alias="monthCode", description="Código SAT del mes de la factura global.")
150+
year: int = Field(..., description="Año de la factura global a 4 dígitos.")
151+
152+
class RelatedInvoice(BaseDto):
153+
"""Modelo para representar la relacion entre la factura actual y otras facturas previas."""
154+
relationship_type_code: str = Field(..., alias="relationshipTypeCode", description="Código de la relación de la factura relacionada.")
155+
uuid: str = Field(..., description="UUID de la factura relacionada.")
156+
157+
class InvoicePayment(BaseDto):
158+
"""Modelo para los pagos recibidos para liquidar la factura."""
159+
payment_date: str = Field(..., alias="paymentDate", description="Fecha de pago.")
160+
payment_form_code: str = Field(..., alias="paymentFormCode", description="Código de la forma de pago.")
161+
162+
currency_code: Literal ["MXN", "USD", "EUR"] = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en el pago.")
163+
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..")
164+
amount: Decimal = Field(..., description="Monto del pago.")
165+
source_bank_tin: str = Field(..., alias="sourceBankTin", description="RFC del banco origen. (Rfc del banco emisor del pago)")
166+
source_bank_account: str = Field(..., alias="sourceBankAccount", description="Cuenta bancaria origen. (Cuenta bancaria del banco emisor del pago)")
167+
target_bank_tin: str = Field(..., alias="targetBankTin", description="RFC del banco destino. (Rfc del banco receptor del pago)")
168+
target_bank_account: str = Field(..., alias="targetBankAccount", description="Cuenta bancaria destino (Cuenta bancaria del banco receptor del pago)")
169+
170+
class PaidInvoiceTax(BaseDto):
171+
"""Modelo para los impuestos aplicables a la factura pagada."""
172+
base: Decimal = Field(..., description="Base del impuesto.")
173+
tax_code: str = Field(..., alias="taxCode", description="Código del impuesto.")
174+
tax_type_code: str = Field(..., alias="taxTypeCode", description="Tipo de factor.")
175+
tax_rate: Decimal = Field(..., alias="taxRate", description="Tasa del impuesto.")
176+
tax_flag_code: Literal["T", "R"] = Field(..., alias="taxFlagCode", description="Código que indica la naturaleza del impuesto.")
177+
178+
class PaidInvoice(BaseDto):
179+
"""Modelo para las facturas pagadas con el pago recibido."""
180+
uuid: str = Field(..., alias="uuid", description="UUID de la factura pagada.")
181+
series: str = Field(..., alias="series", description="Serie de la factura pagada.")
182+
amount: Decimal = Field(..., alias="amount", description="Monto pagado de la factura.")
183+
number: str = Field(..., alias="number", description="Folio de la factura pagada.")
184+
currency_code: str = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada en la factura pagada.")
185+
partiality_number: int = Field(..., alias="partialityNumber", description="Número de parcialidad.")
186+
sub_total: Decimal = Field(..., alias="subTotal", description="Subtotal de la factura pagada.")
187+
previous_balance: Decimal = Field(..., alias="previousBalance", description="Saldo anterior de la factura pagada.")
188+
remaining_balance: Decimal = Field(..., alias="remainingBalance", description="Saldo restante de la factura pagada.")
189+
tax_object_code: str = Field(..., alias="taxObjectCode", description="Código de obligaciones de impuesto.")
190+
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).")
191+
paid_invoice_taxes: List[PaidInvoiceTax] = Field(..., alias="paidInvoiceTaxes", description="Impuestos aplicables a la factura pagada.")
192+
193+
194+
class InvoiceResponse(BaseDto):
195+
"""Modelo para la respuesta del SAT después del timbrado de la factura."""
196+
id: Optional[str] = Field(default=None, description="ID de la respuesta.")
197+
invoice_id: Optional[str] = Field(default=None, alias="invoiceId", description="ID de la factura a la que pertenece la respuesta.")
198+
invoice_uuid: Optional[str] = Field(default=None, alias="invoiceUuid", description="Folio Fiscal (UUID) proporcionado por el SAT tras el timbrado de la factura.")
199+
invoice_certificate_number: Optional[str] = Field(default=None, alias="invoiceCertificateNumber", description="Número de certificado del emisor.")
200+
invoice_base64_sello: Optional[str] = Field(default=None, alias="invoiceBase64Sello", description="Sello digital del CFDI en formato Base64.")
201+
invoice_signature_date: Optional[datetime] = Field(default=None, alias="invoiceSignatureDate", description="Fecha y hora de la firma electrónica del CFDI por parte del emisor.")
202+
invoice_base64_qr_code: Optional[str] = Field(default=None, alias="invoiceBase64QrCode", description="Imagen del código QR en formato Base64.")
203+
invoice_base64: Optional[str] = Field(default=None, alias="invoiceBase64", description="XML de la factura en formato Base64.")
204+
sat_base64_sello: Optional[str] = Field(default=None, alias="satBase64Sello", description="Sello digital del SAT en formato Base64.")
205+
sat_base64_original_string: Optional[str] = Field(default=None, alias="satBase64OriginalString", description="Cadena original de la factura codificada en Base64.")
206+
sat_certificate_number: Optional[str] = Field(default=None, alias="satCertificateNumber", description="Número de certificado del SAT.")
207+
208+
model_config = ConfigDict(
209+
populate_by_name=True,
210+
json_encoders={datetime: lambda v: v.isoformat()}
211+
)
212+
213+
214+
class Invoice(BaseDto):
215+
"""Modelo para la factura."""
216+
version_code: Optional[str] = Field(default="4.0", alias="versionCode", description="Código de la versión de la factura.")
217+
series: str = Field(..., description="Número de serie que utiliza el contribuyente para control interno.")
218+
date: datetime = Field(..., description="Fecha y hora de expedición del comprobante fiscal.")
219+
payment_form_code: str = Field(..., alias="paymentFormCode", description="Código de la forma de pago.")
220+
currency_code: Literal["MXN", "USD", "EUR"] = Field(default="MXN", alias="currencyCode", description="Código de la moneda utilizada.")
221+
type_code: Optional[Literal["I", "E", "T", "N", "P"]] = Field(default="I", alias="typeCode", description="Código de tipo de factura.")
222+
expedition_zip_code: str = Field(..., alias="expeditionZipCode", description="Código postal del emisor.")
223+
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.")
224+
payment_method_code: Literal["PUE", "PPD"] = Field(..., alias="paymentMethodCode", description="Código de método para la factura de pago.")
225+
exchange_rate: Optional[Decimal] = Field(default=1, alias="exchangeRate", description="Tipo de cambio FIX.")
226+
issuer: Optional[InvoiceIssuer] = Field(..., description="El emisor de la factura.")
227+
recipient: Optional[InvoiceRecipient] = Field(..., description="El receptor de la factura.")
228+
items: List[InvoiceItem] = Field(..., description="Conceptos de la factura (productos o servicios).")
229+
global_information: Optional[GlobalInformation] = Field(default=None, alias="globalInformation", description="Información global de la factura.")
230+
related_invoices: Optional[List[RelatedInvoice]] = Field(default=None, alias="relatedInvoices", description="Facturas relacionadas.")
231+
payments: Optional[List[InvoicePayment]] = Field(default=None, description="Pago o pagos recibidos para liquidar la factura cuando la factura es un complemento de pago.")
232+
responses: Optional[List[InvoiceResponse]] = Field(default=None, description="Respuestas del SAT. Contiene la información de timbrado de la factura.")
233+
234+
235+
model_config = ConfigDict(
236+
populate_by_name=True,
237+
json_encoders={Decimal: str}
238+
)
239+

fiscalapi/services/base_service.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,19 @@ def _process_response(self, response: requests.Response, response_model: Type[T]
194194

195195

196196

197-
def send_request(self, method: str, endpoint: str, response_model: Type[T], **kwargs) -> ApiResponse[T]:
197+
# def send_request(self, method: str, endpoint: str, response_model: Type[T], **kwargs) -> ApiResponse[T]:
198+
# payload = kwargs.pop("payload", None)
199+
# if payload is not None and isinstance(payload, BaseModel):
200+
# # Excluir propiedades con valor None
201+
# kwargs["json"] = payload.model_dump(mode="json", by_alias=True, exclude_none=True)
202+
203+
# response = self._request(method, endpoint, **kwargs)
204+
# return self._process_response(response, response_model)
205+
206+
def send_request(self, method: str, endpoint: str, response_model: Type[T], details: bool = False, **kwargs) -> ApiResponse[T]:
207+
if details:
208+
endpoint += "?details=true"
209+
198210
payload = kwargs.pop("payload", None)
199211
if payload is not None and isinstance(payload, BaseModel):
200212
# Excluir propiedades con valor None

fiscalapi/services/fiscalapi_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fiscalapi.models.common_models import FiscalApiSettings
22
from fiscalapi.services.catalog_service import CatalogService
3+
from fiscalapi.services.invoice_service import InvoiceService
34
from fiscalapi.services.people_service import PeopleService
45
from fiscalapi.services.product_service import ProductService
56
from fiscalapi.services.tax_file_servive import TaxFileService
@@ -13,4 +14,5 @@ def __init__(self, settings: FiscalApiSettings):
1314
self.people = PeopleService(settings)
1415
self.tax_files = TaxFileService(settings)
1516
self.catalogs = CatalogService(settings)
17+
self.invoices = InvoiceService(settings)
1618
self.settings = settings
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from fiscalapi.models.common_models import ApiResponse, PagedList
2+
from fiscalapi.models.fiscalapi_models import Invoice
3+
from fiscalapi.services.base_service import BaseService
4+
5+
6+
class InvoiceService(BaseService):
7+
8+
# get paged list of invoices
9+
def get_list(self, page_number: int, page_size: int) -> ApiResponse[PagedList[Invoice]]:
10+
endpoint = f"invoices?pageNumber={page_number}&pageSize={page_size}"
11+
return self.send_request("GET", endpoint, PagedList[Invoice])
12+
13+
# get invoice by id
14+
def get_by_id(self, invoice_id: int, details: bool = False) -> ApiResponse[Invoice]:
15+
endpoint = f"invoices/{invoice_id}"
16+
return self.send_request("GET", endpoint, Invoice, details=details)
17+

0 commit comments

Comments
 (0)