Merge pull request 'feature/items-validation' (#134) from feature/items-validation into development
Reviewed-on: ADUANASOFT/anexo76#134
This commit is contained in:
@@ -69,9 +69,15 @@ from api.v1.modules.a76.general_catalogs.units_of_measure.seed_ame import (
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.seed_adua import (
|
||||
seed as adua_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.tariff_fractions.seed import (
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.seed import (
|
||||
seed as tariff_fractions_seed,
|
||||
)
|
||||
from api.v1.modules.a76.general_catalogs.fractions.historical_tariff_fractions.seed import (
|
||||
seed as historical_tariff_fractions_seed,
|
||||
)
|
||||
from api.v1.modules.public.reference_data.trailer_types.seed import (
|
||||
seed as trailer_types_seed,
|
||||
)
|
||||
from api.v1.modules.core.permissions.seed import (
|
||||
seed_invoices,
|
||||
seed_user,
|
||||
@@ -307,6 +313,20 @@ def upgrade() -> None:
|
||||
"""
|
||||
)
|
||||
|
||||
values_trailer = ", ".join(
|
||||
[
|
||||
f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
for code, desc in trailer_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.trailer_type (trailer_type_key, description) VALUES
|
||||
{values_trailer}
|
||||
ON CONFLICT (trailer_type_key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
values_vm = ", ".join(
|
||||
[
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')"
|
||||
@@ -481,12 +501,49 @@ def upgrade() -> None:
|
||||
"""
|
||||
)
|
||||
|
||||
def format_bool(val):
|
||||
"""Convert boolean string to SQL boolean."""
|
||||
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||
return "NULL"
|
||||
return "TRUE" if str(val).upper() == "TRUE" else "FALSE"
|
||||
|
||||
def format_timestamp(val):
|
||||
"""Format timestamp for PostgreSQL."""
|
||||
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||
return "NULL"
|
||||
# El valor ya viene en formato 'YYYY-MM-DD HH:MM:SS'
|
||||
return f"'{str(val)}'"
|
||||
|
||||
values_historical_fractions = ", ".join(
|
||||
[
|
||||
f"({format_value(historical_fraction)}, {format_value(unit_measure)}, {format_value(country)}, "
|
||||
f"{format_value(fraction_type)}, {format_value(sector)}, {format_value(import_tax)}, "
|
||||
f"{format_value(export_tax)}, {format_timestamp(pub_date)}, {format_bool(is_immex)}, "
|
||||
f"{format_bool(normal_temp)}, {format_bool(services_temp)}, {format_bool(certified_temp)}, "
|
||||
f"{format_bool(by_log)}, {format_timestamp(end_date)})"
|
||||
for historical_fraction, unit_measure, country, fraction_type, sector, import_tax, export_tax, pub_date, is_immex, normal_temp, services_temp, certified_temp, by_log, end_date in historical_tariff_fractions_seed
|
||||
]
|
||||
)
|
||||
|
||||
if values_historical_fractions:
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO a76.historical_tariff_fractions
|
||||
(historical_fraction, unit_of_measure_code, country, fraction_type, sector,
|
||||
import_tax_rate, export_tax_rate, publication_date, is_immex,
|
||||
normal_temporality, services_temporality, certified_temporality, by_log, end_date)
|
||||
VALUES {values_historical_fractions}
|
||||
ON CONFLICT DO NOTHING;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
|
||||
op.drop_table("valuation_methods", schema="public")
|
||||
op.drop_table("transport_types", schema="public")
|
||||
op.drop_table("trailer_types", schema="public")
|
||||
op.drop_table("transport_modes", schema="public")
|
||||
op.drop_table("sectors", schema="public")
|
||||
op.drop_table("payment_methods", schema="public")
|
||||
|
||||
@@ -67,9 +67,6 @@ class FaLineItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
# Subitems
|
||||
is_subitem: Mapped[Optional[bool]] = mapped_column(Boolean) # ESSUBPARTIDA
|
||||
contains_subitems: Mapped[Optional[bool]] = mapped_column(Boolean) # CONTIENESUBP
|
||||
includes_subitems: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean
|
||||
) # INCUYESUBPARTIDAS
|
||||
subitem_number: Mapped[Optional[int]] = mapped_column(Integer) # SUBPARTIDA
|
||||
|
||||
# Special flags
|
||||
|
||||
@@ -39,7 +39,7 @@ class Company(Base, TimestampMixin):
|
||||
# Programa
|
||||
program: Mapped[Optional[str]] = mapped_column(String(7))
|
||||
program_number: Mapped[Optional[str]] = mapped_column(String(40))
|
||||
prosec: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
prosec: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
|
||||
prosec_authorization: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
|
||||
# Sectores
|
||||
@@ -61,9 +61,9 @@ class Company(Base, TimestampMixin):
|
||||
|
||||
# Configuración básica
|
||||
logo: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
has_express_line: Mapped[Optional[str]] = mapped_column(String(2), default="N")
|
||||
has_express_line: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, server_default="false")
|
||||
order_format_type: Mapped[Optional[str]] = mapped_column(String(19))
|
||||
is_service_company: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
|
||||
is_service_company: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, server_default="false")
|
||||
client_name: Mapped[Optional[str]] = mapped_column(String(300))
|
||||
subassembly_mode: Mapped[Optional[str]] = mapped_column(String(7))
|
||||
|
||||
@@ -77,9 +77,11 @@ class Company(Base, TimestampMixin):
|
||||
scaf_readonly: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
parts_replacement: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
activate_facmexame: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
part_reference: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
part_reference: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
part_reference: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
international_firm: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
seventh_amendment: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean
|
||||
) # Septima enimenda (FinalContadorAElectronico)
|
||||
|
||||
# Configuraciones simples
|
||||
ftp_key: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Integer, Numeric, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from core.database import Base
|
||||
|
||||
|
||||
class HistoricalTariffFraction(Base):
|
||||
"""
|
||||
Historical tariff fractions catalog.
|
||||
Maps to SQL Server table: GFraccionesHistorico
|
||||
"""
|
||||
|
||||
__tablename__ = "historical_tariff_fractions"
|
||||
__table_args__ = {"schema": "a76"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, nullable=False)
|
||||
historical_fraction: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)
|
||||
unit_of_measure_code: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.unit_of_measure_customs.code"), nullable=True)
|
||||
country: Mapped[Optional[str]] = mapped_column(ForeignKey("public.countries.m3_key"), nullable=True)
|
||||
fraction_type: Mapped[Optional[str]] = mapped_column(String(7), nullable=True)
|
||||
sector: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
|
||||
import_tax_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(7, 2), nullable=True)
|
||||
export_tax_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(7, 2), nullable=True)
|
||||
publication_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
is_immex: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
|
||||
normal_temporality: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
|
||||
services_temporality: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
|
||||
certified_temporality: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
|
||||
by_log: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True)
|
||||
end_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
File diff suppressed because it is too large
Load Diff
52
backend/api/v1/modules/a76/general_catalogs/router.py
Normal file
52
backend/api/v1/modules/a76/general_catalogs/router.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from fastapi import APIRouter
|
||||
from .company import router as company_router
|
||||
from .exchange_rate.routes import router as exchange_rate_router
|
||||
from .identifiers.routes import router as identifiers_router
|
||||
from .packages.routes import router as package_router
|
||||
from .ports.routes import router as ports_router
|
||||
from .fractions.tariff_fractions.routes import router as tariff_fractions_router
|
||||
from .fractions.us_tariff_fractions.routes import router as us_tariff_fractions_router
|
||||
from .depreciation_catalog.routes import router as depreciation_catalog_router
|
||||
from .fda_catalog.routes import router as fda_catalog_router
|
||||
from .seal.routes import router as seal_router
|
||||
from .units_of_measure.routes import router as units_of_measure_router
|
||||
from .concepts.routes import router as concepts_router
|
||||
from .customs_broker_concepts.routes import router as customs_broker_concepts_router
|
||||
from .classification_concepts.routes import router as classification_concepts_router
|
||||
from .unit_conversions.routes import router as unit_conversions_router
|
||||
from .equivalencies.routes import router as equivalencies_router
|
||||
from .multi_currency_types.routes import router as multi_currency_types_router
|
||||
from .inpc.routes import router as inpc_router
|
||||
from .legends.routes import router as legends_router
|
||||
from .signatures.routes import router as signatures_router
|
||||
from .error_catalogs.routes import router as error_catalogs_router
|
||||
from .doda.routes import router as doda_router
|
||||
from .prevalidators.routes import router as prevalidators_router
|
||||
from .electronic_notices.routes import router as electronic_notices_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(company_router, tags=["a76 / company"])
|
||||
router.include_router(package_router)
|
||||
router.include_router(ports_router)
|
||||
router.include_router(tariff_fractions_router)
|
||||
router.include_router(us_tariff_fractions_router)
|
||||
router.include_router(depreciation_catalog_router)
|
||||
router.include_router(fda_catalog_router)
|
||||
router.include_router(seal_router, tags=["a76 / seal"])
|
||||
router.include_router(units_of_measure_router)
|
||||
router.include_router(identifiers_router)
|
||||
router.include_router(exchange_rate_router, tags=["a76 / exchange_rate"])
|
||||
router.include_router(concepts_router)
|
||||
router.include_router(customs_broker_concepts_router)
|
||||
router.include_router(classification_concepts_router)
|
||||
router.include_router(unit_conversions_router)
|
||||
router.include_router(equivalencies_router)
|
||||
router.include_router(multi_currency_types_router)
|
||||
router.include_router(inpc_router)
|
||||
router.include_router(legends_router)
|
||||
router.include_router(signatures_router)
|
||||
router.include_router(error_catalogs_router)
|
||||
router.include_router(doda_router)
|
||||
router.include_router(prevalidators_router)
|
||||
router.include_router(electronic_notices_router)
|
||||
@@ -1,3 +1,4 @@
|
||||
from typing import Optional
|
||||
from core.exceptions import ErrorCollector
|
||||
from .. import models
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -8,21 +9,80 @@ def invoice_exists(
|
||||
invoice_number: str,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector
|
||||
) -> bool:
|
||||
invoice_exists = (
|
||||
db.query(models.InvoiceHeader.id)
|
||||
errors: Optional[ErrorCollector],
|
||||
):
|
||||
invoice = (
|
||||
db.query(models.InvoiceHeader)
|
||||
.filter(
|
||||
models.InvoiceHeader.invoice_number == invoice_number,
|
||||
models.InvoiceHeader.tenant_id == tenant_id,
|
||||
models.InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
.first()
|
||||
)
|
||||
|
||||
if invoice_exists:
|
||||
errors.add_duplicate_error(
|
||||
|
||||
if invoice:
|
||||
if errors:
|
||||
errors.add_duplicate_error(
|
||||
"invoice_number",
|
||||
invoice_number,
|
||||
f"Ya existe una factura con el número '{invoice_number}'",
|
||||
)
|
||||
)
|
||||
return invoice
|
||||
return None
|
||||
|
||||
def invoice_exists_by_id(
|
||||
db: Session,
|
||||
invoice_id: str,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: Optional[ErrorCollector],
|
||||
):
|
||||
invoice = (
|
||||
db.query(models.InvoiceHeader)
|
||||
.filter(
|
||||
models.InvoiceHeader.id == invoice_id,
|
||||
models.InvoiceHeader.tenant_id == tenant_id,
|
||||
models.InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if invoice:
|
||||
if errors:
|
||||
errors.add_duplicate_error(
|
||||
"invoice_id",
|
||||
invoice_id,
|
||||
f"Ya existe una factura con el número '{invoice_id}'",
|
||||
)
|
||||
return invoice
|
||||
return None
|
||||
|
||||
|
||||
def invoice_updated(
|
||||
db: Session,
|
||||
invoice_id: str,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
) -> bool:
|
||||
is_updated = (
|
||||
db.query(models.InvoiceHeader.is_updated)
|
||||
.filter(
|
||||
models.InvoiceHeader.id == invoice_id,
|
||||
models.InvoiceHeader.tenant_id == tenant_id,
|
||||
models.InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
if is_updated:
|
||||
errors.add_error(
|
||||
field="invoice_number",
|
||||
message=f"La factura con el número '{invoice_id}' ya ha sido actualizada y no se puede modificar.",
|
||||
solution="Capturar otro número de Factura de Importación Temporal o Desactualizar la factura.",
|
||||
code="INVOICE_UPDATED",
|
||||
value=invoice_id,
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
38
backend/api/v1/modules/a76/items/common/common_validators.py
Normal file
38
backend/api/v1/modules/a76/items/common/common_validators.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from sqlalchemy import func
|
||||
from core.exceptions import ErrorCollector
|
||||
from ..line_items import models
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
def item_exists(
|
||||
db: Session,
|
||||
item_line: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
):
|
||||
item_exists = (
|
||||
db.query(models.LineItem.id)
|
||||
.filter(
|
||||
models.LineItem.line_number == item_line,
|
||||
models.LineItem.tenant_id == tenant_id,
|
||||
models.LineItem.company_id == company_id,
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
if item_exists:
|
||||
return item_exists
|
||||
return None
|
||||
|
||||
def count_items(
|
||||
db: Session,
|
||||
invoice_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
):
|
||||
count = db.query(func.count()).select_from(models.Item).filter(
|
||||
models.Item.invoice_id == invoice_id,
|
||||
models.Item.tenant_id == tenant_id,
|
||||
models.Item.company_id == company_id,
|
||||
).scalar()
|
||||
|
||||
return count
|
||||
155
backend/api/v1/modules/a76/items/common/fractions.py
Normal file
155
backend/api/v1/modules/a76/items/common/fractions.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.general_catalogs.fractions.historical_tariff_fractions.models import HistoricalTariffFraction
|
||||
from api.v1.modules.sitar.tlcs import TLCSService
|
||||
from api.v1.modules.sitar.prosec import ProsecService
|
||||
from api.v1.modules.sitar.fracciones import FraccionesService
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import exists
|
||||
from core.exceptions import ErrorCollector
|
||||
|
||||
def search_historical_fraction(db: Session, fraction: str, country: str, fraction_type: str, sector: str, invoice_date: Optional[datetime], errors: Optional[ErrorCollector]) -> Tuple[Optional[str], float]:
|
||||
"""Search for historical fraction data
|
||||
This corresponds to BUSCA_FRACCION_HISTORICA in original Clarion code
|
||||
|
||||
Returns:
|
||||
Tuple of (rate_im, adv_impo)
|
||||
"""
|
||||
# Placeholder for historical search
|
||||
historical_exists = db.query(exists().where(HistoricalTariffFraction.historical_fraction == fraction)).scalar()
|
||||
if not historical_exists:
|
||||
errors.add_error(
|
||||
field=f"fraction, country, fraction_type, sector",
|
||||
message=f"La fraccion {fraction}, con pais {country}, con preferencia {fraction_type} y sector {sector} no existe.",
|
||||
solution=["Revisar que si exista la preferencia para esta fracción en caso de ser historico, registrarlo en el catálogo de fracciones historicas."],
|
||||
code="HISTORICAL_FRACTION_NOT_FOUND",
|
||||
)
|
||||
else:
|
||||
historical_exists = db.query(HistoricalTariffFraction).filter(
|
||||
HistoricalTariffFraction.historical_fraction == fraction,
|
||||
HistoricalTariffFraction.country == country,
|
||||
HistoricalTariffFraction.fraction_type == fraction_type,
|
||||
HistoricalTariffFraction.sector == sector,
|
||||
HistoricalTariffFraction.publication_date <= invoice_date,
|
||||
).first()
|
||||
|
||||
return historical_exists.import_tax_rate, historical_exists.import_tax_rate
|
||||
|
||||
|
||||
def search_fraction_preference(
|
||||
db: Session,
|
||||
country: str,
|
||||
fraccion: str,
|
||||
fraction_type: str,
|
||||
sector: Optional[str] = None,
|
||||
invoice_date: Optional[datetime] = None,
|
||||
errors: Optional[ErrorCollector] = None
|
||||
) -> Tuple[Optional[str], float]:
|
||||
"""Search fraction preference and return rate_im and adv_impo
|
||||
|
||||
Args:
|
||||
country: Country code
|
||||
fraction_type: Type of fraction (e.g., 'TLCS', 'PROSEC')
|
||||
company: Company object with configuration
|
||||
fraccion: Tariff fraction code to search
|
||||
sector: Sector code (required for PROSEC searches)
|
||||
|
||||
Returns:
|
||||
Tuple of (rate_im, adv_impo) where:
|
||||
- rate_im: Tax rate as string (e.g., "EXE", "5.0%")
|
||||
- adv_impo: Numeric ad valorem rate
|
||||
"""
|
||||
rate_im = None
|
||||
adv_impo = 0.0
|
||||
fraccion_8 = fraccion[:8]
|
||||
|
||||
country_group = "USA" if country == "MEX" else country
|
||||
|
||||
"""" TLCS Search """
|
||||
if fraction_type.upper() == "TLCS":
|
||||
try:
|
||||
# Get TLCS service instance
|
||||
tlcs_service = TLCSService.get_instance()
|
||||
|
||||
# Fetch TLCS data using the service
|
||||
tlcs_data = asyncio.run(
|
||||
tlcs_service.search(fraccion=fraccion_8, pais=country_group, limit=100)
|
||||
)
|
||||
|
||||
if not tlcs_data:
|
||||
rate_im, adv_impo = search_historical_fraction(db=db, fraction=fraccion_8, country=country, fraction_type=fraction_type, sector=sector, invoice_date=invoice_date, errors=errors)
|
||||
else:
|
||||
first_record = tlcs_data[0]
|
||||
rate_im = first_record.TASATXT
|
||||
adv_impo = float(first_record.TASA1NUM or 0.0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching Fracciones data from SITAR API: {e}")
|
||||
|
||||
""" ALADI Search """
|
||||
if fraction_type.upper() == "ALADI":
|
||||
rate_im, adv_impo = search_historical_fraction(db=db, fraction=fraccion_8, country=country, fraction_type=fraction_type, sector=sector, invoice_date=invoice_date, errors=errors)
|
||||
|
||||
""" PROSEC Search """
|
||||
if fraction_type.upper() == "PROSEC":
|
||||
try:
|
||||
# Búsqueda en API de PROSEC (TARIFA_AS..sProsec)
|
||||
prosec_service = ProsecService.get_instance()
|
||||
|
||||
# Buscar primero con ARTICULO = '4to'
|
||||
prosec_data = asyncio.run(
|
||||
prosec_service.search(
|
||||
fraccion=fraccion_8, sector=sector, articulo="4to", limit=100
|
||||
)
|
||||
)
|
||||
|
||||
# Si no se encuentra con '4to', buscar con '5to'
|
||||
if not prosec_data:
|
||||
prosec_data = asyncio.run(
|
||||
prosec_service.search(
|
||||
fraccion=fraccion_8, sector=sector, articulo="5to", limit=100
|
||||
)
|
||||
)
|
||||
|
||||
if not prosec_data:
|
||||
rate_im, adv_impo = search_historical_fraction(db=db, fraction=fraccion_8, country=country, fraction_type=fraction_type, sector=sector, invoice_date=invoice_date, errors=errors)
|
||||
else:
|
||||
first_record = prosec_data[0]
|
||||
rate_im = first_record.TASATXT
|
||||
adv_impo = float(first_record.TASANUM or 0.0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching Fracciones data from SITAR API: {e}")
|
||||
|
||||
""" General search fallback """
|
||||
if fraction_type.upper() == "GENERAL":
|
||||
# Búsqueda en API de Fracciones (sFracciones)
|
||||
try:
|
||||
fracciones_service = FraccionesService.get_instance()
|
||||
|
||||
# Extraer fracción (primeros 8 caracteres) e nico (caracteres 9-10)
|
||||
nico = fraccion[8:10] if len(fraccion) >= 10 else None
|
||||
|
||||
# Buscar por fracción e histórico
|
||||
fracciones_data = asyncio.run(
|
||||
fracciones_service.search(
|
||||
fraccion=fraccion_8, nico=nico, limit=100
|
||||
)
|
||||
)
|
||||
|
||||
if not fracciones_data:
|
||||
rate_im, adv_impo = search_historical_fraction(db=db, fraction=fraccion_8, country=country, fraction_type=fraction_type, sector=sector, invoice_date=invoice_date, errors=errors)
|
||||
else:
|
||||
# Se encontraron registros, tomar el primero
|
||||
first_record = fracciones_data[0]
|
||||
rate_im = first_record.ADVIMPOTXT # AdvImpoTxt
|
||||
adv_impo = float(first_record.ADVIMPONUM or 0.0)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error fetching Fracciones data from SITAR API: {e}")
|
||||
|
||||
return rate_im, adv_impo
|
||||
@@ -1,158 +1,344 @@
|
||||
"""
|
||||
Funciones helper compartidas para validaciones de items.
|
||||
"""
|
||||
from sqlalchemy import exists
|
||||
from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.invoices.common.common_validators import invoice_exists_by_id
|
||||
from core.exceptions import ErrorCollector
|
||||
from typing import Optional
|
||||
from sqlalchemy import func
|
||||
|
||||
from ....common.fractions import search_fraction_preference
|
||||
from ....common.common_validators import item_exists
|
||||
from ....models import Item
|
||||
from ....line_items.models import LineItem
|
||||
from ....line_customs.models import FractionType, LineCustom
|
||||
from api.v1.modules.a76.items.schemas import LineItemCreate
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
from api.v1.modules.public.reference_data.countries.models import Country
|
||||
from api.v1.modules.public.reference_data.sectors.models import Sector
|
||||
from api.v1.modules.public.reference_data.valuation_methods.models import (
|
||||
ValuationMethod,
|
||||
)
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
|
||||
|
||||
def validate_catalog_reference(
|
||||
def validate_common(
|
||||
db: Session,
|
||||
model_class,
|
||||
id_value: Optional[int],
|
||||
field_name: str,
|
||||
line: LineItemCreate,
|
||||
invoice_id: int, # Para creación, se pasa directamente; para update, se consulta del item
|
||||
tenant_id: int,
|
||||
company_id: Optional[int],
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
error_message: str = None
|
||||
) -> bool:
|
||||
"""
|
||||
Valida que una referencia a catálogo exista en la base de datos.
|
||||
|
||||
Args:
|
||||
db: Sesión de base de datos
|
||||
model_class: Clase del modelo SQLAlchemy a consultar
|
||||
id_value: ID a validar
|
||||
field_name: Nombre del campo para el error
|
||||
tenant_id: ID del tenant
|
||||
company_id: ID de la compañía (opcional)
|
||||
errors: Colector de errores
|
||||
error_message: Mensaje personalizado de error
|
||||
|
||||
Returns:
|
||||
True si existe, False si no
|
||||
"""
|
||||
if not id_value:
|
||||
return False
|
||||
|
||||
query = db.query(model_class).filter(
|
||||
model_class.id == id_value,
|
||||
model_class.tenant_id == tenant_id
|
||||
line_number: int,
|
||||
):
|
||||
# Para updates, line.item_id existe; para creates, es None
|
||||
item_header = None
|
||||
if line.item_id:
|
||||
item_header = db.query(Item).filter(Item.id == line.item_id).first()
|
||||
if item_header:
|
||||
invoice_id = item_header.invoice_id
|
||||
|
||||
invoice: InvoiceHeader = invoice_exists_by_id(
|
||||
db, invoice_id, tenant_id, company_id, errors
|
||||
)
|
||||
|
||||
# Agregar filtro de company_id si el modelo lo tiene y se proporciona
|
||||
if company_id and hasattr(model_class, 'company_id'):
|
||||
query = query.filter(model_class.company_id == company_id)
|
||||
|
||||
exists = query.first() is not None
|
||||
|
||||
if not exists:
|
||||
msg = error_message or f"El valor {id_value} no existe en el catálogo"
|
||||
line_item: LineItem = item_exists(
|
||||
db, line.line_number, tenant_id, company_id
|
||||
)
|
||||
|
||||
fecha_factura = invoice.invoice_date if invoice else None
|
||||
fraction = None
|
||||
|
||||
class_ = db.query(Class).filter(Class.id == line.class_id).first()
|
||||
if not class_:
|
||||
errors.add_error(
|
||||
field=field_name,
|
||||
message=msg,
|
||||
solution="Selecciona un valor válido del catálogo",
|
||||
code="NOT_FOUND"
|
||||
field=f"line[{line_number}].class_id",
|
||||
message="La clase especificada no existe.",
|
||||
solution=["Darla de alta en el catalogo de clases."],
|
||||
code="CLASS_NOT_FOUND",
|
||||
)
|
||||
|
||||
return exists
|
||||
|
||||
|
||||
def validate_positive_value(
|
||||
value: Optional[float],
|
||||
field_name: str,
|
||||
errors: ErrorCollector,
|
||||
required: bool = True,
|
||||
allow_zero: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Valida que un valor numérico sea positivo.
|
||||
|
||||
Args:
|
||||
value: Valor a validar
|
||||
field_name: Nombre del campo para el error
|
||||
errors: Colector de errores
|
||||
required: Si el campo es obligatorio
|
||||
allow_zero: Si se permite el valor cero
|
||||
|
||||
Returns:
|
||||
True si es válido, False si no
|
||||
"""
|
||||
if value is None:
|
||||
if required:
|
||||
else:
|
||||
if not line.unit_of_measure and not class_.unit_of_measure:
|
||||
errors.add_error(
|
||||
field=field_name,
|
||||
message=f"El campo {field_name} es obligatorio",
|
||||
solution="Proporciona un valor válido",
|
||||
code="REQUIRED"
|
||||
field=f"line[{line_number}].unit_of_measure",
|
||||
message="La unidad de medida es obligatoria para la clase especificada.",
|
||||
solution=["Proporciona una unidad de medida valida."],
|
||||
code="UNIT_OF_MEASURE_REQUIRED",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
if allow_zero and value == 0:
|
||||
return True
|
||||
|
||||
if value <= 0:
|
||||
|
||||
if not line.customs.fraction:
|
||||
if not line_item:
|
||||
if not class_.fraction:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.fraction",
|
||||
message="La fracción arancelaria es obligatoria para la clase especificada.",
|
||||
solution=["Proporciona una fracción arancelaria valida."],
|
||||
code="FRACTION_REQUIRED",
|
||||
)
|
||||
else:
|
||||
fraction = class_.fraction
|
||||
else:
|
||||
if not line.customs.fraction:
|
||||
if not class_.fraction:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.fraction",
|
||||
message="La fracción arancelaria es obligatoria para la clase especificada.",
|
||||
solution=["Proporciona una fracción arancelaria valida."],
|
||||
code="FRACTION_REQUIRED",
|
||||
)
|
||||
else:
|
||||
fraction = class_.fraction
|
||||
else:
|
||||
if line_item:
|
||||
fraction = line.customs.fraction
|
||||
|
||||
if not line.description.description_spanish and not class_.description_es:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].description.description_spanish",
|
||||
message="La descripción en español es obligatoria para la clase especificada.",
|
||||
solution=["Proporciona una descripción en español valida."],
|
||||
code="DESCRIPTION_SPANISH_REQUIRED",
|
||||
)
|
||||
|
||||
if not line.description.description_english and not class_.description_en:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].description.description_english",
|
||||
message="La descripción en inglés es obligatoria para la clase especificada.",
|
||||
solution=["Proporciona una descripción en inglés valida."],
|
||||
code="DESCRIPTION_ENGLISH_REQUIRED",
|
||||
)
|
||||
|
||||
if line.quantity.quantity and line.quantity.quantity <= 0:
|
||||
errors.add_error(
|
||||
field=field_name,
|
||||
message=f"El campo {field_name} debe ser mayor a cero",
|
||||
solution="Proporciona un valor positivo",
|
||||
code="INVALID_VALUE"
|
||||
field=f"line[{line_number}].quantity.quantity",
|
||||
message="La cantidad debe ser mayor a cero.",
|
||||
solution=["Proporciona una cantidad valida."],
|
||||
code="QUANTITY_MUST_BE_GREATER_THAN_ZERO",
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def normalize_yes_no_value(value: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Normaliza valores SI/NO a formato estándar.
|
||||
|
||||
Args:
|
||||
value: Valor a normalizar (SI, NO, S, N)
|
||||
|
||||
Returns:
|
||||
'SI' o 'NO', o None si el valor es None
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
val = value.upper().strip()
|
||||
if val in ['SI', 'S']:
|
||||
return 'SI'
|
||||
elif val in ['NO', 'N']:
|
||||
return 'NO'
|
||||
|
||||
return value # Retornar original si no coincide
|
||||
|
||||
|
||||
def validate_string_not_empty(
|
||||
value: Optional[str],
|
||||
field_name: str,
|
||||
errors: ErrorCollector,
|
||||
required: bool = True
|
||||
) -> bool:
|
||||
"""
|
||||
Valida que un string no esté vacío.
|
||||
|
||||
Args:
|
||||
value: Valor a validar
|
||||
field_name: Nombre del campo para el error
|
||||
errors: Colector de errores
|
||||
required: Si el campo es obligatorio
|
||||
|
||||
Returns:
|
||||
True si es válido, False si no
|
||||
"""
|
||||
if value is None or (isinstance(value, str) and not value.strip()):
|
||||
if required:
|
||||
errors.add_error(
|
||||
field=field_name,
|
||||
message=f"El campo {field_name} no puede estar vacío",
|
||||
solution="Proporciona un valor válido",
|
||||
code="REQUIRED"
|
||||
if line.unit_of_measure:
|
||||
um = (
|
||||
db.query(func.count(UnitOfMeasure.id))
|
||||
.filter(
|
||||
UnitOfMeasure.id == line.unit_of_measure,
|
||||
UnitOfMeasure.tenant_id == tenant_id,
|
||||
UnitOfMeasure.company_id == company_id,
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
if um == 0:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].unit_of_measure",
|
||||
message="La unidad de medida especificada no existe.",
|
||||
solution=["Proporciona una unidad de medida valida."],
|
||||
code="UNIT_OF_MEASURE_NOT_FOUND",
|
||||
)
|
||||
|
||||
if line.quantity.package_id:
|
||||
package = (
|
||||
db.query(func.count(Package.id))
|
||||
.filter(
|
||||
Package.id == line.quantity.package_id,
|
||||
Package.tenant_id == tenant_id,
|
||||
Package.company_id == company_id,
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
if package == 0:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].quantity.package_id",
|
||||
message="El paquete especificado no existe.",
|
||||
solution=["Proporciona un paquete valido."],
|
||||
code="PACKAGE_NOT_FOUND",
|
||||
)
|
||||
if not line.quantity.package_quantity:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].quantity.package_quantity",
|
||||
message="La cantidad de paquetes es obligatoria cuando se proporciona el paquete.",
|
||||
solution=["Proporciona una cantidad de paquetes valida."],
|
||||
code="PACKAGE_QUANTITY_REQUIRED",
|
||||
)
|
||||
if line.quantity.package_quantity <= 0:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].quantity.package_quantity",
|
||||
message="La cantidad de paquetes debe ser mayor a cero.",
|
||||
solution=["Proporciona una cantidad de paquetes valida."],
|
||||
code="PACKAGE_QUANTITY_MUST_BE_GREATER_THAN_ZERO",
|
||||
)
|
||||
else:
|
||||
if line.quantity.package_quantity and line.quantity.package_quantity > 0:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].quantity.package_id",
|
||||
message="El paquete es obligatorio cuando se proporciona la cantidad de paquetes.",
|
||||
solution=["Proporciona un paquete valido."],
|
||||
code="PACKAGE_ID_REQUIRED",
|
||||
)
|
||||
|
||||
country = None
|
||||
fraction_type = None
|
||||
sector = None
|
||||
if fraction:
|
||||
fraction = line.customs.fraction if line.customs.fraction else fraction
|
||||
|
||||
country = line.customs.origin_country
|
||||
if line_item:
|
||||
country = (
|
||||
line_item.customs.fraction if line_item.customs.origin_country else country
|
||||
)
|
||||
|
||||
fraction_type = line.customs.fraction_type.upper()
|
||||
if line_item:
|
||||
fraction_type = (
|
||||
line_item.customs.fraction_type
|
||||
if line_item.customs.fraction_type
|
||||
else fraction_type
|
||||
)
|
||||
|
||||
sector = line.customs.sector
|
||||
if line_item:
|
||||
sector = line_item.customs.sector if line_item.customs.sector else sector
|
||||
|
||||
country_m3 = db.query(Country.m3_key).filter(Country.m3_key == country).scalar()
|
||||
if not country_m3:
|
||||
country_m3 = (
|
||||
db.query(Country.m3_key).filter(Country.ame_key == country).scalar()
|
||||
)
|
||||
|
||||
country = country_m3
|
||||
if not country:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.origin_country",
|
||||
message="El país de origen especificado no existe.",
|
||||
solution=["Proporciona un país de origen valido."],
|
||||
code="ORIGIN_COUNTRY_NOT_FOUND",
|
||||
)
|
||||
else:
|
||||
if fraction_type.strip().upper() not in vars(FractionType).values():
|
||||
valid_types = [
|
||||
v
|
||||
for k, v in vars(FractionType).items()
|
||||
if not k.startswith("_") and isinstance(v, str)
|
||||
]
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.fraction_type",
|
||||
message="El tipo de fracción especificado no es válido.",
|
||||
solution=[
|
||||
f"Proporciona un tipo de fracción válido. Valores permitidos: {', '.join(valid_types)}"
|
||||
],
|
||||
code="FRACTION_TYPE_INVALID",
|
||||
value=fraction_type,
|
||||
)
|
||||
else:
|
||||
if fraction_type.strip().upper() == FractionType.PROSEC and not sector:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.sector",
|
||||
message="El sector es obligatorio cuando el tipo de fracción es 'PROSEC'.",
|
||||
solution=["Proporciona un sector valido."],
|
||||
code="SECTOR_REQUIRED_FOR_PROSEC",
|
||||
)
|
||||
elif fraction_type.strip().upper() != FractionType.PROSEC and sector:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.sector",
|
||||
message="El sector solo es aplicable cuando el tipo de fracción es 'PROSEC'.",
|
||||
solution=[
|
||||
"Elimina el sector o cambia el tipo de fracción a 'PROSEC'."
|
||||
],
|
||||
code="SECTOR_ONLY_FOR_PROSEC",
|
||||
)
|
||||
elif fraction_type.strip().upper() == FractionType.PROSEC and sector:
|
||||
sector_db: Sector = (
|
||||
db.query(Sector).filter(Sector.key == sector).scalar()
|
||||
)
|
||||
if sector_db:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.sector",
|
||||
message="El sector especificado no existe.",
|
||||
solution=["Proporciona un sector valido."],
|
||||
code="SECTOR_NOT_FOUND",
|
||||
)
|
||||
else:
|
||||
if not sector_db.authorized:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.sector",
|
||||
message="El sector especificado no está autorizado.",
|
||||
solution=["Proporciona un sector autorizado."],
|
||||
code="SECTOR_NOT_AUTHORIZED",
|
||||
)
|
||||
|
||||
company_db = db.query(Company).filter(Company.id == company_id).first()
|
||||
if not company_db.prosec:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.sector",
|
||||
message=" La empresa no cuenta con autorización PROSEC.",
|
||||
solution=[
|
||||
"Accese a los datos de la empresa y selecione la opción Pertenece al Programa de Promoción Sectorial y capture el número de permiso PROSEC."
|
||||
],
|
||||
code="COMPANY_NOT_AUTHORIZED_FOR_PROSEC",
|
||||
)
|
||||
|
||||
if fraction:
|
||||
search_fraction_preference(
|
||||
db=db,
|
||||
country=country,
|
||||
fraccion=fraction,
|
||||
fraction_type=fraction_type,
|
||||
sector=sector,
|
||||
invoice_date=fecha_factura,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
if line.customs.american_fraction:
|
||||
american_fraction_exists = db.query(
|
||||
exists().where(
|
||||
LineCustom.american_fraction == line.customs.american_fraction
|
||||
)
|
||||
).scalar()
|
||||
if not american_fraction_exists:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].customs.american_fraction",
|
||||
message="La fracción americana especificada no existe.",
|
||||
solution=["Proporciona una fracción americana valida."],
|
||||
code="AMERICAN_FRACTION_NOT_FOUND",
|
||||
)
|
||||
|
||||
if item_header and item_header.order:
|
||||
if len(item_header.order) > 20:
|
||||
errors.add_error(
|
||||
field=f"item.order",
|
||||
message="El campo orden no debe exceder los 20 caracteres.",
|
||||
solution=["Proporciona un valor valido para el campo orden."],
|
||||
code="ORDER_EXCEEDS_MAX_LENGTH",
|
||||
)
|
||||
|
||||
unit_of_measure = line.unit_of_measure or (
|
||||
class_.unit_of_measure if class_ else None
|
||||
)
|
||||
if unit_of_measure == "PZA" and line.quantity.quantity % 1 != 0:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].quantity.quantity",
|
||||
message="La cantidad debe ser un número entero cuando la unidad de medida es PZA.",
|
||||
solution=["Proporciona una cantidad entera."],
|
||||
code="QUANTITY_MUST_BE_INTEGER_FOR_PIECES",
|
||||
)
|
||||
|
||||
if line.valuation_method:
|
||||
valuation_method_exists = db.query(
|
||||
exists().where(ValuationMethod.key == line.valuation_method)
|
||||
).scalar()
|
||||
if not valuation_method_exists:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].valuation_method",
|
||||
message="El método de valoración especificado no existe.",
|
||||
solution=["Proporciona un método de valoración valido."],
|
||||
code="VALUATION_METHOD_NOT_FOUND",
|
||||
)
|
||||
|
||||
if line.part_number_id:
|
||||
part_exists = db.query(exists().where(Part.id == line.part_number_id)).scalar()
|
||||
if not part_exists:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].part_number_id",
|
||||
message="El número de parte especificado no existe.",
|
||||
solution=["Proporciona un número de parte valido."],
|
||||
code="PART_NUMBER_NOT_FOUND",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,135 +1,354 @@
|
||||
"""
|
||||
Validaciones para creación de items vía API.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import func, exists
|
||||
from sqlalchemy.orm import Session
|
||||
from ....common.common_validators import count_items
|
||||
from core.exceptions import ErrorCollector
|
||||
from api.v1.modules.a76.items.line_items.schemas import LineItemCreate
|
||||
from .common import validate_string_not_empty, validate_positive_value
|
||||
|
||||
from ....line_items.models import LineItem
|
||||
from ....line_financials.models import LineFinancial
|
||||
from ....line_financials.schemas import LineFinancialCreate
|
||||
from ....line_quantities.models import LineQuantity
|
||||
from ....line_quantities.schemas import LineQuantityCreate
|
||||
from ....line_customs.models import LineCustom
|
||||
from ....line_customs.schemas import LineCustomCreate
|
||||
from ....line_descriptions.models import LineDescription
|
||||
from ....line_descriptions.schemas import LineDescriptionCreate
|
||||
from ....line_references.models import LineReference
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from ....models import Item
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
)
|
||||
from .common import validate_common
|
||||
|
||||
|
||||
def validate_create(
|
||||
db: Session,
|
||||
line: LineItemCreate,
|
||||
line, # LineItemCreate schema (Pydantic)
|
||||
invoice_id: int, # Passed from service
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
) -> None:
|
||||
line_number: int,
|
||||
):
|
||||
"""
|
||||
Validaciones para crear LineItems vía API (actualmente en uso).
|
||||
Validates and calculates fields for a new line item before DB creation.
|
||||
Works with Pydantic schemas, modifying them in-place.
|
||||
|
||||
Args:
|
||||
db: Sesión de base de datos
|
||||
line: Datos del line item
|
||||
tenant_id: ID del tenant
|
||||
company_id: ID de la compañía
|
||||
errors: Colector de errores
|
||||
line: LineItemCreate schema with nested data (financial, quantity, customs, etc.)
|
||||
invoice_id: ID of the invoice this line belongs to
|
||||
fa_data: FaLineItemCreateDTO or None (None for INV system)
|
||||
"""
|
||||
# 1. Validar line_number
|
||||
if not line.line_number:
|
||||
errors.add_error(
|
||||
field="line_number",
|
||||
message="El número de línea es obligatorio",
|
||||
solution="Proporciona un número de línea válido",
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
# 2. Validar class_id
|
||||
if not line.class_id:
|
||||
errors.add_error(
|
||||
field="class_id",
|
||||
message="Clase (ID) es obligatorio",
|
||||
solution="Selecciona una clasificación válida del catálogo",
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
# 4. Validar unit_of_measure
|
||||
if not line.unit_of_measure:
|
||||
errors.add_error(
|
||||
field="unit_of_measure",
|
||||
message="U.M. es obligatorio",
|
||||
solution="Proporciona una unidad de medida válida",
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
# 5. Validar quantity.quantity
|
||||
if not line.quantity:
|
||||
errors.add_error(
|
||||
field="quantity",
|
||||
message="Quantity es obligatorio",
|
||||
solution="Proporciona una cantidad válida",
|
||||
code="REQUIRED",
|
||||
)
|
||||
else:
|
||||
# Validar con nombre amigable
|
||||
if line.quantity.quantity is None or line.quantity.quantity <= 0:
|
||||
errors.add_error(
|
||||
field="quantity.quantity",
|
||||
message="Quantity debe ser mayor a cero",
|
||||
solution="Proporciona una cantidad válida",
|
||||
code=(
|
||||
"INVALID_VALUE"
|
||||
if line.quantity.quantity is not None
|
||||
else "REQUIRED"
|
||||
),
|
||||
)
|
||||
|
||||
# 6. Validar financial.unit_cost
|
||||
# Inicializar nested schemas si no existen (para poder validar y modificar)
|
||||
if not line.financial:
|
||||
errors.add_error(
|
||||
field="financial",
|
||||
message="Unit Cost es obligatorio",
|
||||
solution="Proporciona el costo unitario del item",
|
||||
code="REQUIRED",
|
||||
)
|
||||
else:
|
||||
has_cost = (
|
||||
line.financial.unit_cost_usd
|
||||
or line.financial.unit_cost_mxn
|
||||
or line.financial.unit_cost_capture
|
||||
)
|
||||
if not has_cost:
|
||||
errors.add_error(
|
||||
field="financial.unit_cost",
|
||||
message="Unit Cost es obligatorio",
|
||||
solution="Proporciona al menos un costo unitario (USD, MXN o captura)",
|
||||
code="REQUIRED",
|
||||
line.financial = LineFinancialCreate()
|
||||
if not line.quantity:
|
||||
line.quantity = LineQuantityCreate()
|
||||
if not line.customs:
|
||||
line.customs = LineCustomCreate()
|
||||
if not line.description:
|
||||
line.description = LineDescriptionCreate()
|
||||
|
||||
# Access fa_data safely
|
||||
fa_data = getattr(line, "fa_data", None)
|
||||
|
||||
# Required field validations
|
||||
if not line.class_id:
|
||||
errors.add_required_error(field=f"line[{line_number}].class_id")
|
||||
|
||||
if not line.quantity.quantity or line.quantity.quantity <= 0:
|
||||
errors.add_required_error(field=f"line[{line_number}].quantity.quantity")
|
||||
|
||||
# TODO: Añadir validacion SSisGen:CalcularCostoUnitarioEnBaseAValorTotalScaf <-- de la tabla de preferencias de el sistema
|
||||
# if SSisGen:CalcularCostoUnitarioEnBaseAValorTotalScaf == False:
|
||||
if fa_data and not fa_data.is_subitem:
|
||||
if (
|
||||
not line.financial.unit_cost_capture
|
||||
or line.financial.unit_cost_capture <= 0
|
||||
):
|
||||
errors.add_required_error(
|
||||
field=f"line[{line_number}].financial.unit_cost_capture"
|
||||
)
|
||||
|
||||
# 7. Validar description.description_spanish
|
||||
if line.description:
|
||||
if not line.quantity.net_weight or line.quantity.net_weight <= 0:
|
||||
errors.add_required_error(field=f"line[{line_number}].quantity.net_weight")
|
||||
|
||||
if not line.customs.origin_country:
|
||||
errors.add_required_error(field=f"line[{line_number}].customs.origin_country")
|
||||
|
||||
if not line.customs.fraction_type:
|
||||
errors.add_required_error(field=f"line[{line_number}].customs.fraction_type")
|
||||
|
||||
# FA-specific validations
|
||||
if fa_data:
|
||||
if (
|
||||
not line.description.description_spanish
|
||||
or not line.description.description_spanish.strip()
|
||||
fa_data.is_subitem and fa_data.contains_subitems
|
||||
) and not fa_data.subitem_number:
|
||||
errors.add_required_error(
|
||||
field=f"line[{line_number}].fa_data.subitem_number"
|
||||
)
|
||||
|
||||
# Validar que si es un subitem, existe un item principal correspondiente
|
||||
if (
|
||||
fa_data.is_subitem
|
||||
and fa_data.subitem_number
|
||||
and fa_data.subitem_number != 0
|
||||
):
|
||||
principal_item_exists = db.query(
|
||||
exists().where(
|
||||
(LineItem.id == FaLineItem.id)
|
||||
& (LineItem.item_id == Item.id)
|
||||
& (Item.invoice_id == invoice_id)
|
||||
& (LineItem.line_number == line_number)
|
||||
& (FaLineItem.is_subitem == False)
|
||||
& (FaLineItem.contains_subitems == True)
|
||||
& (LineItem.tenant_id == tenant_id)
|
||||
& (LineItem.company_id == company_id)
|
||||
)
|
||||
).scalar()
|
||||
|
||||
if not principal_item_exists:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}]",
|
||||
message=f"No existe un item principal registrado para esta linea {line_number} con subitem {fa_data.subitem_number}",
|
||||
solution=[
|
||||
"Registrar el item principal correspondiente a esta linea antes de registrar subitems."
|
||||
],
|
||||
code="SUBITEM_WITHOUT_PRINCIPAL_ITEM",
|
||||
)
|
||||
|
||||
if fa_data.is_subitem and (
|
||||
fa_data.subitem_number == 0 or not fa_data.subitem_number
|
||||
):
|
||||
errors.add_error(
|
||||
field="description.description_spanish",
|
||||
message="Description in Spanish es obligatorio",
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED",
|
||||
field=f"line[{line_number}]",
|
||||
message=f"El número de subitem no puede ser 0 si la línea es un subitem.",
|
||||
solution=["Asignar un número de subitem mayor a 0 para esta línea."],
|
||||
code="SUBITEM_NUMBER_INVALID",
|
||||
)
|
||||
else:
|
||||
errors.add_error(
|
||||
field="description.description_spanish",
|
||||
message="Description in Spanish es obligatorio",
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED",
|
||||
|
||||
validate_common(db, line, invoice_id, tenant_id, company_id, errors, line_number)
|
||||
|
||||
if not errors.has_errors():
|
||||
# Obtener la factura para acceder a tipo de cambio, moneda y peso
|
||||
invoice: InvoiceHeader = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# 8. Validar customs.origin_country
|
||||
if not line.customs or not line.customs.origin_country:
|
||||
errors.add_error(
|
||||
field="customs.origin_country",
|
||||
message="País de Origen es obligatorio",
|
||||
solution="Selecciona el país de origen del item",
|
||||
code="REQUIRED"
|
||||
|
||||
if not invoice or not invoice.financials or not invoice.logistics:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}]",
|
||||
message="No se pudo obtener información de la factura",
|
||||
solution=[
|
||||
"Verificar que la factura existe y tiene datos financieros y logísticos"
|
||||
],
|
||||
code="INVOICE_DATA_MISSING",
|
||||
)
|
||||
return
|
||||
|
||||
# Obtener la clase para valores por defecto
|
||||
class_info: Class = (
|
||||
db.query(Class)
|
||||
.filter(
|
||||
Class.id == line.class_id,
|
||||
Class.tenant_id == tenant_id,
|
||||
Class.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# 9. Validar customs.fraction_type
|
||||
if not line.customs or not line.customs.fraction_type:
|
||||
errors.add_error(
|
||||
field="customs.fraction_type",
|
||||
message="Tipo de Tarifa es obligatorio",
|
||||
solution="Selecciona el tipo de tarifa (GENERAL, PROSEC, ALADI, TLCS)",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR TIPO DE CAMBIO
|
||||
# ==========================================
|
||||
exchange_rate = invoice.financials.exchange_rate or Decimal("1.0")
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR UNIDAD DE MEDIDA
|
||||
# ==========================================
|
||||
# Si no se proporcionó unidad de medida, usar la de la clase
|
||||
if not line.unit_of_measure and class_info:
|
||||
line.unit_of_measure = class_info.unit_of_measure
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR TIPOS DE MONEDA Y CALCULAR COSTOS
|
||||
# ==========================================
|
||||
currency_type = invoice.financials.currency_type
|
||||
unit_cost_capture = line.financial.unit_cost_capture or Decimal("0")
|
||||
|
||||
# Calcular costos según tipo de moneda
|
||||
if currency_type == "USD" or currency_type == "ME": # Moneda Extranjera (ME)
|
||||
line.financial.unit_cost_capture = unit_cost_capture
|
||||
line.financial.unit_cost_usd = unit_cost_capture
|
||||
line.financial.unit_cost_mxn = unit_cost_capture * exchange_rate
|
||||
elif currency_type == "MXN" or currency_type == "MN": # Moneda Nacional (MN)
|
||||
line.financial.unit_cost_capture = unit_cost_capture
|
||||
line.financial.unit_cost_usd = (
|
||||
unit_cost_capture / exchange_rate if exchange_rate else Decimal("0")
|
||||
)
|
||||
line.financial.unit_cost_mxn = unit_cost_capture
|
||||
# Si es otro tipo de moneda, dejamos el costo como está
|
||||
|
||||
# ==========================================
|
||||
# VALIDAR Y CONVERTIR PESOS NETOS
|
||||
# ==========================================
|
||||
invoice_weight_type = invoice.logistics.weight_type # 'kgs' o 'lbs'
|
||||
quantity = line.quantity.quantity or Decimal("0")
|
||||
net_weight_input = line.quantity.net_weight or Decimal("0")
|
||||
|
||||
# Determinar si la unidad de medida es de peso
|
||||
unit_is_kgs = line.unit_of_measure and line.unit_of_measure.upper() == "KGS"
|
||||
unit_is_lbs = line.unit_of_measure and line.unit_of_measure.upper() == "LB"
|
||||
|
||||
# Calcular peso neto en kilogramos (estándar interno)
|
||||
if unit_is_kgs:
|
||||
if invoice_weight_type == "kgs":
|
||||
line.quantity.net_weight = quantity
|
||||
else: # invoice en libras
|
||||
line.quantity.net_weight = quantity * Decimal("2.204624")
|
||||
elif unit_is_lbs:
|
||||
if invoice_weight_type == "kgs":
|
||||
line.quantity.net_weight = quantity / Decimal("2.204624")
|
||||
else: # invoice en libras
|
||||
line.quantity.net_weight = quantity
|
||||
else:
|
||||
# Otra unidad de medida - usar peso capturado y convertir si es necesario
|
||||
if invoice_weight_type == "kgs":
|
||||
# El peso capturado está en kilos
|
||||
line.quantity.net_weight = net_weight_input
|
||||
else:
|
||||
# El peso capturado está en libras, convertir a kilos
|
||||
line.quantity.net_weight = net_weight_input / Decimal("2.204624")
|
||||
|
||||
# ==========================================
|
||||
# CALCULAR PESO BRUTO
|
||||
# ==========================================
|
||||
gross_weight_input = line.quantity.gross_weight
|
||||
package_quantity = line.quantity.package_quantity or 0
|
||||
package_weight_unit = Decimal("0")
|
||||
|
||||
# Obtener peso unitario del bulto si existe
|
||||
if line.quantity.package_key:
|
||||
package: Package = (
|
||||
db.query(Package)
|
||||
.filter(
|
||||
Package.key == line.quantity.package_key,
|
||||
Package.tenant_id == tenant_id,
|
||||
Package.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if package and package.weight_unit:
|
||||
package_weight_unit = package.weight_unit
|
||||
|
||||
# Si no se proporcionó peso bruto, calcularlo
|
||||
if not gross_weight_input or gross_weight_input == 0:
|
||||
if invoice_weight_type == "kgs":
|
||||
line.quantity.gross_weight = line.quantity.net_weight + (
|
||||
package_weight_unit * package_quantity
|
||||
)
|
||||
else: # libras
|
||||
line.quantity.gross_weight = line.quantity.net_weight + (
|
||||
(package_weight_unit * Decimal("2.204624")) * package_quantity
|
||||
)
|
||||
else:
|
||||
# Convertir peso bruto capturado según tipo de factura
|
||||
if invoice_weight_type == "kgs":
|
||||
line.quantity.gross_weight = gross_weight_input
|
||||
else: # libras
|
||||
line.quantity.gross_weight = gross_weight_input / Decimal("2.204624")
|
||||
|
||||
# ==========================================
|
||||
# VALIDAR PESO BRUTO < PESO NETO
|
||||
# ==========================================
|
||||
if line.quantity.gross_weight < line.quantity.net_weight:
|
||||
line.quantity.gross_weight = line.quantity.net_weight + (
|
||||
package_weight_unit * package_quantity
|
||||
)
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR DESCRIPCIÓN DE BULTOS
|
||||
# ==========================================
|
||||
if package_quantity and package_quantity > 0 and line.quantity.package_key:
|
||||
package: Package = (
|
||||
db.query(Package)
|
||||
.filter(
|
||||
Package.key == line.quantity.package_key,
|
||||
Package.tenant_id == tenant_id,
|
||||
Package.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if package:
|
||||
line.quantity.package_description = package.description_es
|
||||
else:
|
||||
line.quantity.package_quantity = 0
|
||||
line.quantity.package_key = None
|
||||
line.quantity.package_description = None
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR FRACCIÓN AMERICANA POR DEFECTO
|
||||
# ==========================================
|
||||
if not line.customs.american_fraction and class_info and class_info.us_fraction:
|
||||
line.customs.american_fraction = class_info.us_fraction
|
||||
|
||||
# Buscar el advalorem de la fracción americana
|
||||
if line.customs.american_fraction:
|
||||
us_fraction: USTariffFraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.code == line.customs.american_fraction,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if us_fraction:
|
||||
# Si el tipo es 'ME' (Moneda Extranjera), usar costo fijo
|
||||
# De lo contrario, usar ad valorem
|
||||
if us_fraction.type_code == "foreign":
|
||||
line.customs.advalorem_american = us_fraction.fixed_cost
|
||||
else:
|
||||
line.customs.advalorem_american = us_fraction.ad_valorem
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR DESCRIPCIONES POR DEFECTO
|
||||
# ==========================================
|
||||
if not line.description.description_spanish and class_info:
|
||||
line.description.description_spanish = class_info.description_es
|
||||
|
||||
if not line.description.description_english and class_info:
|
||||
line.description.description_english = class_info.description_en
|
||||
|
||||
# ==========================================
|
||||
# NORMALIZAR CAMPOS DE TEXTO
|
||||
# ==========================================
|
||||
# Convertir a mayúsculas campos que lo requieran
|
||||
if line.description.brand:
|
||||
line.description.brand = line.description.brand.upper().strip()
|
||||
|
||||
if line.description.model:
|
||||
line.description.model = line.description.model.upper().strip()
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR VALORES POR DEFECTO DE IMPUESTOS
|
||||
# ==========================================
|
||||
# Si no se especificó pago de impuesto, tomar de preferencias del sistema (SisImp)
|
||||
# TODO: Implementar lectura de preferencias del sistema
|
||||
# Por ahora dejamos None si no se proporcionó
|
||||
|
||||
# Si no se especificó forma de pago, tomar de preferencias del sistema
|
||||
# TODO: Implementar lectura de preferencias del sistema
|
||||
|
||||
# Si no se especificó método de valoración, tomar de preferencias del sistema
|
||||
# TODO: Implementar lectura de preferencias del sistema
|
||||
|
||||
@@ -1,246 +1,202 @@
|
||||
"""
|
||||
Validaciones para actualización de items vía API.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.invoices.common.common_validators import invoice_exists
|
||||
from core.exceptions import ErrorCollector
|
||||
from api.v1.modules.a76.items.line_items.schemas import LineItemUpdate
|
||||
from .common import validate_string_not_empty, validate_positive_value
|
||||
|
||||
from ....line_items.models import LineItem
|
||||
from ....models import Item
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
)
|
||||
from .common import validate_common
|
||||
|
||||
|
||||
def validate_update(
|
||||
db: Session,
|
||||
line: LineItemUpdate,
|
||||
line: LineItem,
|
||||
existing_line: LineItem,
|
||||
invoice_id: int, # Passed from service
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
invoice_id: int = None,
|
||||
) -> None:
|
||||
line_number: int,
|
||||
):
|
||||
"""
|
||||
Validaciones para actualizar LineItems vía API.
|
||||
Incluye todas las validaciones de negocio de Clarion.
|
||||
|
||||
Args:
|
||||
db: Sesión de base de datos
|
||||
line: Datos del line item a actualizar
|
||||
tenant_id: ID del tenant
|
||||
company_id: ID de la compañía
|
||||
errors: Colector de errores
|
||||
invoice_id: ID de la factura asociada (opcional, para validar subpartidas)
|
||||
Validar y procesar actualización parcial de línea de importación temporal.
|
||||
Si un campo no se proporciona, se mantiene el valor existente.
|
||||
"""
|
||||
# 1. Validar line_number si se proporciona
|
||||
if line.line_number is not None and not line.line_number:
|
||||
errors.add_error(
|
||||
field="line_number",
|
||||
message="El número de línea no puede estar vacío",
|
||||
solution="Proporciona un número de línea válido",
|
||||
code="REQUIRED",
|
||||
validate_common(db, line, invoice_id, tenant_id, company_id, errors, line_number)
|
||||
|
||||
if not errors.has_errors():
|
||||
# Obtener la factura para acceder a tipo de cambio, moneda y peso
|
||||
invoice: InvoiceHeader = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# 2. Validar class_id si se proporciona
|
||||
if line.class_id is not None and not line.class_id:
|
||||
errors.add_error(
|
||||
field="class_id",
|
||||
message="Clase (ID) no puede estar vacío",
|
||||
solution="Selecciona una clasificación válida del catálogo",
|
||||
code="REQUIRED",
|
||||
)
|
||||
if not invoice or not invoice.financials or not invoice.logistics:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}]",
|
||||
message="No se pudo obtener información de la factura",
|
||||
solution=[
|
||||
"Verificar que la factura existe y tiene datos financieros y logísticos"
|
||||
],
|
||||
code="INVOICE_DATA_MISSING",
|
||||
)
|
||||
return
|
||||
|
||||
# 4. Validar unit_of_measure si se proporciona
|
||||
if line.unit_of_measure is not None and not line.unit_of_measure:
|
||||
errors.add_error(
|
||||
field="unit_of_measure",
|
||||
message="U.M. no puede estar vacío",
|
||||
solution="Proporciona una unidad de medida válida",
|
||||
code="REQUIRED",
|
||||
)
|
||||
# ==========================================
|
||||
# ACTUALIZACIÓN PARCIAL DE CAMPOS
|
||||
# Si no se proporciona, mantener valor existente
|
||||
# ==========================================
|
||||
|
||||
# 5. Validar cantidad si se proporciona
|
||||
if line.quantity:
|
||||
# Si se proporciona el objeto quantity, validar que quantity.quantity sea válido
|
||||
if line.quantity.quantity is not None:
|
||||
if line.quantity.quantity <= 0:
|
||||
errors.add_error(
|
||||
field="quantity.quantity",
|
||||
message="Quantity debe ser mayor a cero",
|
||||
solution="Proporciona una cantidad válida",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
# Tipo de cambio de la factura
|
||||
exchange_rate = invoice.financials.exchange_rate or Decimal("1.0")
|
||||
|
||||
# Unidad de medida
|
||||
if not line.unit_of_measure:
|
||||
line.unit_of_measure = existing_line.unit_of_measure
|
||||
|
||||
# Costo unitario
|
||||
if line.financial.unit_cost_capture is None:
|
||||
line.financial.unit_cost_capture = existing_line.financial.unit_cost_capture
|
||||
|
||||
# Convertir peso neto si se proporcionó
|
||||
invoice_weight_type = invoice.logistics.weight_type
|
||||
if line.quantity.net_weight is not None:
|
||||
# Se proporcionó nuevo peso neto, convertir según tipo
|
||||
net_weight_input = line.quantity.net_weight
|
||||
|
||||
if invoice_weight_type == "kgs":
|
||||
line.quantity.net_weight = net_weight_input
|
||||
else: # libras, convertir a kilos
|
||||
line.quantity.net_weight = net_weight_input / Decimal("2.204624")
|
||||
else:
|
||||
# Si se proporciona quantity pero quantity.quantity es None, es requerido
|
||||
errors.add_error(
|
||||
field="quantity.quantity",
|
||||
message="Quantity es obligatorio",
|
||||
solution="Proporciona una cantidad mayor a 0",
|
||||
code="REQUIRED",
|
||||
)
|
||||
# Mantener peso existente
|
||||
line.quantity.net_weight = existing_line.quantity.net_weight
|
||||
|
||||
# 6. Validar peso neto si se proporciona
|
||||
if line.quantity and line.quantity.net_weight is not None:
|
||||
if line.quantity.net_weight <= 0:
|
||||
errors.add_error(
|
||||
field="quantity.net_weight",
|
||||
message="Net Weight debe ser mayor a cero",
|
||||
solution="Proporciona un peso neto válido",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
# Convertir peso bruto si se proporcionó
|
||||
if line.quantity.gross_weight is not None:
|
||||
gross_weight_input = line.quantity.gross_weight
|
||||
|
||||
# 7. Validar costo unitario si se proporciona financial (excepto subpartidas)
|
||||
if line.financial:
|
||||
is_subitem = line.fa_data and line.fa_data.is_subitem if line.fa_data else False
|
||||
if invoice_weight_type == "kgs":
|
||||
line.quantity.gross_weight = gross_weight_input
|
||||
else: # libras, convertir a kilos
|
||||
line.quantity.gross_weight = gross_weight_input / Decimal("2.204624")
|
||||
else:
|
||||
# Mantener peso existente
|
||||
line.quantity.gross_weight = existing_line.quantity.gross_weight
|
||||
|
||||
if not is_subitem:
|
||||
has_cost = (
|
||||
line.financial.unit_cost_usd
|
||||
or line.financial.unit_cost_mxn
|
||||
or line.financial.unit_cost_capture
|
||||
)
|
||||
if not has_cost:
|
||||
errors.add_error(
|
||||
field="financial.unit_cost",
|
||||
message="Unit Cost es obligatorio",
|
||||
solution="Proporciona al menos un costo unitario (USD, MXN o captura)",
|
||||
code="REQUIRED",
|
||||
)
|
||||
# Validar que sean positivos
|
||||
if line.financial.unit_cost_usd is not None:
|
||||
if line.financial.unit_cost_usd <= 0:
|
||||
errors.add_error(
|
||||
field="financial.unit_cost_usd",
|
||||
message="Unit Cost (USD) debe ser mayor a cero",
|
||||
solution="Proporciona un costo unitario válido",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
if line.financial.unit_cost_mxn is not None:
|
||||
if line.financial.unit_cost_mxn <= 0:
|
||||
errors.add_error(
|
||||
field="financial.unit_cost_mxn",
|
||||
message="Unit Cost (MXN) debe ser mayor a cero",
|
||||
solution="Proporciona un costo unitario válido",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
if line.financial.unit_cost_capture is not None:
|
||||
if line.financial.unit_cost_capture <= 0:
|
||||
errors.add_error(
|
||||
field="financial.unit_cost_capture",
|
||||
message="Unit Cost (Captura) debe ser mayor a cero",
|
||||
solution="Proporciona un costo unitario válido",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
# Cantidad de bultos
|
||||
if line.quantity.package_quantity is None:
|
||||
line.quantity.package_quantity = existing_line.quantity.package_quantity
|
||||
|
||||
# 8. Validar datos aduanales si se proporcionan
|
||||
if line.customs:
|
||||
# Validar país de origen (OBLIGATORIO)
|
||||
if line.customs.origin_country is not None:
|
||||
if not line.customs.origin_country:
|
||||
errors.add_error(
|
||||
field="customs.origin_country",
|
||||
message="País de Origen es obligatorio",
|
||||
solution="Selecciona el país de origen del item",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# Validar tipo de tarifa (OBLIGATORIO)
|
||||
if line.customs.fraction_type is not None:
|
||||
if not line.customs.fraction_type:
|
||||
errors.add_error(
|
||||
field="customs.fraction_type",
|
||||
message="Tipo de Tarifa es obligatorio",
|
||||
solution="Selecciona el tipo de tarifa (GENERAL, PROSEC, ALADI, TLCS)",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# Validar preferencia arancelaria
|
||||
if line.customs.preference is not None and not line.customs.preference:
|
||||
errors.add_error(
|
||||
field="customs.preference",
|
||||
message="La preferencia arancelaria no puede estar vacía",
|
||||
solution="Selecciona la preferencia arancelaria",
|
||||
code="REQUIRED",
|
||||
)
|
||||
# Clave de bultos
|
||||
if not line.quantity.package_key:
|
||||
line.quantity.package_key = existing_line.quantity.package_key
|
||||
|
||||
# Validar formato de pago de impuestos
|
||||
if line.customs.tax_paid:
|
||||
val_tax = line.customs.tax_paid.upper()
|
||||
if val_tax not in ["SI", "NO", "S", "N"]:
|
||||
errors.add_error(
|
||||
field="customs.tax_paid",
|
||||
message="El valor de pago de impuesto debe ser SI/NO o S/N",
|
||||
solution="Proporciona un valor válido: SI, NO, S o N",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
# País de origen
|
||||
if not line.customs.origin_country:
|
||||
line.customs.origin_country = existing_line.customs.origin_country
|
||||
|
||||
# Validar forma de pago si existe
|
||||
if line.customs.payment_form:
|
||||
from api.v1.modules.a76.general_catalogs.forms_of_payment.models import (
|
||||
PaymentForm,
|
||||
)
|
||||
# Fracción arancelaria
|
||||
if not line.customs.fraction:
|
||||
line.customs.fraction = existing_line.customs.fraction
|
||||
|
||||
payment = (
|
||||
db.query(PaymentForm)
|
||||
# Tipo de fracción
|
||||
if not line.customs.fraction_type:
|
||||
line.customs.fraction_type = existing_line.customs.fraction_type
|
||||
|
||||
# Sector
|
||||
if not line.customs.sector:
|
||||
line.customs.sector = existing_line.customs.sector
|
||||
|
||||
# Fracción americana y su advalorem
|
||||
if line.customs.american_fraction:
|
||||
# Se proporcionó nueva fracción americana, buscar su advalorem
|
||||
us_fraction: USTariffFraction = (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
PaymentForm.code == line.customs.payment_form,
|
||||
PaymentForm.tenant_id == tenant_id,
|
||||
USTariffFraction.code == line.customs.american_fraction,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not payment:
|
||||
errors.add_error(
|
||||
field="customs.payment_form",
|
||||
message=f"La forma de pago '{line.customs.payment_form}' no es válida",
|
||||
solution="Selecciona una forma de pago válida del catálogo",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
|
||||
# 9. Validar descripción en español (OBLIGATORIA)
|
||||
if line.description and hasattr(line.description, 'description_spanish'):
|
||||
if line.description.description_spanish is not None:
|
||||
if not line.description.description_spanish.strip():
|
||||
errors.add_error(
|
||||
field="description.description_spanish",
|
||||
message="Descripción en Español es obligatoria",
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# 10. Validar subpartidas si se actualizan
|
||||
if line.fa_data and line.fa_data.is_subitem:
|
||||
# Es subpartida, debe tener partida principal
|
||||
if not line.fa_data.main_line_id:
|
||||
errors.add_error(
|
||||
field="fa_data.main_line_id",
|
||||
message="La subpartida debe tener asignada una partida principal",
|
||||
solution="Selecciona la partida principal de esta subpartida",
|
||||
code="REQUIRED",
|
||||
)
|
||||
elif invoice_id:
|
||||
# Validar que la partida principal exista en la misma factura
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
if us_fraction:
|
||||
if us_fraction.type_code == "ME":
|
||||
line.customs.advalorem_american = us_fraction.fixed_cost
|
||||
else:
|
||||
line.customs.advalorem_american = us_fraction.ad_valorem
|
||||
else:
|
||||
# Mantener fracción americana existente
|
||||
line.customs.american_fraction = existing_line.customs.american_fraction
|
||||
line.customs.advalorem_american = existing_line.customs.advalorem_american
|
||||
|
||||
parent = (
|
||||
db.query(LineItem)
|
||||
.join(LineItem.item)
|
||||
.filter(
|
||||
LineItem.line_number == line.fa_data.main_line_id,
|
||||
Item.invoice_id == invoice_id,
|
||||
LineItem.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
# Orden de compra
|
||||
if not line.reference.purchase_order:
|
||||
line.reference.purchase_order = existing_line.reference.purchase_order
|
||||
|
||||
# Descripciones
|
||||
if not line.description.description_spanish:
|
||||
line.description.description_spanish = (
|
||||
existing_line.description.description_spanish
|
||||
)
|
||||
|
||||
if not parent:
|
||||
errors.add_error(
|
||||
field="fa_data.main_line_id",
|
||||
message=f"La partida principal {line.fa_data.main_line_id} no existe en esta factura",
|
||||
solution="Verifica el número de la partida principal",
|
||||
code="NOT_FOUND",
|
||||
)
|
||||
elif parent.fa_data and parent.fa_data.is_subitem:
|
||||
errors.add_error(
|
||||
field="fa_data.main_line_id",
|
||||
message="La partida principal no puede ser otra subpartida",
|
||||
solution="Selecciona una partida normal como principal",
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
if not line.description.description_english:
|
||||
line.description.description_english = (
|
||||
existing_line.description.description_english
|
||||
)
|
||||
|
||||
if not line.description.extra_description:
|
||||
line.description.extra_description = (
|
||||
existing_line.description.extra_description
|
||||
)
|
||||
|
||||
# Marca y modelo
|
||||
if line.description.brand:
|
||||
line.description.brand = line.description.brand.upper().strip()
|
||||
else:
|
||||
line.description.brand = existing_line.description.brand
|
||||
|
||||
if line.description.model:
|
||||
line.description.model = line.description.model.upper().strip()
|
||||
else:
|
||||
line.description.model = existing_line.description.model
|
||||
|
||||
# Subpartidas (si aplica)
|
||||
# TODO: Implementar lógica de subpartidas si Loc:LevantarSubpartidas = 'S'
|
||||
|
||||
|
||||
# Número de parte
|
||||
if not line.part_number:
|
||||
line.part_number = existing_line.part_number
|
||||
|
||||
# Pago de impuesto
|
||||
if line.tax_payment is None:
|
||||
line.tax_payment = existing_line.tax_payment
|
||||
|
||||
# Forma de pago
|
||||
if not line.payment_method:
|
||||
line.payment_method = existing_line.payment_method
|
||||
|
||||
# Método de valoración
|
||||
if not line.valuation_method:
|
||||
if existing_line.valuation_method:
|
||||
line.valuation_method = existing_line.valuation_method
|
||||
# else: TODO: Tomar de SisImp:MetValor (preferencias del sistema)
|
||||
|
||||
# Número de entrada
|
||||
if not line.description.entry_number:
|
||||
line.description.entry_number = existing_line.description.entry_number
|
||||
|
||||
# Lote
|
||||
if not line.description.lot:
|
||||
line.description.lot = existing_line.description.lot
|
||||
|
||||
@@ -6,6 +6,13 @@ from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
|
||||
class FractionType:
|
||||
"""Enumeration for fraction types"""
|
||||
GENERAL = "GENERAL"
|
||||
PROSEC = "PROSEC"
|
||||
ALADI = "ALADI"
|
||||
TLCS = "TLCS"
|
||||
|
||||
class LineCustom(Base):
|
||||
"""
|
||||
@@ -22,7 +29,7 @@ class LineCustom(Base):
|
||||
|
||||
# Tariff/Customs Classifications
|
||||
fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCION / FRACCIONIMPO / FRACCIONEXPO
|
||||
fraction_type: Mapped[Optional[str]] = mapped_column(String(7)) # TIPOFRACCION / TIPOFRACCIONIMPO / TIPOFRACCIONEXPO
|
||||
fraction_type: Mapped[Optional[FractionType]] = mapped_column(String(7)) # TIPOFRACCION / TIPOFRACCIONIMPO / TIPOFRACCIONEXPO
|
||||
american_fraction: Mapped[Optional[str]] = mapped_column(String(16)) # FRACCIONAMERICANA
|
||||
alternate_fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCIONALTERNA
|
||||
reference_fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCIONREFERENCIA
|
||||
|
||||
@@ -46,14 +46,21 @@ class LineItemBase(BaseModel):
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
item_id: int = Field(..., description="ID of the parent item")
|
||||
line_number: int = Field(..., description="Line number")
|
||||
|
||||
# Part identification
|
||||
part_number_id: Optional[int] = Field(
|
||||
None, description="Part number", alias="part_number", serialization_alias="part_number_id"
|
||||
None,
|
||||
description="Part number",
|
||||
alias="part_number",
|
||||
serialization_alias="part_number_id",
|
||||
)
|
||||
component_part_number_id: Optional[int] = Field(
|
||||
None, description="Component part number", alias="component_part_number", serialization_alias="component_part_number_id"
|
||||
None,
|
||||
description="Component part number",
|
||||
alias="component_part_number",
|
||||
serialization_alias="component_part_number_id",
|
||||
)
|
||||
class_id: Optional[int] = Field(None, description="Class code")
|
||||
|
||||
@@ -181,6 +188,12 @@ class LineItemBase(BaseModel):
|
||||
class LineItemCreate(LineItemBase):
|
||||
"""Schema for creating line item with all nested data"""
|
||||
|
||||
# Override base fields - estos se asignan automáticamente en el service
|
||||
item_id: Optional[int] = Field(
|
||||
None, description="ID of the parent item (auto-assigned)"
|
||||
)
|
||||
line_number: Optional[int] = Field(None, description="Line number (auto-assigned)")
|
||||
|
||||
financial: Optional[LineFinancialCreate] = Field(
|
||||
None, description="Financial data for this line"
|
||||
)
|
||||
@@ -204,6 +217,8 @@ class LineItemCreate(LineItemBase):
|
||||
class LineItemUpdate(LineItemBase):
|
||||
"""Schema for updating line item with all nested data"""
|
||||
|
||||
# Override base fields - todos opcionales en updates
|
||||
item_id: Optional[int] = Field(None, description="ID of the parent item")
|
||||
line_number: Optional[int] = Field(None, description="Line number")
|
||||
financial: Optional[LineFinancialUpdate] = Field(
|
||||
None, description="Financial data for this line"
|
||||
|
||||
@@ -16,6 +16,10 @@ from sqlalchemy import and_, or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from api.v1.modules.a76.invoices.common.common_validators import (
|
||||
invoice_exists_by_id,
|
||||
invoice_updated,
|
||||
)
|
||||
from core.exceptions import ErrorCollector
|
||||
from .imports.temporary.validators.create import validate_create
|
||||
from .imports.temporary.validators.update import validate_update
|
||||
@@ -32,8 +36,6 @@ from .line_references.models import LineReference
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from .models import Item
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,6 +45,101 @@ class ItemService:
|
||||
Service for managing Items and related entities with tenant/company isolation
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _get_next_line_number(db: Session, invoice_id: int) -> int:
|
||||
"""Calculate the next line_number for a given invoice based on database."""
|
||||
from sqlalchemy import func
|
||||
|
||||
max_line = (
|
||||
db.query(func.max(LineItem.line_number))
|
||||
.join(Item, LineItem.item_id == Item.id)
|
||||
.filter(Item.invoice_id == invoice_id)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
return 1 if max_line is None else max_line + 1
|
||||
|
||||
@staticmethod
|
||||
def _renumber_all_invoice_lines(db: Session, invoice_id: int) -> None:
|
||||
"""Renumber all line_items for a given invoice to be consecutive (1, 2, 3, ...)."""
|
||||
items = db.query(Item).filter(Item.invoice_id == invoice_id).all()
|
||||
all_lines = [line for item in items for line in item.lines]
|
||||
all_lines.sort(key=lambda x: x.line_number if x.line_number else 0)
|
||||
|
||||
for idx, line in enumerate(all_lines, start=1):
|
||||
line.line_number = idx
|
||||
|
||||
@staticmethod
|
||||
def _lock_invoice(
|
||||
db: Session,
|
||||
invoice_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
) -> Optional[InvoiceHeader]:
|
||||
"""Lock invoice to prevent concurrent modifications. Returns locked invoice or adds error."""
|
||||
try:
|
||||
invoice = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.with_for_update()
|
||||
.first()
|
||||
)
|
||||
|
||||
if not invoice:
|
||||
errors.add_error(
|
||||
field="invoice_id",
|
||||
message="La factura no existe o no se pudo bloquear",
|
||||
code="LOCK_FAILED",
|
||||
value=str(invoice_id),
|
||||
)
|
||||
return invoice
|
||||
except Exception as e:
|
||||
logger.error(f"Error locking invoice {invoice_id}: {e}")
|
||||
errors.add_error(
|
||||
field="invoice_id",
|
||||
message="Error al intentar bloquear la factura",
|
||||
code="LOCK_ERROR",
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _create_line_nested_data(
|
||||
db: Session, line: LineItem, line_data, tenant_id: int, company_id: int
|
||||
) -> None:
|
||||
"""Create all nested data for a line item."""
|
||||
nested_models = [
|
||||
(line_data.financial, LineFinancial),
|
||||
(line_data.quantity, LineQuantity),
|
||||
(line_data.customs, LineCustom),
|
||||
(line_data.description, LineDescription),
|
||||
(line_data.reference, LineReference),
|
||||
]
|
||||
|
||||
for data, model_class in nested_models:
|
||||
if data:
|
||||
nested_dict = (
|
||||
data.model_dump(exclude_unset=True)
|
||||
if hasattr(data, "model_dump")
|
||||
else data.model_dump()
|
||||
)
|
||||
nested_dict["item_line_id"] = line.id
|
||||
db.add(model_class(**nested_dict))
|
||||
|
||||
# FA data uses line.id as primary key
|
||||
if line_data.fa_data:
|
||||
fa_dict = line_data.fa_data.model_dump(
|
||||
exclude_unset=True, exclude={"line_item_id"}
|
||||
)
|
||||
fa_dict.update(
|
||||
{"id": line.id, "tenant_id": tenant_id, "company_id": company_id}
|
||||
)
|
||||
db.add(FaLineItem(**fa_dict))
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, item_id: int, tenant_id: int, company_id: int
|
||||
@@ -108,7 +205,7 @@ class ItemService:
|
||||
search_term = f"%{filters['search']}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Item.invoice_number.ilike(search_term),
|
||||
Item.invoice_id.ilike(search_term),
|
||||
Item.reference_number.ilike(search_term),
|
||||
Item.order.ilike(search_term),
|
||||
Item.guide_number.ilike(search_term),
|
||||
@@ -163,78 +260,62 @@ class ItemService:
|
||||
errors = ErrorCollector()
|
||||
|
||||
# Validar que la factura exista y no esté actualizada (si viene invoice_id)
|
||||
invoice = None
|
||||
if item_data.invoice_id:
|
||||
invoice = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == item_data.invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not item_data.invoice_id:
|
||||
errors.add_required_error(field="invoice_id")
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
|
||||
if not invoice:
|
||||
errors.add_error(
|
||||
field="invoice_id",
|
||||
message="La factura especificada no existe",
|
||||
code="NOT_FOUND",
|
||||
value=str(item_data.invoice_id),
|
||||
)
|
||||
if not invoice_exists_by_id(
|
||||
db, item_data.invoice_id, tenant_id, company_id, errors
|
||||
):
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
if not invoice_updated(db, item_data.invoice_id, tenant_id, company_id, errors):
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
|
||||
# Lock invoice and pre-calculate line_numbers
|
||||
if not ItemService._lock_invoice(
|
||||
db, item_data.invoice_id, tenant_id, company_id, errors
|
||||
):
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
|
||||
line_numbers = []
|
||||
if item_data.lines:
|
||||
starting_line = ItemService._get_next_line_number(db, item_data.invoice_id)
|
||||
line_numbers = [starting_line + i for i in range(len(item_data.lines))]
|
||||
|
||||
# Validar cada line item que se va a crear
|
||||
if item_data.lines:
|
||||
for idx, line_data in enumerate(item_data.lines):
|
||||
# Convertir a LineItemCreate para validar
|
||||
line_create = LineItemCreate(**line_data.model_dump())
|
||||
line_number = line_numbers[idx] # Usar el line_number calculado
|
||||
|
||||
validate_create(db, line_create, tenant_id, company_id, errors)
|
||||
validate_create(
|
||||
db,
|
||||
line_data, # Schema Pydantic completo
|
||||
item_data.invoice_id, # invoice_id
|
||||
tenant_id,
|
||||
company_id,
|
||||
errors,
|
||||
line_number,
|
||||
)
|
||||
|
||||
# Validaciones adicionales específicas del negocio
|
||||
if line_data.fa_data and line_data.fa_data.is_subitem is None:
|
||||
errors.add_required_error(
|
||||
field=f"lines[{line_number}].fa_data.is_subitem"
|
||||
)
|
||||
|
||||
if line_data.fa_data and line_data.fa_data.subitem_number is None:
|
||||
errors.add_required_error(
|
||||
field=f"lines[{line_number}].fa_data.subitem_number"
|
||||
)
|
||||
|
||||
# Validar apóstrofes en número de parte
|
||||
if line_data.part_number_id and "'" in str(line_data.part_number_id):
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].part_number",
|
||||
field=f"lines[{line_number}].part_number",
|
||||
message=f"Advertencia: El Número de Parte contiene apóstrofes y serán omitidos",
|
||||
code="WARNING_APOSTROPHE",
|
||||
)
|
||||
|
||||
# Validar tipo de partida
|
||||
if hasattr(line_data, "item_type"):
|
||||
tipo_partida = line_data.item_type
|
||||
if tipo_partida and tipo_partida not in ["N", "S"]:
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].item_type",
|
||||
message=f"Tipo de partida debe ser 'N' (Normal) o 'S' (Subpartida), recibido: '{tipo_partida}'",
|
||||
code="INVALID_ITEM_TYPE",
|
||||
value=str(tipo_partida),
|
||||
)
|
||||
|
||||
# Si es subpartida (S), debe tener partida principal
|
||||
if tipo_partida == "S":
|
||||
if (
|
||||
not hasattr(line_data, "main_line_id")
|
||||
or not line_data.main_line_id
|
||||
):
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].main_line_id",
|
||||
message="Las subpartidas (tipo 'S') deben tener una partida principal",
|
||||
code="MISSING_MAIN_LINE",
|
||||
)
|
||||
|
||||
# Validar que el line_number sea consecutivo (si se especifica)
|
||||
if hasattr(line_data, "line_number") and line_data.line_number:
|
||||
expected_line = idx + 1
|
||||
if line_data.line_number != expected_line:
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].line_number",
|
||||
message=f"Número de línea esperado: {expected_line}, recibido: {line_data.line_number}",
|
||||
code="INVALID_LINE_SEQUENCE",
|
||||
value=str(line_data.line_number),
|
||||
)
|
||||
|
||||
# Si hay errores, lanzar excepción ANTES de intentar crear
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
|
||||
@@ -254,14 +335,6 @@ class ItemService:
|
||||
|
||||
# Create line items if provided
|
||||
for idx, line_data in enumerate(lines_data):
|
||||
# Extract nested data from line
|
||||
financial_data = line_data.financial
|
||||
quantity_data = line_data.quantity
|
||||
customs_data = line_data.customs
|
||||
description_data = line_data.description
|
||||
reference_data = line_data.reference
|
||||
fa_data = line_data.fa_data
|
||||
|
||||
line_dict = line_data.model_dump(
|
||||
exclude={
|
||||
"financial",
|
||||
@@ -272,66 +345,35 @@ class ItemService:
|
||||
"fa_data",
|
||||
}
|
||||
)
|
||||
line_dict["item_id"] = db_item.id
|
||||
line_dict["tenant_id"] = tenant_id
|
||||
line_dict["company_id"] = company_id
|
||||
line_dict.update(
|
||||
{
|
||||
"item_id": db_item.id,
|
||||
"tenant_id": tenant_id,
|
||||
"company_id": company_id,
|
||||
"line_number": (
|
||||
line_numbers[idx]
|
||||
if line_numbers
|
||||
else ItemService._get_next_line_number(
|
||||
db, item_data.invoice_id
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Map schema field names to model field names
|
||||
if "part_number_id" in line_dict:
|
||||
line_dict["part_number"] = line_dict.pop("part_number_id")
|
||||
if "component_part_number_id" in line_dict:
|
||||
line_dict["component_part_number"] = line_dict.pop("component_part_number_id")
|
||||
line_dict["part_number"] = line_dict.pop("part_number_id", None)
|
||||
line_dict["component_part_number"] = line_dict.pop(
|
||||
"component_part_number_id", None
|
||||
)
|
||||
|
||||
# Create line item
|
||||
db_line = LineItem(**line_dict)
|
||||
db.add(db_line)
|
||||
db.flush() # Get the line ID
|
||||
db.flush()
|
||||
|
||||
# Create financial data if provided
|
||||
if financial_data:
|
||||
financial_dict = financial_data.model_dump()
|
||||
financial_dict["item_line_id"] = db_line.id
|
||||
db_financial = LineFinancial(**financial_dict)
|
||||
db.add(db_financial)
|
||||
|
||||
# Create quantity data if provided
|
||||
if quantity_data:
|
||||
quantity_dict = quantity_data.model_dump()
|
||||
quantity_dict["item_line_id"] = db_line.id
|
||||
db_quantity = LineQuantity(**quantity_dict)
|
||||
db.add(db_quantity)
|
||||
|
||||
# Create customs data if provided
|
||||
if customs_data:
|
||||
customs_dict = customs_data.model_dump()
|
||||
customs_dict["item_line_id"] = db_line.id
|
||||
db_customs = LineCustom(**customs_dict)
|
||||
db.add(db_customs)
|
||||
|
||||
# Create description data if provided
|
||||
if description_data:
|
||||
description_dict = description_data.model_dump()
|
||||
description_dict["item_line_id"] = db_line.id
|
||||
db_description = LineDescription(**description_dict)
|
||||
db.add(db_description)
|
||||
|
||||
# Create reference data if provided
|
||||
if reference_data:
|
||||
reference_dict = reference_data.model_dump()
|
||||
reference_dict["item_line_id"] = db_line.id
|
||||
db_reference = LineReference(**reference_dict)
|
||||
db.add(db_reference)
|
||||
|
||||
# Create FA data if provided
|
||||
if fa_data:
|
||||
fa_dict = fa_data.model_dump(
|
||||
exclude={"line_item_id"}
|
||||
) # Exclude line_item_id from DTO
|
||||
fa_dict["id"] = db_line.id # FA table uses same ID as line item
|
||||
fa_dict["tenant_id"] = tenant_id
|
||||
fa_dict["company_id"] = company_id
|
||||
db_fa = FaLineItem(**fa_dict)
|
||||
db.add(db_fa)
|
||||
# Create all nested data
|
||||
ItemService._create_line_nested_data(
|
||||
db, db_line, line_data, tenant_id, company_id
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
@@ -367,39 +409,25 @@ class ItemService:
|
||||
# Validaciones con ErrorCollector
|
||||
errors = ErrorCollector()
|
||||
|
||||
# Si se está actualizando el invoice_id, validar la factura
|
||||
invoice = None
|
||||
if item_data.invoice_id:
|
||||
invoice = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == item_data.invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
# Lock invoice
|
||||
invoice_id_to_lock = (
|
||||
item_data.invoice_id if item_data.invoice_id else db_item.invoice_id
|
||||
)
|
||||
if not ItemService._lock_invoice(
|
||||
db, invoice_id_to_lock, tenant_id, company_id, errors
|
||||
):
|
||||
errors.raise_if_errors("Error al actualizar el item")
|
||||
|
||||
if not invoice:
|
||||
errors.add_error(
|
||||
field="invoice_id",
|
||||
message="La factura especificada no existe",
|
||||
code="NOT_FOUND",
|
||||
value=str(item_data.invoice_id),
|
||||
)
|
||||
else:
|
||||
# Si no se está actualizando invoice_id, obtener la factura actual por invoice_id
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
|
||||
invoice = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(InvoiceHeader.id == db_item.invoice_id)
|
||||
.first()
|
||||
)
|
||||
# Pre-calcular line_numbers para cada línea (en update, las líneas se renumeran desde 1)
|
||||
line_numbers = []
|
||||
if item_data.lines:
|
||||
line_numbers = [i + 1 for i in range(len(item_data.lines))]
|
||||
|
||||
# Validar cada line item que se va a actualizar
|
||||
if item_data.lines:
|
||||
for idx, line_data in enumerate(item_data.lines):
|
||||
line_number = line_numbers[idx] # Usar el line_number calculado
|
||||
|
||||
# Si el line tiene ID, es actualización; si no, es creación
|
||||
if hasattr(line_data, "id") and line_data.id:
|
||||
# Buscar el line item existente
|
||||
@@ -408,13 +436,28 @@ class ItemService:
|
||||
None,
|
||||
)
|
||||
if existing_line:
|
||||
# Convertir a LineItemUpdate para validar
|
||||
line_update = LineItemUpdate(**line_data.model_dump())
|
||||
validate_update(db, line_update, tenant_id, company_id, errors)
|
||||
# Validar update con línea existente
|
||||
validate_update(
|
||||
db,
|
||||
line_data, # Schema de update
|
||||
existing_line, # LineItem existente en DB
|
||||
invoice_id_to_lock, # invoice_id
|
||||
tenant_id,
|
||||
company_id,
|
||||
errors,
|
||||
line_number,
|
||||
)
|
||||
else:
|
||||
# Es un nuevo line item, validar como creación
|
||||
line_create = LineItemCreate(**line_data.model_dump())
|
||||
validate_create(db, line_create, tenant_id, company_id, errors)
|
||||
validate_create(
|
||||
db,
|
||||
line_data, # Schema Pydantic completo
|
||||
invoice_id_to_lock, # invoice_id
|
||||
tenant_id,
|
||||
company_id,
|
||||
errors,
|
||||
line_number,
|
||||
)
|
||||
|
||||
# Validaciones adicionales específicas del negocio
|
||||
# (Aplican tanto para crear como actualizar)
|
||||
@@ -422,7 +465,7 @@ class ItemService:
|
||||
# Validar apóstrofes en número de parte
|
||||
if line_data.part_number_id and "'" in str(line_data.part_number_id):
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].part_number",
|
||||
field=f"lines[{line_number}].part_number",
|
||||
message=f"Advertencia: El Número de Parte contiene apóstrofes y serán omitidos",
|
||||
solution=None,
|
||||
code="WARNING_APOSTROPHE",
|
||||
@@ -433,7 +476,7 @@ class ItemService:
|
||||
tipo_partida = line_data.item_type
|
||||
if tipo_partida and tipo_partida not in ["N", "S"]:
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].item_type",
|
||||
field=f"lines[{line_number}].item_type",
|
||||
message=f"Tipo de partida debe ser 'N' (Normal) o 'S' (Subpartida), recibido: '{tipo_partida}'",
|
||||
solution=None,
|
||||
code="INVALID_ITEM_TYPE",
|
||||
@@ -447,24 +490,12 @@ class ItemService:
|
||||
or not line_data.main_line_id
|
||||
):
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].main_line_id",
|
||||
field=f"lines[{line_number}].main_line_id",
|
||||
message="Las subpartidas (tipo 'S') deben tener una partida principal",
|
||||
solution=None,
|
||||
code="MISSING_MAIN_LINE",
|
||||
)
|
||||
|
||||
# Validar que el line_number sea consecutivo (si se especifica)
|
||||
if hasattr(line_data, "line_number") and line_data.line_number:
|
||||
expected_line = idx + 1
|
||||
if line_data.line_number != expected_line:
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].line_number",
|
||||
message=f"Número de línea esperado: {expected_line}, recibido: {line_data.line_number}",
|
||||
solution=None,
|
||||
code="INVALID_LINE_SEQUENCE",
|
||||
value=str(line_data.line_number),
|
||||
)
|
||||
|
||||
# Si hay errores, lanzar excepción ANTES de actualizar
|
||||
errors.raise_if_errors("Error al actualizar el item")
|
||||
|
||||
@@ -486,15 +517,7 @@ class ItemService:
|
||||
db.flush()
|
||||
|
||||
# Create new lines
|
||||
for line_data in lines_data:
|
||||
# Extract nested data from line
|
||||
financial_data = line_data.financial
|
||||
quantity_data = line_data.quantity
|
||||
customs_data = line_data.customs
|
||||
description_data = line_data.description
|
||||
reference_data = line_data.reference
|
||||
fa_data = line_data.fa_data
|
||||
|
||||
for idx, line_data in enumerate(lines_data):
|
||||
line_dict = line_data.model_dump(
|
||||
exclude={
|
||||
"financial",
|
||||
@@ -506,57 +529,32 @@ class ItemService:
|
||||
},
|
||||
exclude_unset=True,
|
||||
)
|
||||
line_dict["item_id"] = db_item.id
|
||||
line_dict["tenant_id"] = tenant_id
|
||||
line_dict["company_id"] = company_id
|
||||
line_dict.update(
|
||||
{
|
||||
"item_id": db_item.id,
|
||||
"tenant_id": tenant_id,
|
||||
"company_id": company_id,
|
||||
"line_number": idx + 1,
|
||||
}
|
||||
)
|
||||
|
||||
# Map schema field names to model field names
|
||||
if "part_number_id" in line_dict:
|
||||
line_dict["part_number"] = line_dict.pop("part_number_id")
|
||||
if "component_part_number_id" in line_dict:
|
||||
line_dict["component_part_number"] = line_dict.pop("component_part_number_id")
|
||||
line_dict["part_number"] = line_dict.pop("part_number_id", None)
|
||||
line_dict["component_part_number"] = line_dict.pop(
|
||||
"component_part_number_id", None
|
||||
)
|
||||
|
||||
db_line = LineItem(**line_dict)
|
||||
db.add(db_line)
|
||||
db.flush()
|
||||
|
||||
# Create nested data if provided
|
||||
if financial_data is not None:
|
||||
financial_dict = financial_data.model_dump(exclude_unset=True)
|
||||
financial_dict["item_line_id"] = db_line.id
|
||||
db.add(LineFinancial(**financial_dict))
|
||||
# Create all nested data
|
||||
ItemService._create_line_nested_data(
|
||||
db, db_line, line_data, tenant_id, company_id
|
||||
)
|
||||
|
||||
if quantity_data is not None:
|
||||
quantity_dict = quantity_data.model_dump(exclude_unset=True)
|
||||
quantity_dict["item_line_id"] = db_line.id
|
||||
db.add(LineQuantity(**quantity_dict))
|
||||
|
||||
if customs_data is not None:
|
||||
customs_dict = customs_data.model_dump(exclude_unset=True)
|
||||
customs_dict["item_line_id"] = db_line.id
|
||||
db.add(LineCustom(**customs_dict))
|
||||
|
||||
if description_data is not None:
|
||||
description_dict = description_data.model_dump(
|
||||
exclude_unset=True
|
||||
)
|
||||
description_dict["item_line_id"] = db_line.id
|
||||
db.add(LineDescription(**description_dict))
|
||||
|
||||
if reference_data is not None:
|
||||
reference_dict = reference_data.model_dump(exclude_unset=True)
|
||||
reference_dict["item_line_id"] = db_line.id
|
||||
db.add(LineReference(**reference_dict))
|
||||
|
||||
# Create FA data if provided
|
||||
if fa_data is not None:
|
||||
fa_dict = fa_data.model_dump(
|
||||
exclude_unset=True, exclude={"line_item_id"}
|
||||
)
|
||||
fa_dict["id"] = db_line.id # FA table uses same ID as line item
|
||||
fa_dict["tenant_id"] = tenant_id
|
||||
fa_dict["company_id"] = company_id
|
||||
db.add(FaLineItem(**fa_dict))
|
||||
# Renumber all lines for this invoice to ensure consecutive numbering
|
||||
ItemService._renumber_all_invoice_lines(db, db_item.invoice_id)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
@@ -582,7 +580,20 @@ class ItemService:
|
||||
if not db_item:
|
||||
return False
|
||||
|
||||
invoice_id = db_item.invoice_id
|
||||
errors = ErrorCollector()
|
||||
|
||||
# Lock the invoice
|
||||
if not ItemService._lock_invoice(
|
||||
db, invoice_id, tenant_id, company_id, errors
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Invoice not found or could not be locked"
|
||||
)
|
||||
|
||||
db.delete(db_item)
|
||||
db.flush()
|
||||
ItemService._renumber_all_invoice_lines(db, invoice_id)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ from api.v1.modules.a76.transportation.trailers.models import Trailer
|
||||
from api.v1.modules.a76.transportation.drivers.models import Driver
|
||||
|
||||
# --- MODELO DE FRACCIONES ---
|
||||
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
|
||||
|
||||
# --- MODELO DE UNIDADES DE MEDIDA ---
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
@@ -50,6 +50,10 @@ from .schemas import (
|
||||
FacturaImportacionCompleta,
|
||||
)
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
)
|
||||
|
||||
|
||||
class ConsolidadoImportacionMexService:
|
||||
def __init__(self):
|
||||
@@ -515,11 +519,7 @@ class ConsolidadoImportacionMexService:
|
||||
.filter(InvoiceHeader.id.in_(target_invoice_ids))
|
||||
.all()
|
||||
)
|
||||
invoice_map = {inv.id: inv for inv in invoices_list}
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
)
|
||||
invoice_map = {inv.id: inv for inv in invoices_list}
|
||||
|
||||
for line in lines:
|
||||
qty = (
|
||||
|
||||
@@ -30,7 +30,7 @@ from api.v1.modules.a76.transportation.trailers.models import Trailer
|
||||
from api.v1.modules.a76.transportation.drivers.models import Driver
|
||||
|
||||
# --- MODELO DE FRACCIONES ---
|
||||
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
|
||||
|
||||
# --- SCHEMAS ---
|
||||
from ...mex.schemas import (
|
||||
@@ -345,7 +345,7 @@ class ConsolidadoImportacionMexService:
|
||||
invoices_list = db.query(InvoiceHeader).filter(InvoiceHeader.id.in_(target_invoice_ids)).all()
|
||||
invoice_map = {inv.id: inv for inv in invoices_list}
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.us_tariff_fractions.models import USTariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction
|
||||
|
||||
for line in lines:
|
||||
qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first()
|
||||
|
||||
@@ -32,7 +32,7 @@ from api.v1.modules.a76.transportation.trailers.models import Trailer
|
||||
from api.v1.modules.a76.transportation.drivers.models import Driver
|
||||
|
||||
# --- MODELO DE FRACCIONES ---
|
||||
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
|
||||
|
||||
# --- MODELO DE UNIDADES DE MEDIDA ---
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
|
||||
@@ -30,7 +30,7 @@ from api.v1.modules.a76.transportation.trailers.models import Trailer
|
||||
from api.v1.modules.a76.transportation.drivers.models import Driver
|
||||
|
||||
# --- MODELO DE FRACCIONES ---
|
||||
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
|
||||
|
||||
# --- SCHEMAS ---
|
||||
from ...mex.schemas import (
|
||||
|
||||
@@ -30,7 +30,7 @@ from api.v1.modules.a76.transportation.trailers.models import Trailer
|
||||
from api.v1.modules.a76.transportation.drivers.models import Driver
|
||||
|
||||
# --- MODELO DE FRACCIONES ---
|
||||
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
|
||||
|
||||
# --- SCHEMAS ---
|
||||
# Reuse schemas from neighbor package as they fit the same data structure
|
||||
|
||||
@@ -31,7 +31,7 @@ from api.v1.modules.a76.transportation.trailers.models import Trailer
|
||||
from api.v1.modules.a76.transportation.drivers.models import Driver
|
||||
|
||||
# --- MODELO DE FRACCIONES ---
|
||||
from api.v1.modules.a76.general_catalogs.tariff_fractions.models import TariffFraction
|
||||
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction
|
||||
|
||||
# --- SCHEMAS ---
|
||||
from .schemas import (
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi import APIRouter
|
||||
from .customs_brokers.routes import router as customs_broker_router
|
||||
|
||||
# Importar routers de módulos
|
||||
from .general_catalogs.router import router as general_catalogs_router
|
||||
from .invoices.routes import router as invoices_router
|
||||
from .items.routes import router as items_router
|
||||
from .classes import router as classes_router
|
||||
@@ -18,33 +19,10 @@ from .general_catalogs.company import router as company_router
|
||||
from .country_rule_oct.routes import router as country_rule_oct_router
|
||||
from .transportation.drivers.routes import router as drivers_router
|
||||
from .doc_types_dig.routes import router as doc_types_dig_router
|
||||
from .general_catalogs.exchange_rate.routes import router as exchange_rate_router
|
||||
from .general_catalogs.identifiers.routes import router as identifiers_router
|
||||
from .fraction_rule_octave.routes import router as fraction_rule_octave_router
|
||||
from .general_catalogs.packages.routes import router as package_router
|
||||
from .general_catalogs.ports.routes import router as ports_router
|
||||
from .general_catalogs.tariff_fractions.routes import router as tariff_fractions_router
|
||||
from .general_catalogs.us_tariff_fractions.routes import router as us_tariff_fractions_router
|
||||
from .general_catalogs.depreciation_catalog.routes import router as depreciation_catalog_router
|
||||
from .general_catalogs.fda_catalog.routes import router as fda_catalog_router
|
||||
from .parts import router as parts_router
|
||||
from .pedmientos.router import router as pedimentos_router
|
||||
from .permission_rule_oct.routes import router as permission_rule_oct_router
|
||||
from .general_catalogs.seal.routes import router as seal_router
|
||||
from .general_catalogs.units_of_measure.routes import router as units_of_measure_router
|
||||
from .general_catalogs.concepts.routes import router as concepts_router
|
||||
from .general_catalogs.customs_broker_concepts.routes import router as customs_broker_concepts_router
|
||||
from .general_catalogs.classification_concepts.routes import router as classification_concepts_router
|
||||
from .general_catalogs.unit_conversions.routes import router as unit_conversions_router
|
||||
from .general_catalogs.equivalencies.routes import router as equivalencies_router
|
||||
from .general_catalogs.multi_currency_types.routes import router as multi_currency_types_router
|
||||
from .general_catalogs.inpc.routes import router as inpc_router
|
||||
from .general_catalogs.legends.routes import router as legends_router
|
||||
from .general_catalogs.signatures.routes import router as signatures_router
|
||||
from .general_catalogs.error_catalogs.routes import router as error_catalogs_router
|
||||
from .general_catalogs.doda.routes import router as doda_router
|
||||
from .general_catalogs.prevalidators.routes import router as prevalidators_router
|
||||
from .general_catalogs.electronic_notices.routes import router as electronic_notices_router
|
||||
from .transportation.trailers.routes import router as trailers_router
|
||||
from .transportation.transporters.routes import router as transporters_router
|
||||
from .transportation.vehicles.routes import router as vehicles_router
|
||||
@@ -66,63 +44,25 @@ from .manifests.manifiesto_anexo.routes import router as manifest_anexos_router
|
||||
router = APIRouter()
|
||||
|
||||
# Registrar módulos
|
||||
router.include_router(general_catalogs_router, prefix="/a76", tags=["a76 / general_catalogs"])
|
||||
router.include_router(invoices_router, prefix="/a76", tags=["a76 / invoices"])
|
||||
router.include_router(items_router, prefix="/a76", tags=["a76 / items"])
|
||||
router.include_router(invoice_settings_router)
|
||||
router.include_router(item_presets_router, prefix="/a76/item-presets", tags=["a76 / item_presets"])
|
||||
router.include_router(pedimentos_router, prefix="/a76")
|
||||
router.include_router(
|
||||
client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"]
|
||||
)
|
||||
router.include_router(company_router, prefix="/a76", tags=["a76 / company"])
|
||||
router.include_router(client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"])
|
||||
router.include_router(classes_router, prefix="/a76", tags=["a76 / classes"])
|
||||
router.include_router(parts_router, prefix="/a76", tags=["a76 / parts"])
|
||||
router.include_router(
|
||||
permission_rule_oct_router, prefix="/a76", tags=["a76 / permission_rule_oct"]
|
||||
)
|
||||
router.include_router(package_router, prefix="/a76")
|
||||
router.include_router(ports_router, prefix="/a76")
|
||||
router.include_router(tariff_fractions_router, prefix="/a76")
|
||||
router.include_router(us_tariff_fractions_router, prefix="/a76")
|
||||
router.include_router(depreciation_catalog_router, prefix="/a76")
|
||||
router.include_router(fda_catalog_router, prefix="/a76")
|
||||
router.include_router(seal_router, prefix="/a76", tags=["a76 / seal"])
|
||||
router.include_router(units_of_measure_router, prefix="/a76")
|
||||
router.include_router(
|
||||
fraction_rule_octave_router, prefix="/a76", tags=["a76 / fraction_rule_octave"]
|
||||
)
|
||||
router.include_router(identifiers_router, prefix="/a76")
|
||||
router.include_router(
|
||||
country_rule_oct_router, prefix="/a76", tags=["a76 / country_rule_oct"]
|
||||
)
|
||||
router.include_router(exchange_rate_router, prefix="/a76",
|
||||
tags=["a76 / exchange_rate"])
|
||||
router.include_router(permission_rule_oct_router, prefix="/a76", tags=["a76 / permission_rule_oct"])
|
||||
router.include_router(fraction_rule_octave_router, prefix="/a76", tags=["a76 / fraction_rule_octave"])
|
||||
router.include_router(country_rule_oct_router, prefix="/a76", tags=["a76 / country_rule_oct"])
|
||||
router.include_router(trailers_router, prefix="/a76/transportation", tags=["a76 / trailers"])
|
||||
router.include_router(
|
||||
customs_broker_router, prefix="/a76", tags=["a76 / customs_broker"]
|
||||
)
|
||||
router.include_router(customs_broker_router, prefix="/a76", tags=["a76 / customs_broker"])
|
||||
router.include_router(doc_types_dig_router, prefix="/a76", tags=["a76 / document_types_digitization"])
|
||||
router.include_router(drivers_router, prefix="/a76", tags=["a76 / drivers"])
|
||||
router.include_router(transporters_router, prefix="/a76",
|
||||
tags=["a76 / transporters"])
|
||||
router.include_router(transporters_router, prefix="/a76", tags=["a76 / transporters"])
|
||||
router.include_router(vehicles_router, prefix="/a76/transportation", tags=["a76 / vehicles"])
|
||||
|
||||
# Registrar catálogos generales adicionales
|
||||
router.include_router(concepts_router, prefix="/a76")
|
||||
router.include_router(customs_broker_concepts_router, prefix="/a76")
|
||||
router.include_router(classification_concepts_router, prefix="/a76")
|
||||
router.include_router(unit_conversions_router, prefix="/a76")
|
||||
router.include_router(equivalencies_router, prefix="/a76")
|
||||
router.include_router(multi_currency_types_router, prefix="/a76")
|
||||
router.include_router(inpc_router, prefix="/a76")
|
||||
router.include_router(legends_router, prefix="/a76")
|
||||
router.include_router(signatures_router, prefix="/a76")
|
||||
router.include_router(error_catalogs_router, prefix="/a76")
|
||||
router.include_router(doda_router, prefix="/a76")
|
||||
router.include_router(prevalidators_router, prefix="/a76")
|
||||
router.include_router(electronic_notices_router, prefix="/a76")
|
||||
|
||||
|
||||
# Registrar router de tipos de material públicos
|
||||
router.include_router(
|
||||
material_types_router,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from core.database import Base
|
||||
from sqlalchemy import PrimaryKeyConstraint, SmallInteger, String
|
||||
from sqlalchemy import Boolean, PrimaryKeyConstraint, SmallInteger, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ class Sector(Base):
|
||||
description: Mapped[str] = mapped_column(
|
||||
String(150), nullable=False
|
||||
) # descripción oficial (en español)
|
||||
authorized: Mapped[SmallInteger] = mapped_column(
|
||||
SmallInteger
|
||||
) # 1 = autorizado, 0 = no autorizado
|
||||
authorized: Mapped[bool] = mapped_column(
|
||||
Boolean
|
||||
) # True = autorizado, False = no autorizado
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Sector(key={self.key}, description={self.description}, authorized={self.authorized})>"
|
||||
|
||||
320
backend/api/v1/modules/sitar/README.md
Normal file
320
backend/api/v1/modules/sitar/README.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# SITAR API Module
|
||||
|
||||
Módulo reorganizado para integración con la API externa de SITAR (Sistema de Información de Aranceles).
|
||||
|
||||
## Estructura
|
||||
|
||||
Cada recurso ahora tiene su propia carpeta con separación de responsabilidades:
|
||||
|
||||
```
|
||||
backend/api/v1/modules/sitar/
|
||||
├── common/ # Servicio base compartido
|
||||
│ ├── __init__.py
|
||||
│ └── base_service.py # SitarAPIBaseService con autenticación
|
||||
├── tlcs/ # TLCS (Tratados de Libre Comercio)
|
||||
│ ├── __init__.py
|
||||
│ ├── schemas.py # TLCSResponse
|
||||
│ ├── service.py # TLCSService
|
||||
│ └── router.py # Endpoints FastAPI
|
||||
├── fracciones/ # Fracciones arancelarias mexicanas
|
||||
├── fracciones_usa/ # Fracciones USA
|
||||
├── regulaciones/ # Regulaciones y restricciones
|
||||
├── prosec/ # PROSEC
|
||||
├── precios_estimados/ # Precios estimados
|
||||
├── fundamentos_tlc/ # Fundamentos TLC
|
||||
├── aladi2/ # ALADI2
|
||||
├── cuotas2/ # Cuotas compensatorias
|
||||
├── cupos/ # Cupos de importación
|
||||
├── fracciones_anteriores/ # Historial de fracciones
|
||||
├── informacion_general/ # Información general
|
||||
├── ieps/ # IEPS
|
||||
├── noms/ # Normas Oficiales Mexicanas
|
||||
├── rcg2/ # Reglas de Carácter General
|
||||
├── reit/ # REIT
|
||||
├── requisito_previo/ # Requisitos previos
|
||||
├── vehiculos_marcas/ # Marcas de vehículos
|
||||
├── vehiculos_modelos/ # Modelos de vehículos
|
||||
├── __init__.py # Exporta todos los servicios
|
||||
└── main_router.py # Router principal con todos los endpoints
|
||||
```
|
||||
|
||||
## Uso del Servicio
|
||||
|
||||
### Desde cualquier parte del código
|
||||
|
||||
Cada recurso tiene su propio servicio **singleton** que maneja automáticamente la autenticación:
|
||||
|
||||
```python
|
||||
from api.v1.modules.sitar.tlcs import TLCSService
|
||||
from api.v1.modules.sitar.fracciones import FraccionesService
|
||||
|
||||
# Obtener instancia singleton
|
||||
tlcs_service = TLCSService.get_instance()
|
||||
|
||||
# Buscar TLCS (async)
|
||||
tlcs_data = await tlcs_service.search(
|
||||
fraccion="84716001",
|
||||
pais="USA",
|
||||
limit=100
|
||||
)
|
||||
|
||||
# Procesar resultados
|
||||
for tlcs in tlcs_data:
|
||||
print(f"Tasa: {tlcs.TASATXT}, País: {tlcs.PAIS}")
|
||||
```
|
||||
|
||||
### Desde código síncrono
|
||||
|
||||
Si estás en contexto síncrono, usa `asyncio.run()`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from api.v1.modules.sitar.tlcs import TLCSService
|
||||
|
||||
tlcs_service = TLCSService.get_instance()
|
||||
tlcs_data = asyncio.run(tlcs_service.search(fraccion="84716001", pais="USA"))
|
||||
```
|
||||
|
||||
## Métodos Disponibles
|
||||
|
||||
### TLCS (Tratados de Libre Comercio)
|
||||
|
||||
- `search_tlcs(fraccion, pais, nico, skip, limit)` - Buscar TLCS
|
||||
- `get_tlcs_by_id(sysid, fraccion)` - Obtener TLCS por ID
|
||||
|
||||
### Fracciones Arancelarias
|
||||
|
||||
- `search_fracciones(fraccion, nico, skip, limit)` - Buscar fracciones mexicanas
|
||||
- `get_fraccion_by_id(sysid)` - Obtener fracción por ID
|
||||
- `search_fracciones_usa(fraccion, skip, limit)` - Buscar fracciones USA
|
||||
- `get_fraccion_usa_by_id(consecutivo)` - Obtener fracción USA por ID
|
||||
- `search_fracciones_anteriores(fraccion_actual, fraccion_anterior, skip, limit)` - Buscar historial de fracciones
|
||||
- `get_fracciones_anteriores_by_id(sysid)` - Obtener historial por ID
|
||||
|
||||
### Regulaciones y Restricciones
|
||||
|
||||
- `search_regulaciones(fraccion, nico, skip, limit)` - Buscar regulaciones
|
||||
- `get_regulacion_by_id(sysid)` - Obtener regulación por ID
|
||||
- `search_noms(fraccion, pais, nico, skip, limit)` - Buscar Normas Oficiales Mexicanas
|
||||
- `get_noms_by_id(sysid)` - Obtener NOM por ID
|
||||
- `search_requisito_previo(fraccion, nico, skip, limit)` - Buscar requisitos previos
|
||||
- `get_requisito_previo_by_id(sysid)` - Obtener requisito previo por ID
|
||||
|
||||
### PROSEC y Programas de Promoción
|
||||
|
||||
- `search_prosec(fraccion, nico, skip, limit)` - Buscar PROSEC
|
||||
- `get_prosec_by_id(sysid)` - Obtener PROSEC por ID
|
||||
- `search_reit(fraccion, nico, skip, limit)` - Buscar REIT
|
||||
- `get_reit_by_id(sysid)` - Obtener REIT por ID
|
||||
|
||||
### Precios e Impuestos
|
||||
|
||||
- `search_precios_estimados(fraccion, nico, skip, limit)` - Buscar precios estimados
|
||||
- `get_precio_estimado_by_id(sysid)` - Obtener precio estimado por ID
|
||||
- `search_ieps(fraccion, nico, skip, limit)` - Buscar IEPS
|
||||
- `get_ieps_by_id(consecutivo)` - Obtener IEPS por ID
|
||||
|
||||
### Fundamentos y Acuerdos
|
||||
|
||||
- `search_fundamentos_tlc(fraccion, nico, tipat_only, skip, limit)` - Buscar fundamentos TLC
|
||||
- `get_fundamento_tlc_by_id(sysid)` - Obtener fundamento por ID
|
||||
- `search_aladi2(fraccion, pais, nico, skip, limit)` - Buscar ALADI2
|
||||
- `get_aladi2_by_id(sysid)` - Obtener ALADI2 por ID
|
||||
|
||||
### Cuotas y Cupos
|
||||
|
||||
- `search_cuotas2(fraccion, pais, nico, skip, limit)` - Buscar cuotas compensatorias
|
||||
- `get_cuotas2_by_id(sysid)` - Obtener cuota por ID
|
||||
- `search_cupos(fraccion, nico, skip, limit)` - Buscar cupos de importación
|
||||
- `get_cupos_by_id(sysid)` - Obtener cupo por ID
|
||||
|
||||
### Reglas de Carácter General
|
||||
|
||||
- `search_rcg2(fraccion, nico, skip, limit)` - Buscar RCG2
|
||||
- `get_rcg2_by_id(sysid)` - Obtener RCG2 por ID
|
||||
|
||||
### Información General
|
||||
|
||||
- `search_informacion_general(fraccion, nico, skip, limit)` - Buscar información general
|
||||
- `get_informacion_general_by_id(sysid)` - Obtener información general por ID
|
||||
|
||||
### Vehículos
|
||||
|
||||
- `search_vehiculos_marcas(fraccion, marca, skip, limit)` - Buscar marcas de vehículos
|
||||
- `get_vehiculos_marcas_by_id(sysid)` - Obtener marca por ID
|
||||
- `search_vehiculos_modelos(fraccion, marca, modelo, skip, limit)` - Buscar modelos de vehículos
|
||||
- `get_vehiculos_modelos_by_id(sysid)` - Obtener modelo por ID
|
||||
|
||||
## Configuración
|
||||
|
||||
Requiere las siguientes variables de entorno:
|
||||
|
||||
```bash
|
||||
SITAR_API_URL=https://api.sitar.example.com
|
||||
SITAR_API_USER=your_username
|
||||
SITAR_API_PASSWORD=your_password
|
||||
```
|
||||
|
||||
## Servicios Disponibles
|
||||
|
||||
Cada módulo sigue el mismo patrón con métodos `search()` y `get_by_id()`:
|
||||
|
||||
### TLCS - `TLCSService`
|
||||
|
||||
```python
|
||||
from api.v1.modules.sitar.tlcs import TLCSService
|
||||
service = TLCSService.get_instance()
|
||||
await service.search(fraccion="84716001", pais="USA", nico=None, skip=0, limit=100)
|
||||
await service.get_by_id(sysid=123, fraccion="84716001")
|
||||
```
|
||||
|
||||
### Fracciones - `FraccionesService`
|
||||
|
||||
```python
|
||||
from api.v1.modules.sitar.fracciones import FraccionesService
|
||||
service = FraccionesService.get_instance()
|
||||
await service.search(fraccion="84716001", nico=None, skip=0, limit=100)
|
||||
await service.get_by_id(sysid=123)
|
||||
```
|
||||
|
||||
### Fracciones USA - `FraccionesUSAService`
|
||||
|
||||
```python
|
||||
from api.v1.modules.sitar.fracciones_usa import FraccionesUSAService
|
||||
service = FraccionesUSAService.get_instance()
|
||||
await service.search(fraccion="84716001", skip=0, limit=100)
|
||||
await service.get_by_id(consecutivo=123)
|
||||
```
|
||||
|
||||
### Otros servicios disponibles:
|
||||
|
||||
- **RegulacionesService** - Regulaciones y restricciones
|
||||
- **ProsecService** - PROSEC (Programa de Promoción Sectorial)
|
||||
- **PreciosEstimadosService** - Precios estimados
|
||||
- **FundamentosTLCService** - Fundamentos de tratados de libre comercio
|
||||
- **Aladi2Service** - ALADI2 (Asociación Latinoamericana de Integración)
|
||||
- **Cuotas2Service** - Cuotas compensatorias
|
||||
- **CuposService** - Cupos de importación
|
||||
- **FraccionesAnterioresService** - Historial de fracciones
|
||||
- **InformacionGeneralService** - Información general de fracciones
|
||||
- **IepsService** - IEPS (Impuesto Especial sobre Producción y Servicios)
|
||||
- **NomsService** - Normas Oficiales Mexicanas
|
||||
- **Rcg2Service** - Reglas de Carácter General
|
||||
- **ReitService** - Registro de Empresas de Industria Terminal
|
||||
- **RequisitoPrevioService** - Requisitos previos
|
||||
- **VehiculosMarcasService** - Marcas de vehículos
|
||||
- **VehiculosModelosService** - Modelos de vehículos
|
||||
|
||||
```### Desde endpoints asíncronos
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from api.v1.modules.sitar.tlcs import TLCSService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/tariff/{fraccion}")
|
||||
async def get_tariff_info(fraccion: str, country: str = "USA"):
|
||||
"""Endpoint que consulta información arancelaria"""
|
||||
try:
|
||||
tlcs_service = TLCSService.get_instance()
|
||||
data = await tlcs_service.search(fraccion=fraccion, pais=country)
|
||||
return {"success": True, "data": data}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
```
|
||||
## Arquitectura
|
||||
|
||||
### Servicio Base Compartido
|
||||
|
||||
Todos los servicios heredan de `SitarAPIBaseService` que maneja:
|
||||
|
||||
- Autenticación con token caching
|
||||
- Renovación automática de tokens
|
||||
- Manejo de errores HTTP
|
||||
- Timeout configurable
|
||||
|
||||
### Singleton Pattern
|
||||
|
||||
Cada servicio usa el patrón singleton para:
|
||||
|
||||
- Compartir la conexión HTTP
|
||||
- Reutilizar tokens de autenticación
|
||||
- Evitar múltiples instancias
|
||||
|
||||
### Separación de Responsabilidades
|
||||
|
||||
- **schemas.py**: Modelos Pydantic para validación de datos
|
||||
- **service.py**: Lógica de negocio y llamadas a API
|
||||
- **router.py**: Endpoints FastAPI (opcional)
|
||||
- **__init__.py**: Exportaciones públicas
|
||||
|
||||
## Notas Importantes
|
||||
|
||||
1. **Autenticación**: El token se cachea y renueva automáticamente
|
||||
2. **Timeouts**: Configurado a 10 segundos por defecto
|
||||
3. **Límites**: Máximo 1000 registros por consulta
|
||||
4. **Errores**: Todos los servicios lanzan excepciones httpx en caso de error
|
||||
5. **Async/Sync**: Los servicios son async, usa `asyncio.run()` en código síncrono
|
||||
|
||||
```
|
||||
|
||||
Esto expondrá endpoints como:
|
||||
- `GET /api/v1/sitar/tlcs/` - Buscar TLCS
|
||||
- `GET /api/v1/sitar/fracciones/` - Buscar fracciones
|
||||
- etc.
|
||||
|
||||
## Ejemplo Completo
|
||||
|
||||
```python
|
||||
from api.v1.modules.sitar import SitarAPIService
|
||||
|
||||
async def get_tariff_info(fraccion: str, country: str):
|
||||
"""Obtener información arancelaria completa"""
|
||||
sitar = SitarAPIService.get_instance()
|
||||
|
||||
try:
|
||||
# Buscar TLCS
|
||||
tlcs = await sitar.search_tlcs(fraccion=fraccion, pais=country)
|
||||
|
||||
if tlcs:
|
||||
first_tlcs = tlcs[0]
|
||||
return {
|
||||
"rate": first_tlcs.TASATXT,
|
||||
"adv_impo": float(first_tlcs.TASA1NUM or 0.0),
|
||||
"country": first_tlcs.PAIS
|
||||
}
|
||||
|
||||
# Si no hay TLCS, buscar fracción general
|
||||
fracciones = await sitar.search_fracciones(fraccion=fraccion)
|
||||
|
||||
if fracciones:
|
||||
first_frac = fracciones[0]
|
||||
return {
|
||||
"rate": first_frac.ADVIMPOTXT,
|
||||
"adv_impo": float(first_frac.ADVIMPONUM or 0.0),
|
||||
"description": first_frac.DESCRIPCION
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return None
|
||||
```
|
||||
## Manejo de Errores
|
||||
|
||||
El servicio lanza excepciones `httpx.HTTPError` en caso de error:
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
try:
|
||||
tlcs = await sitar.search_tlcs(fraccion="invalid")
|
||||
except httpx.HTTPStatusError as e:
|
||||
print(f"HTTP Error: {e.response.status_code}")
|
||||
except httpx.RequestError as e:
|
||||
print(f"Request Error: {e}")
|
||||
except Exception as e:
|
||||
print(f"General Error: {e}")
|
||||
```
|
||||
90
backend/api/v1/modules/sitar/__init__.py
Normal file
90
backend/api/v1/modules/sitar/__init__.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
SITAR API Module
|
||||
|
||||
Módulo reorganizado para integración con la API externa de SITAR.
|
||||
Cada recurso tiene su propia carpeta con schemas, service y router.
|
||||
|
||||
Estructura:
|
||||
backend/api/v1/modules/sitar/
|
||||
├── common/ # Base service compartido
|
||||
├── tlcs/ # TLCS (Tratados de Libre Comercio)
|
||||
├── fracciones/ # Fracciones arancelarias mexicanas
|
||||
├── fracciones_usa/ # Fracciones arancelarias USA
|
||||
├── regulaciones/ # Regulaciones y restricciones
|
||||
├── prosec/ # PROSEC
|
||||
├── precios_estimados/ # Precios estimados
|
||||
├── fundamentos_tlc/ # Fundamentos TLC
|
||||
├── aladi2/ # ALADI2
|
||||
├── cuotas2/ # Cuotas compensatorias
|
||||
├── cupos/ # Cupos de importación
|
||||
├── fracciones_anteriores/ # Historial de fracciones
|
||||
├── informacion_general/ # Información general
|
||||
├── ieps/ # IEPS
|
||||
├── noms/ # Normas Oficiales Mexicanas
|
||||
├── rcg2/ # Reglas de Carácter General
|
||||
├── reit/ # REIT
|
||||
├── requisito_previo/ # Requisitos previos
|
||||
├── vehiculos_marcas/ # Marcas de vehículos
|
||||
└── vehiculos_modelos/ # Modelos de vehículos
|
||||
|
||||
Uso:
|
||||
# Importar servicios específicos
|
||||
from api.v1.modules.sitar.tlcs import TLCSService
|
||||
from api.v1.modules.sitar.fracciones import FraccionesService
|
||||
|
||||
# Usar servicios
|
||||
tlcs_service = TLCSService.get_instance()
|
||||
data = await tlcs_service.search(fraccion="84716001")
|
||||
|
||||
# Importar routers para FastAPI
|
||||
from api.v1.modules.sitar.tlcs import router as tlcs_router
|
||||
from api.v1.modules.sitar.fracciones import router as fracciones_router
|
||||
|
||||
app.include_router(tlcs_router, prefix="/api/v1/sitar/tlcs", tags=["sitar-tlcs"])
|
||||
"""
|
||||
|
||||
from .common import SitarAPIBaseService
|
||||
|
||||
# Import all services for convenient access
|
||||
from .tlcs import TLCSService
|
||||
from .fracciones import FraccionesService
|
||||
from .fracciones_usa import FraccionesUSAService
|
||||
from .regulaciones import RegulacionesService
|
||||
from .prosec import ProsecService
|
||||
from .precios_estimados import PreciosEstimadosService
|
||||
from .fundamentos_tlc import FundamentosTLCService
|
||||
from .aladi2 import Aladi2Service
|
||||
from .cuotas2 import Cuotas2Service
|
||||
from .cupos import CuposService
|
||||
from .fracciones_anteriores import FraccionesAnterioresService
|
||||
from .informacion_general import InformacionGeneralService
|
||||
from .ieps import IepsService
|
||||
from .noms import NomsService
|
||||
from .rcg2 import Rcg2Service
|
||||
from .reit import ReitService
|
||||
from .requisito_previo import RequisitoPrevioService
|
||||
from .vehiculos_marcas import VehiculosMarcasService
|
||||
from .vehiculos_modelos import VehiculosModelosService
|
||||
|
||||
__all__ = [
|
||||
"SitarAPIBaseService",
|
||||
"TLCSService",
|
||||
"FraccionesService",
|
||||
"FraccionesUSAService",
|
||||
"RegulacionesService",
|
||||
"ProsecService",
|
||||
"PreciosEstimadosService",
|
||||
"FundamentosTLCService",
|
||||
"Aladi2Service",
|
||||
"Cuotas2Service",
|
||||
"CuposService",
|
||||
"FraccionesAnterioresService",
|
||||
"InformacionGeneralService",
|
||||
"IepsService",
|
||||
"NomsService",
|
||||
"Rcg2Service",
|
||||
"ReitService",
|
||||
"RequisitoPrevioService",
|
||||
"VehiculosMarcasService",
|
||||
"VehiculosModelosService",
|
||||
]
|
||||
7
backend/api/v1/modules/sitar/aladi2/__init__.py
Normal file
7
backend/api/v1/modules/sitar/aladi2/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Aladi2 Module"""
|
||||
|
||||
from .schemas import Aladi2Response
|
||||
from .service import Aladi2Service
|
||||
from .router import router
|
||||
|
||||
__all__ = ["Aladi2Response", "Aladi2Service", "router"]
|
||||
37
backend/api/v1/modules/sitar/aladi2/router.py
Normal file
37
backend/api/v1/modules/sitar/aladi2/router.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Aladi2 Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import Depends
|
||||
|
||||
from core.security import get_current_user
|
||||
from .service import Aladi2Service
|
||||
from .schemas import Aladi2Response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[Aladi2Response])
|
||||
async def search(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
pais: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = Aladi2Service.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, pais=pais, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=Aladi2Response)
|
||||
async def get_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await Aladi2Service.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
24
backend/api/v1/modules/sitar/aladi2/schemas.py
Normal file
24
backend/api/v1/modules/sitar/aladi2/schemas.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""ALADI2 Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Aladi2Response(BaseModel):
|
||||
"""ALADI2 (Asociación Latinoamericana de Integración)"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
ACUERDO: Optional[str] = Field(None, max_length=49)
|
||||
PAIS: Optional[str] = Field(None, max_length=3)
|
||||
TASATXT: Optional[str] = Field(None, max_length=19)
|
||||
TASANUM: Optional[str] = None
|
||||
TASACALCULADA: Optional[str] = Field(None, max_length=19)
|
||||
TIPOTASA: Optional[int] = None
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
NOTAS: Optional[str] = Field(None, max_length=499)
|
||||
OBSERVACIONES: Optional[str] = None
|
||||
NICO: Optional[str] = Field(None, max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
37
backend/api/v1/modules/sitar/aladi2/service.py
Normal file
37
backend/api/v1/modules/sitar/aladi2/service.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Aladi2 Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import Aladi2Response
|
||||
|
||||
|
||||
class Aladi2Service(SitarAPIBaseService):
|
||||
_instance: Optional["Aladi2Service"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "Aladi2Service":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
pais: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[Aladi2Response]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if pais:
|
||||
params["pais"] = pais
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request("GET", "/api/v1/aladi2/", params=params)
|
||||
return [Aladi2Response(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> Aladi2Response:
|
||||
data = await self._make_request("GET", f"/api/v1/aladi2/{sysid}")
|
||||
return Aladi2Response(**data)
|
||||
5
backend/api/v1/modules/sitar/common/__init__.py
Normal file
5
backend/api/v1/modules/sitar/common/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""SITAR API Common Module"""
|
||||
|
||||
from .base_service import SitarAPIBaseService
|
||||
|
||||
__all__ = ["SitarAPIBaseService"]
|
||||
104
backend/api/v1/modules/sitar/common/base_service.py
Normal file
104
backend/api/v1/modules/sitar/common/base_service.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
SITAR API Base Service
|
||||
|
||||
Base service class for SITAR API authentication and HTTP requests.
|
||||
All specific resource services inherit from this.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
import httpx
|
||||
|
||||
|
||||
class SitarAPIBaseService:
|
||||
"""Base service for SITAR API integration with authentication"""
|
||||
|
||||
_token: Optional[str] = None
|
||||
_token_expires: Optional[datetime] = None
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize base service with API credentials"""
|
||||
self.base_url = os.getenv("SITAR_API_URL")
|
||||
self.username = os.getenv("SITAR_API_USER")
|
||||
self.password = os.getenv("SITAR_API_PASSWORD")
|
||||
self.timeout = 10.0
|
||||
|
||||
if not all([self.base_url, self.username, self.password]):
|
||||
raise ValueError(
|
||||
"Missing SITAR API configuration. "
|
||||
"Set SITAR_API_URL, SITAR_API_USER, and SITAR_API_PASSWORD environment variables."
|
||||
)
|
||||
|
||||
async def _get_token(self) -> str:
|
||||
"""
|
||||
Get authentication token, refreshing if necessary
|
||||
|
||||
Returns:
|
||||
str: Bearer token for API authentication
|
||||
|
||||
Raises:
|
||||
httpx.HTTPError: If authentication fails
|
||||
"""
|
||||
# Return cached token if still valid
|
||||
if self._token and self._token_expires and datetime.now() < self._token_expires:
|
||||
return self._token
|
||||
|
||||
# Authenticate and get new token
|
||||
login_url = f"{self.base_url}/fractions/api/v1/auth/login"
|
||||
payload = {"username": self.username, "password": self.password}
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(login_url, json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
self._token = data.get("access_token") or data.get("token")
|
||||
|
||||
if not self._token:
|
||||
raise ValueError("No token received from SITAR API")
|
||||
|
||||
# Set token expiration (assume 1 hour if not specified)
|
||||
self._token_expires = datetime.now() + timedelta(hours=1)
|
||||
|
||||
return self._token
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Make authenticated request to SITAR API
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
endpoint: API endpoint path
|
||||
params: Query parameters
|
||||
json_data: JSON body data
|
||||
|
||||
Returns:
|
||||
JSON response data
|
||||
|
||||
Raises:
|
||||
httpx.HTTPError: If request fails
|
||||
"""
|
||||
token = await self._get_token()
|
||||
url = f"{self.base_url}/fractions/{endpoint}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.request(
|
||||
method=method,
|
||||
url=url,
|
||||
params=params,
|
||||
json=json_data,
|
||||
headers=headers,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
7
backend/api/v1/modules/sitar/cuotas2/__init__.py
Normal file
7
backend/api/v1/modules/sitar/cuotas2/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Cuotas2 Module"""
|
||||
|
||||
from .schemas import Cuotas2Response
|
||||
from .service import Cuotas2Service
|
||||
from .router import router
|
||||
|
||||
__all__ = ["Cuotas2Response", "Cuotas2Service", "router"]
|
||||
35
backend/api/v1/modules/sitar/cuotas2/router.py
Normal file
35
backend/api/v1/modules/sitar/cuotas2/router.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Cuotas2 Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import Cuotas2Service
|
||||
from .schemas import Cuotas2Response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[Cuotas2Response])
|
||||
async def search(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
pais: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = Cuotas2Service.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, pais=pais, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=Cuotas2Response)
|
||||
async def get_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await Cuotas2Service.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
30
backend/api/v1/modules/sitar/cuotas2/schemas.py
Normal file
30
backend/api/v1/modules/sitar/cuotas2/schemas.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Cuotas2 Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Cuotas2Response(BaseModel):
|
||||
"""Cuotas compensatorias"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
PRODUCTO: Optional[str] = Field(None, max_length=300)
|
||||
PAIS: Optional[str] = Field(None, max_length=3)
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
DOF2: Optional[str] = Field(None, max_length=149)
|
||||
EMPRESA: Optional[str] = Field(None, max_length=100)
|
||||
INCOTERM: Optional[str] = Field(None, max_length=10)
|
||||
DOLARES: Optional[str] = None
|
||||
CLAVEMONEDA: Optional[str] = Field(None, max_length=3)
|
||||
CLAVEUM: Optional[str] = Field(None, max_length=2)
|
||||
PORCENTAJE: Optional[str] = None
|
||||
DESCRIPCIONDOLARES: Optional[str] = Field(None, max_length=50)
|
||||
NOTAS: Optional[str] = None
|
||||
ESTATUS: Optional[str] = Field(None, max_length=19)
|
||||
NUMERORESOLUCION: Optional[int] = None
|
||||
RESOLUCION: Optional[str] = Field(None, max_length=150)
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
37
backend/api/v1/modules/sitar/cuotas2/service.py
Normal file
37
backend/api/v1/modules/sitar/cuotas2/service.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Cuotas2 Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import Cuotas2Response
|
||||
|
||||
|
||||
class Cuotas2Service(SitarAPIBaseService):
|
||||
_instance: Optional["Cuotas2Service"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "Cuotas2Service":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
pais: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[Cuotas2Response]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if pais:
|
||||
params["pais"] = pais
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request("GET", "/api/v1/cuotas2/", params=params)
|
||||
return [Cuotas2Response(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> Cuotas2Response:
|
||||
data = await self._make_request("GET", f"/api/v1/cuotas2/{sysid}")
|
||||
return Cuotas2Response(**data)
|
||||
7
backend/api/v1/modules/sitar/cupos/__init__.py
Normal file
7
backend/api/v1/modules/sitar/cupos/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Cupos Module"""
|
||||
|
||||
from .schemas import CuposResponse
|
||||
from .service import CuposService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["CuposResponse", "CuposService", "router"]
|
||||
34
backend/api/v1/modules/sitar/cupos/router.py
Normal file
34
backend/api/v1/modules/sitar/cupos/router.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Cupos Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import CuposService
|
||||
from .schemas import CuposResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[CuposResponse])
|
||||
async def search(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = CuposService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=CuposResponse)
|
||||
async def get_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await CuposService.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
25
backend/api/v1/modules/sitar/cupos/schemas.py
Normal file
25
backend/api/v1/modules/sitar/cupos/schemas.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Cupos Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CuposResponse(BaseModel):
|
||||
"""Cupos de importación"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
NUMEROCUPO: Optional[int] = None
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
PERMISO: Optional[str] = Field(None, max_length=2)
|
||||
CONDICION: Optional[str] = Field(None, max_length=199)
|
||||
VIGENCIA: Optional[str] = Field(None, max_length=8)
|
||||
PAIS: Optional[str] = Field(None, max_length=5)
|
||||
OBSERVACIONES: Optional[str] = Field(None, max_length=5000)
|
||||
ADVIMPO: Optional[str] = Field(None, max_length=20)
|
||||
ADVEXPO: Optional[str] = Field(None, max_length=20)
|
||||
TIPOOPERACION: Optional[int] = None
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
34
backend/api/v1/modules/sitar/cupos/service.py
Normal file
34
backend/api/v1/modules/sitar/cupos/service.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Cupos Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import CuposResponse
|
||||
|
||||
|
||||
class CuposService(SitarAPIBaseService):
|
||||
_instance: Optional["CuposService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "CuposService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[CuposResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request("GET", "/api/v1/cupos/", params=params)
|
||||
return [CuposResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> CuposResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/cupos/{sysid}")
|
||||
return CuposResponse(**data)
|
||||
7
backend/api/v1/modules/sitar/fracciones/__init__.py
Normal file
7
backend/api/v1/modules/sitar/fracciones/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Fracciones Module"""
|
||||
|
||||
from .schemas import FraccionesResponse
|
||||
from .service import FraccionesService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["FraccionesResponse", "FraccionesService", "router"]
|
||||
43
backend/api/v1/modules/sitar/fracciones/router.py
Normal file
43
backend/api/v1/modules/sitar/fracciones/router.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Fracciones Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import FraccionesService
|
||||
from .schemas import FraccionesResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FraccionesResponse])
|
||||
async def search_fracciones(
|
||||
fraccion: Optional[str] = Query(None, description="Fracción arancelaria"),
|
||||
nico: Optional[str] = Query(None, description="NICO"),
|
||||
skip: int = Query(0, ge=0, description="Registros a saltar"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Máximo de registros"),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Search Mexican tariff fractions"""
|
||||
try:
|
||||
service = FraccionesService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error fetching Fracciones data: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=FraccionesResponse)
|
||||
async def get_fraccion_by_id(
|
||||
sysid: int, current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""Get single Fraccion record by SYSID"""
|
||||
try:
|
||||
service = FraccionesService.get_instance()
|
||||
return await service.get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Fraccion record not found: {str(e)}"
|
||||
)
|
||||
38
backend/api/v1/modules/sitar/fracciones/schemas.py
Normal file
38
backend/api/v1/modules/sitar/fracciones/schemas.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Fracciones Schemas"""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FraccionesResponse(BaseModel):
|
||||
"""Fracciones arancelarias mexicanas"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
FRACCIONPUNTO: Optional[str] = Field(None, max_length=10)
|
||||
DESCRIPCION: Optional[str] = None
|
||||
UMCLAVE: Optional[str] = Field(None, max_length=2)
|
||||
UMABREVIACION: Optional[str] = Field(None, max_length=4)
|
||||
ADVIMPOTXT: Optional[str] = Field(None, max_length=19)
|
||||
ADVIMPONUM: Optional[Decimal] = None
|
||||
TIPOTASAADVIMPO: Optional[int] = None
|
||||
ADVEXPOTXT: Optional[str] = Field(None, max_length=19)
|
||||
ADVEXPONUM: Optional[Decimal] = None
|
||||
TIPOTASAADVEXPO: Optional[int] = None
|
||||
TASAIVAFRANJA: Optional[Decimal] = None
|
||||
TASAIVAINTERIOR: Optional[Decimal] = None
|
||||
TASAISAN: Optional[Decimal] = None
|
||||
ARANCELMIXTO: Optional[str] = Field(None, max_length=1)
|
||||
TASAMIXTA: Optional[Decimal] = None
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
NOTAS: Optional[str] = None
|
||||
HISTORICO: Optional[str] = None
|
||||
ARANCELESPECIFICO: Optional[str] = Field(None, max_length=1)
|
||||
TIPOVEHICULO: Optional[str] = Field(None, max_length=1)
|
||||
APLICAISAN: Optional[str] = Field(None, max_length=1)
|
||||
APLICAIEPS: Optional[str] = Field(None, max_length=1)
|
||||
NIVEL: Optional[int] = None
|
||||
NICO: Optional[str] = Field(None, max_length=14)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
40
backend/api/v1/modules/sitar/fracciones/service.py
Normal file
40
backend/api/v1/modules/sitar/fracciones/service.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Fracciones Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import FraccionesResponse
|
||||
|
||||
|
||||
class FraccionesService(SitarAPIBaseService):
|
||||
"""Service for Fracciones operations"""
|
||||
|
||||
_instance: Optional["FraccionesService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "FraccionesService":
|
||||
"""Get singleton instance"""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[FraccionesResponse]:
|
||||
"""Search Mexican tariff fractions"""
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
|
||||
data = await self._make_request("GET", "/api/v1/fracciones/", params=params)
|
||||
return [FraccionesResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> FraccionesResponse:
|
||||
"""Get single Fraccion record by SYSID"""
|
||||
data = await self._make_request("GET", f"/api/v1/fracciones/{sysid}")
|
||||
return FraccionesResponse(**data)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""FraccionesAnteriores Module"""
|
||||
|
||||
from .schemas import FraccionesAnterioresResponse
|
||||
from .service import FraccionesAnterioresService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["FraccionesAnterioresResponse", "FraccionesAnterioresService", "router"]
|
||||
37
backend/api/v1/modules/sitar/fracciones_anteriores/router.py
Normal file
37
backend/api/v1/modules/sitar/fracciones_anteriores/router.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""FraccionesAnteriores Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import FraccionesAnterioresService
|
||||
from .schemas import FraccionesAnterioresResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FraccionesAnterioresResponse])
|
||||
async def search(
|
||||
fraccion_actual: Optional[str] = Query(None),
|
||||
fraccion_anterior: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = FraccionesAnterioresService.get_instance()
|
||||
return await service.search(
|
||||
fraccion_actual=fraccion_actual,
|
||||
fraccion_anterior=fraccion_anterior,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=FraccionesAnterioresResponse)
|
||||
async def get_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await FraccionesAnterioresService.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Fracciones Anteriores Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FraccionesAnterioresResponse(BaseModel):
|
||||
"""Fracciones arancelarias anteriores (histórico)"""
|
||||
|
||||
FRACCIONACTUAL: Optional[str] = Field(None, max_length=10)
|
||||
FRACCIONANTERIOR: Optional[str] = Field(None, max_length=10)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1,36 @@
|
||||
"""FraccionesAnteriores Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import FraccionesAnterioresResponse
|
||||
|
||||
|
||||
class FraccionesAnterioresService(SitarAPIBaseService):
|
||||
_instance: Optional["FraccionesAnterioresService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "FraccionesAnterioresService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion_actual: Optional[str] = None,
|
||||
fraccion_anterior: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[FraccionesAnterioresResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion_actual:
|
||||
params["fraccion_actual"] = fraccion_actual
|
||||
if fraccion_anterior:
|
||||
params["fraccion_anterior"] = fraccion_anterior
|
||||
data = await self._make_request(
|
||||
"GET", "/api/v1/fracciones-anteriores/", params=params
|
||||
)
|
||||
return [FraccionesAnterioresResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> FraccionesAnterioresResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/fracciones-anteriores/{sysid}")
|
||||
return FraccionesAnterioresResponse(**data)
|
||||
7
backend/api/v1/modules/sitar/fracciones_usa/__init__.py
Normal file
7
backend/api/v1/modules/sitar/fracciones_usa/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Fracciones USA Module"""
|
||||
|
||||
from .schemas import FraccionesUSAResponse
|
||||
from .service import FraccionesUSAService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["FraccionesUSAResponse", "FraccionesUSAService", "router"]
|
||||
41
backend/api/v1/modules/sitar/fracciones_usa/router.py
Normal file
41
backend/api/v1/modules/sitar/fracciones_usa/router.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Fracciones USA Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import FraccionesUSAService
|
||||
from .schemas import FraccionesUSAResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FraccionesUSAResponse])
|
||||
async def search_fracciones_usa(
|
||||
fraccion: Optional[str] = Query(None, description="Fracción arancelaria USA"),
|
||||
skip: int = Query(0, ge=0, description="Registros a saltar"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="Máximo de registros"),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Search USA tariff fractions"""
|
||||
try:
|
||||
service = FraccionesUSAService.get_instance()
|
||||
return await service.search(fraccion=fraccion, skip=skip, limit=limit)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error fetching USA Fracciones data: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{consecutivo}", response_model=FraccionesUSAResponse)
|
||||
async def get_fraccion_usa_by_id(
|
||||
consecutivo: int,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Get single USA Fraccion record by CONSECUTIVO"""
|
||||
try:
|
||||
service = FraccionesUSAService.get_instance()
|
||||
return await service.get_by_id(consecutivo)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"USA Fraccion record not found: {str(e)}"
|
||||
)
|
||||
24
backend/api/v1/modules/sitar/fracciones_usa/schemas.py
Normal file
24
backend/api/v1/modules/sitar/fracciones_usa/schemas.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Fracciones USA Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FraccionesUSAResponse(BaseModel):
|
||||
"""USA tariff fractions"""
|
||||
|
||||
FRACCION_SIN_PUNTO: Optional[str] = None
|
||||
FRACCION_CON_PUNTO: Optional[str] = None
|
||||
FRACCION_MOSTRAR: Optional[str] = None
|
||||
ESPECIFICO: Optional[str] = None
|
||||
NIVEL: Optional[str] = None
|
||||
DESCRIPCION: Optional[str] = None
|
||||
UNIDADCANTIDAD: Optional[str] = None
|
||||
TARIFA1: Optional[str] = None
|
||||
TLC: Optional[str] = None
|
||||
TARIFA2: Optional[str] = None
|
||||
NOTAS: Optional[str] = None
|
||||
CONSECUTIVO: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
37
backend/api/v1/modules/sitar/fracciones_usa/service.py
Normal file
37
backend/api/v1/modules/sitar/fracciones_usa/service.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Fracciones USA Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import FraccionesUSAResponse
|
||||
|
||||
|
||||
class FraccionesUSAService(SitarAPIBaseService):
|
||||
"""Service for USA Fracciones operations"""
|
||||
|
||||
_instance: Optional["FraccionesUSAService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "FraccionesUSAService":
|
||||
"""Get singleton instance"""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[FraccionesUSAResponse]:
|
||||
"""Search USA tariff fractions"""
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
|
||||
data = await self._make_request("GET", "/api/v1/fracciones-usa/", params=params)
|
||||
return [FraccionesUSAResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, consecutivo: int) -> FraccionesUSAResponse:
|
||||
"""Get single USA Fraccion record by CONSECUTIVO"""
|
||||
data = await self._make_request("GET", f"/api/v1/fracciones-usa/{consecutivo}")
|
||||
return FraccionesUSAResponse(**data)
|
||||
7
backend/api/v1/modules/sitar/fundamentos_tlc/__init__.py
Normal file
7
backend/api/v1/modules/sitar/fundamentos_tlc/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Fundamentos TLC Module"""
|
||||
|
||||
from .schemas import FundamentosTLCResponse
|
||||
from .service import FundamentosTLCService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["FundamentosTLCResponse", "FundamentosTLCService", "router"]
|
||||
37
backend/api/v1/modules/sitar/fundamentos_tlc/router.py
Normal file
37
backend/api/v1/modules/sitar/fundamentos_tlc/router.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Fundamentos TLC Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import FundamentosTLCService
|
||||
from .schemas import FundamentosTLCResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FundamentosTLCResponse])
|
||||
async def search_fundamentos(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
tipat_only: bool = Query(False),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = FundamentosTLCService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, tipat_only=tipat_only, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=FundamentosTLCResponse)
|
||||
async def get_fundamento_by_id(
|
||||
sysid: int, current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
try:
|
||||
return await FundamentosTLCService.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
27
backend/api/v1/modules/sitar/fundamentos_tlc/schemas.py
Normal file
27
backend/api/v1/modules/sitar/fundamentos_tlc/schemas.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Fundamentos TLC Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FundamentosTLCResponse(BaseModel):
|
||||
"""Fundamentos de tratados de libre comercio"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=8)
|
||||
PAIS: Optional[str] = Field(None, max_length=3)
|
||||
ACUERDO: Optional[str] = Field(None, max_length=5)
|
||||
TASA1TXT: Optional[str] = Field(None, max_length=19)
|
||||
TASA1NUM: Optional[str] = None
|
||||
TASA2TXT: Optional[str] = Field(None, max_length=19)
|
||||
TASA2NUM: Optional[str] = None
|
||||
FUNDAMENTO1: Optional[str] = None
|
||||
FUNDAMENTO2: Optional[str] = None
|
||||
PERIODO: Optional[str] = Field(None, max_length=299)
|
||||
MODALIDAD: Optional[str] = Field(None, max_length=999)
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
NOTAS: Optional[str] = Field(None, max_length=20)
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
40
backend/api/v1/modules/sitar/fundamentos_tlc/service.py
Normal file
40
backend/api/v1/modules/sitar/fundamentos_tlc/service.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Fundamentos TLC Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import FundamentosTLCResponse
|
||||
|
||||
|
||||
class FundamentosTLCService(SitarAPIBaseService):
|
||||
_instance: Optional["FundamentosTLCService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "FundamentosTLCService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
tipat_only: bool = False,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[FundamentosTLCResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
endpoint = (
|
||||
"/api/v1/fundamentos-tlc/tipat"
|
||||
if tipat_only
|
||||
else "/api/v1/fundamentos-tlc/"
|
||||
)
|
||||
data = await self._make_request("GET", endpoint, params=params)
|
||||
return [FundamentosTLCResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> FundamentosTLCResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/fundamentos-tlc/{sysid}")
|
||||
return FundamentosTLCResponse(**data)
|
||||
7
backend/api/v1/modules/sitar/ieps/__init__.py
Normal file
7
backend/api/v1/modules/sitar/ieps/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Ieps Module"""
|
||||
|
||||
from .schemas import IepsResponse
|
||||
from .service import IepsService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["IepsResponse", "IepsService", "router"]
|
||||
34
backend/api/v1/modules/sitar/ieps/router.py
Normal file
34
backend/api/v1/modules/sitar/ieps/router.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Ieps Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import IepsService
|
||||
from .schemas import IepsResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[IepsResponse])
|
||||
async def search(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = IepsService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{consecutivo}", response_model=IepsResponse)
|
||||
async def get_by_id(consecutivo: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await IepsService.get_instance().get_by_id(consecutivo)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
20
backend/api/v1/modules/sitar/ieps/schemas.py
Normal file
20
backend/api/v1/modules/sitar/ieps/schemas.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""IEPS Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class IepsResponse(BaseModel):
|
||||
"""IEPS (Impuesto Especial sobre Producción y Servicios)"""
|
||||
|
||||
FRACCION_SIN_PUNTO: Optional[str] = Field(None, max_length=10)
|
||||
FRACCION_CON_PUNTO: Optional[str] = Field(None, max_length=10)
|
||||
FUNDAMENTO: Optional[str] = Field(None, max_length=1000)
|
||||
CONDICION: Optional[str] = Field(None, max_length=5000)
|
||||
TASA: Optional[str] = None
|
||||
TASAESPECIFICO: Optional[str] = None
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
CONSECUTIVO: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
34
backend/api/v1/modules/sitar/ieps/service.py
Normal file
34
backend/api/v1/modules/sitar/ieps/service.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Ieps Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import IepsResponse
|
||||
|
||||
|
||||
class IepsService(SitarAPIBaseService):
|
||||
_instance: Optional["IepsService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "IepsService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[IepsResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request("GET", "/api/v1/ieps/", params=params)
|
||||
return [IepsResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, consecutivo: int) -> IepsResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/ieps/{consecutivo}")
|
||||
return IepsResponse(**data)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""InformacionGeneral Module"""
|
||||
|
||||
from .schemas import InformacionGeneralResponse
|
||||
from .service import InformacionGeneralService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["InformacionGeneralResponse", "InformacionGeneralService", "router"]
|
||||
34
backend/api/v1/modules/sitar/informacion_general/router.py
Normal file
34
backend/api/v1/modules/sitar/informacion_general/router.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""InformacionGeneral Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import InformacionGeneralService
|
||||
from .schemas import InformacionGeneralResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[InformacionGeneralResponse])
|
||||
async def search(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = InformacionGeneralService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=InformacionGeneralResponse)
|
||||
async def get_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await InformacionGeneralService.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
21
backend/api/v1/modules/sitar/informacion_general/schemas.py
Normal file
21
backend/api/v1/modules/sitar/informacion_general/schemas.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Información General Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class InformacionGeneralResponse(BaseModel):
|
||||
"""Información general de fracciones"""
|
||||
|
||||
OBSERVACIONES: Optional[str] = Field(None, max_length=1000)
|
||||
NOTAOBSERVACIONES: Optional[str] = Field(None, max_length=1000)
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
TIPOOPERACION: Optional[int] = None
|
||||
ORDEN: Optional[int] = None
|
||||
LEYENDA: Optional[str] = None
|
||||
NOTA: Optional[str] = None
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
36
backend/api/v1/modules/sitar/informacion_general/service.py
Normal file
36
backend/api/v1/modules/sitar/informacion_general/service.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""InformacionGeneral Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import InformacionGeneralResponse
|
||||
|
||||
|
||||
class InformacionGeneralService(SitarAPIBaseService):
|
||||
_instance: Optional["InformacionGeneralService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "InformacionGeneralService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[InformacionGeneralResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request(
|
||||
"GET", "/api/v1/informacion-general/", params=params
|
||||
)
|
||||
return [InformacionGeneralResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> InformacionGeneralResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/informacion-general/{sysid}")
|
||||
return InformacionGeneralResponse(**data)
|
||||
7
backend/api/v1/modules/sitar/noms/__init__.py
Normal file
7
backend/api/v1/modules/sitar/noms/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Noms Module"""
|
||||
|
||||
from .schemas import NomsResponse
|
||||
from .service import NomsService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["NomsResponse", "NomsService", "router"]
|
||||
35
backend/api/v1/modules/sitar/noms/router.py
Normal file
35
backend/api/v1/modules/sitar/noms/router.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Noms Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import NomsService
|
||||
from .schemas import NomsResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[NomsResponse])
|
||||
async def search(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
pais: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = NomsService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, pais=pais, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=NomsResponse)
|
||||
async def get_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await NomsService.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
27
backend/api/v1/modules/sitar/noms/schemas.py
Normal file
27
backend/api/v1/modules/sitar/noms/schemas.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""NOMs Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class NomsResponse(BaseModel):
|
||||
"""Normas Oficiales Mexicanas"""
|
||||
|
||||
FRACCION: Optional[str] = Field("", max_length=10)
|
||||
IMPORTACION: Optional[str] = Field("", max_length=1)
|
||||
EXPORTACION: Optional[str] = Field("", max_length=1)
|
||||
PERMISO: Optional[str] = Field("", max_length=19)
|
||||
ACUERDO: Optional[str] = Field("", max_length=100)
|
||||
CONDICION: Optional[str] = Field("")
|
||||
FUNDAMENTO: Optional[str] = Field("")
|
||||
DOF: Optional[str] = Field("", max_length=8)
|
||||
FORMATONOM: Optional[str] = Field("", max_length=100)
|
||||
INSTRUCCIONES: Optional[str] = Field("", max_length=100)
|
||||
CRITERIO: Optional[str] = Field("", max_length=254)
|
||||
COMPLEMENTO: Optional[str] = Field("", max_length=999)
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
PAIS: Optional[str] = Field("", max_length=3)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
37
backend/api/v1/modules/sitar/noms/service.py
Normal file
37
backend/api/v1/modules/sitar/noms/service.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Noms Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import NomsResponse
|
||||
|
||||
|
||||
class NomsService(SitarAPIBaseService):
|
||||
_instance: Optional["NomsService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "NomsService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
pais: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[NomsResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if pais:
|
||||
params["pais"] = pais
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request("GET", "/api/v1/noms/", params=params)
|
||||
return [NomsResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> NomsResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/noms/{sysid}")
|
||||
return NomsResponse(**data)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Precios Estimados Module"""
|
||||
|
||||
from .schemas import PreciosEstimados2Response
|
||||
from .service import PreciosEstimadosService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["PreciosEstimados2Response", "PreciosEstimadosService", "router"]
|
||||
34
backend/api/v1/modules/sitar/precios_estimados/router.py
Normal file
34
backend/api/v1/modules/sitar/precios_estimados/router.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Precios Estimados Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import PreciosEstimadosService
|
||||
from .schemas import PreciosEstimados2Response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[PreciosEstimados2Response])
|
||||
async def search_precios(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = PreciosEstimadosService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=PreciosEstimados2Response)
|
||||
async def get_precio_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await PreciosEstimadosService.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
18
backend/api/v1/modules/sitar/precios_estimados/schemas.py
Normal file
18
backend/api/v1/modules/sitar/precios_estimados/schemas.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Precios Estimados Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PreciosEstimados2Response(BaseModel):
|
||||
"""Precios Estimados"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
UMC: Optional[str] = Field(None, max_length=2)
|
||||
PRECIO: Optional[str] = None
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
36
backend/api/v1/modules/sitar/precios_estimados/service.py
Normal file
36
backend/api/v1/modules/sitar/precios_estimados/service.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Precios Estimados Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import PreciosEstimados2Response
|
||||
|
||||
|
||||
class PreciosEstimadosService(SitarAPIBaseService):
|
||||
_instance: Optional["PreciosEstimadosService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "PreciosEstimadosService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[PreciosEstimados2Response]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request(
|
||||
"GET", "/api/v1/precios-estimados2/", params=params
|
||||
)
|
||||
return [PreciosEstimados2Response(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> PreciosEstimados2Response:
|
||||
data = await self._make_request("GET", f"/api/v1/precios-estimados2/{sysid}")
|
||||
return PreciosEstimados2Response(**data)
|
||||
7
backend/api/v1/modules/sitar/prosec/__init__.py
Normal file
7
backend/api/v1/modules/sitar/prosec/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""PROSEC Module"""
|
||||
|
||||
from .schemas import ProsecResponse
|
||||
from .service import ProsecService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["ProsecResponse", "ProsecService", "router"]
|
||||
42
backend/api/v1/modules/sitar/prosec/router.py
Normal file
42
backend/api/v1/modules/sitar/prosec/router.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""PROSEC Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import ProsecService
|
||||
from .schemas import ProsecResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ProsecResponse])
|
||||
async def search_prosec(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
sector: Optional[str] = Query(None),
|
||||
articulo: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = ProsecService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion,
|
||||
nico=nico,
|
||||
sector=sector,
|
||||
articulo=articulo,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=ProsecResponse)
|
||||
async def get_prosec_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
service = ProsecService.get_instance()
|
||||
return await service.get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=f"Not found: {str(e)}")
|
||||
21
backend/api/v1/modules/sitar/prosec/schemas.py
Normal file
21
backend/api/v1/modules/sitar/prosec/schemas.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""PROSEC Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProsecResponse(BaseModel):
|
||||
"""PROSEC (Programa de Promoción Sectorial)"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
PRODUCTO: Optional[str] = Field(None, max_length=999)
|
||||
TASA: Optional[str] = Field(None, max_length=19)
|
||||
SECTOR: Optional[str] = Field(None, max_length=2)
|
||||
ANEXO: Optional[str] = Field(None, max_length=19)
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
NOTAS: Optional[str] = Field(None, max_length=5000)
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
43
backend/api/v1/modules/sitar/prosec/service.py
Normal file
43
backend/api/v1/modules/sitar/prosec/service.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""PROSEC Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import ProsecResponse
|
||||
|
||||
|
||||
class ProsecService(SitarAPIBaseService):
|
||||
"""Service for PROSEC operations"""
|
||||
|
||||
_instance: Optional["ProsecService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "ProsecService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
sector: Optional[str] = None,
|
||||
articulo: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[ProsecResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
if sector:
|
||||
params["sector"] = sector
|
||||
if articulo:
|
||||
params["articulo"] = articulo
|
||||
|
||||
data = await self._make_request("GET", "/api/v1/prosec/", params=params)
|
||||
return [ProsecResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> ProsecResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/prosec/{sysid}")
|
||||
return ProsecResponse(**data)
|
||||
7
backend/api/v1/modules/sitar/rcg2/__init__.py
Normal file
7
backend/api/v1/modules/sitar/rcg2/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Rcg2 Module"""
|
||||
|
||||
from .schemas import Rcg2Response
|
||||
from .service import Rcg2Service
|
||||
from .router import router
|
||||
|
||||
__all__ = ["Rcg2Response", "Rcg2Service", "router"]
|
||||
34
backend/api/v1/modules/sitar/rcg2/router.py
Normal file
34
backend/api/v1/modules/sitar/rcg2/router.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Rcg2 Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import Rcg2Service
|
||||
from .schemas import Rcg2Response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[Rcg2Response])
|
||||
async def search(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = Rcg2Service.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=Rcg2Response)
|
||||
async def get_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await Rcg2Service.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
26
backend/api/v1/modules/sitar/rcg2/schemas.py
Normal file
26
backend/api/v1/modules/sitar/rcg2/schemas.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""RCG2 Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Rcg2Response(BaseModel):
|
||||
"""Reglas de Carácter General"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
ANEXO: Optional[str] = Field(None, max_length=19)
|
||||
SECTOR: Optional[str] = None
|
||||
REGIMEN: Optional[str] = None
|
||||
CONDICION: Optional[str] = None
|
||||
ADUANAS: Optional[str] = Field(None, max_length=1000)
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
ARCHIVOCRITERIO: Optional[str] = Field(None, max_length=1000)
|
||||
TIPO: Optional[str] = Field(None, max_length=1)
|
||||
IMPO: Optional[int] = None
|
||||
EXPO: Optional[int] = None
|
||||
WEB_DOCUMENTO_ID: Optional[int] = None
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
34
backend/api/v1/modules/sitar/rcg2/service.py
Normal file
34
backend/api/v1/modules/sitar/rcg2/service.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Rcg2 Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import Rcg2Response
|
||||
|
||||
|
||||
class Rcg2Service(SitarAPIBaseService):
|
||||
_instance: Optional["Rcg2Service"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "Rcg2Service":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[Rcg2Response]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request("GET", "/api/v1/rcg2/", params=params)
|
||||
return [Rcg2Response(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> Rcg2Response:
|
||||
data = await self._make_request("GET", f"/api/v1/rcg2/{sysid}")
|
||||
return Rcg2Response(**data)
|
||||
7
backend/api/v1/modules/sitar/regulaciones/__init__.py
Normal file
7
backend/api/v1/modules/sitar/regulaciones/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Regulaciones Module"""
|
||||
|
||||
from .schemas import RegulacionesResponse
|
||||
from .service import RegulacionesService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["RegulacionesResponse", "RegulacionesService", "router"]
|
||||
37
backend/api/v1/modules/sitar/regulaciones/router.py
Normal file
37
backend/api/v1/modules/sitar/regulaciones/router.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Regulaciones Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import RegulacionesService
|
||||
from .schemas import RegulacionesResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[RegulacionesResponse])
|
||||
async def search_regulaciones(
|
||||
fraccion: Optional[str] = Query(None, description="Fracción arancelaria"),
|
||||
nico: Optional[str] = Query(None, description="NICO"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = RegulacionesService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=RegulacionesResponse)
|
||||
async def get_regulacion_by_id(
|
||||
sysid: int, current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
try:
|
||||
service = RegulacionesService.get_instance()
|
||||
return await service.get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=f"Not found: {str(e)}")
|
||||
19
backend/api/v1/modules/sitar/regulaciones/schemas.py
Normal file
19
backend/api/v1/modules/sitar/regulaciones/schemas.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Regulaciones Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class RegulacionesResponse(BaseModel):
|
||||
"""Regulaciones y restricciones"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
PERMISO: Optional[str] = Field(None, max_length=2)
|
||||
ACUERDO: Optional[str] = Field(None, max_length=15)
|
||||
CLAVE: Optional[str] = Field(None, max_length=10)
|
||||
DESCRIPCION: Optional[str] = None
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
37
backend/api/v1/modules/sitar/regulaciones/service.py
Normal file
37
backend/api/v1/modules/sitar/regulaciones/service.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Regulaciones Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import RegulacionesResponse
|
||||
|
||||
|
||||
class RegulacionesService(SitarAPIBaseService):
|
||||
"""Service for Regulaciones operations"""
|
||||
|
||||
_instance: Optional["RegulacionesService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "RegulacionesService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[RegulacionesResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
|
||||
data = await self._make_request("GET", "/api/v1/regulaciones/", params=params)
|
||||
return [RegulacionesResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> RegulacionesResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/regulaciones/{sysid}")
|
||||
return RegulacionesResponse(**data)
|
||||
7
backend/api/v1/modules/sitar/reit/__init__.py
Normal file
7
backend/api/v1/modules/sitar/reit/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Reit Module"""
|
||||
|
||||
from .schemas import ReitResponse
|
||||
from .service import ReitService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["ReitResponse", "ReitService", "router"]
|
||||
34
backend/api/v1/modules/sitar/reit/router.py
Normal file
34
backend/api/v1/modules/sitar/reit/router.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Reit Router"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from core.security import get_current_user
|
||||
from .service import ReitService
|
||||
from .schemas import ReitResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ReitResponse])
|
||||
async def search(
|
||||
fraccion: Optional[str] = Query(None),
|
||||
nico: Optional[str] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
try:
|
||||
service = ReitService.get_instance()
|
||||
return await service.search(
|
||||
fraccion=fraccion, nico=nico, skip=skip, limit=limit
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{sysid}", response_model=ReitResponse)
|
||||
async def get_by_id(sysid: int, current_user: dict = Depends(get_current_user)):
|
||||
try:
|
||||
return await ReitService.get_instance().get_by_id(sysid)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
25
backend/api/v1/modules/sitar/reit/schemas.py
Normal file
25
backend/api/v1/modules/sitar/reit/schemas.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""REIT Schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ReitResponse(BaseModel):
|
||||
"""Registro de Empresas de Industria Terminal"""
|
||||
|
||||
FRACCION: Optional[str] = Field(None, max_length=10)
|
||||
ARTICULO: Optional[str] = Field(None, max_length=50)
|
||||
FUNDAMENTO: Optional[str] = Field(None, max_length=1500)
|
||||
ACUERDO: Optional[str] = Field(None, max_length=500)
|
||||
PERMISO: Optional[str] = Field(None, max_length=2)
|
||||
DOF: Optional[str] = Field(None, max_length=8)
|
||||
DOCUMENTO: Optional[str] = Field(None, max_length=200)
|
||||
TEMPORALIDAD: Optional[int] = None
|
||||
TEMPORALIDADSERVICIO: Optional[int] = None
|
||||
TEMPORALIDADCERTIFICADA: Optional[int] = None
|
||||
WEB_DOCUMENTO_ID: Optional[int] = None
|
||||
NICO: Optional[str] = Field("", max_length=2)
|
||||
SYSID: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
34
backend/api/v1/modules/sitar/reit/service.py
Normal file
34
backend/api/v1/modules/sitar/reit/service.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Reit Service"""
|
||||
|
||||
from typing import Optional, List
|
||||
from ..common import SitarAPIBaseService
|
||||
from .schemas import ReitResponse
|
||||
|
||||
|
||||
class ReitService(SitarAPIBaseService):
|
||||
_instance: Optional["ReitService"] = None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "ReitService":
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
async def search(
|
||||
self,
|
||||
fraccion: Optional[str] = None,
|
||||
nico: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> List[ReitResponse]:
|
||||
params = {"skip": skip, "limit": min(limit, 1000)}
|
||||
if fraccion:
|
||||
params["fraccion"] = fraccion
|
||||
if nico:
|
||||
params["nico"] = nico
|
||||
data = await self._make_request("GET", "/api/v1/reit/", params=params)
|
||||
return [ReitResponse(**item) for item in data]
|
||||
|
||||
async def get_by_id(self, sysid: int) -> ReitResponse:
|
||||
data = await self._make_request("GET", f"/api/v1/reit/{sysid}")
|
||||
return ReitResponse(**data)
|
||||
@@ -0,0 +1,7 @@
|
||||
"""RequisitoPrevio Module"""
|
||||
|
||||
from .schemas import RequisitoPrevioResponse
|
||||
from .service import RequisitoPrevioService
|
||||
from .router import router
|
||||
|
||||
__all__ = ["RequisitoPrevioResponse", "RequisitoPrevioService", "router"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user