diff --git a/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/dto.py b/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/dto.py
index f43e471a..1a8f15c2 100644
--- a/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/dto.py
+++ b/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/dto.py
@@ -38,11 +38,9 @@ class TariffFractionUpdateDTO(BaseModel):
class TariffFractionResponseDTO(BaseModel):
- """DTO para respuesta de fracción arancelaria"""
+ """DTO para respuesta de fracción arancelaria (catálogo global)"""
id: int
- tenant_id: int
- company_id: int
code: str
fraction: str
description: Optional[str] = None
@@ -50,8 +48,6 @@ class TariffFractionResponseDTO(BaseModel):
umt: Optional[str] = None
adv_impo: Optional[str] = None
adv_expo: Optional[str] = None
- created_at: datetime
- updated_at: datetime
model_config = ConfigDict(from_attributes=True)
diff --git a/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/models.py b/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/models.py
index 717706ff..71256e0e 100644
--- a/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/models.py
+++ b/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/models.py
@@ -4,15 +4,15 @@ Modelos ORM para fracciones arancelarias (SITAR-SCAII)
from typing import Optional
-from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import Integer, PrimaryKeyConstraint, String, Numeric
from sqlalchemy.orm import Mapped, mapped_column
-class TariffFraction(Base, TenantScopedMixin, TimestampMixin):
+class TariffFraction(Base):
"""
Modelo para fracciones arancelarias mexicanas (SITAR-SCAII)
+ Catálogo de referencia global (no tenant-scoped)
Corresponde a la tabla sFracciones
"""
@@ -22,10 +22,10 @@ class TariffFraction(Base, TenantScopedMixin, TimestampMixin):
{"schema": "a76", "extend_existing": True},
)
- id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
# Código completo de la fracción (ej: 01012101)
- code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
+ code: Mapped[str] = mapped_column(String(10), unique=True, index=True, nullable=False)
# Fracción formateada (ej: 0101.21.01)
fraction: Mapped[str] = mapped_column(String(15), index=True)
diff --git a/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/routes.py
index db4809e3..4e7bd2f3 100644
--- a/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/routes.py
+++ b/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/routes.py
@@ -1,5 +1,6 @@
"""
Endpoints API para fracciones arancelarias
+Catálogo de referencia global (no tenant-scoped)
"""
from typing import Any, Dict, Optional
@@ -8,7 +9,6 @@ from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user
-from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
from .dto import (
TariffFractionCreateDTO,
@@ -17,22 +17,6 @@ from .dto import (
)
from .service import TariffFractionService
-# Create base router with generic CRUD routes (disabled list because we'll create a custom one)
-base_router = TenantCRUDRoutes(
- service=TariffFractionService,
- create_schema=TariffFractionCreateDTO,
- update_schema=TariffFractionUpdateDTO,
- response_schema=TariffFractionResponseDTO,
- prefix="/tariff-fractions",
- tags=["a76 / general catalogs / tariff fractions"],
- resource_name="TariffFraction",
- id_name="tariff_fraction_id",
- enable_list=False, # Disable default list, we'll add custom one
- enable_filters=False,
- default_page_size=50,
- max_page_size=10000,
-)
-
router = APIRouter(prefix="/tariff-fractions", tags=["a76 / general catalogs / tariff fractions"])
# Custom list endpoint with search filter
@@ -40,25 +24,22 @@ router = APIRouter(prefix="/tariff-fractions", tags=["a76 / general catalogs / t
"/",
response_model=Dict[str, Any],
summary="List Tariff Fractions",
- description="Get paginated list of Tariff Fractions with optional search filter",
+ description="Get paginated list of Tariff Fractions with optional search filter (global catalog)",
)
async def list_tariff_fractions(
- company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=10000, description="Page size"),
search: Optional[str] = Query(None, description="Search in code, fraction, description, nico, or umt"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
- tenant_id = validate_access_to_resource(db, company_id, current_user)
-
skip = (page - 1) * page_size
filters = {}
if search:
filters["search"] = search
items, total = TariffFractionService.get_all(
- db, tenant_id, company_id, skip, page_size, filters
+ db, skip, page_size, filters
)
return {
@@ -69,6 +50,73 @@ async def list_tariff_fractions(
"pages": (total + page_size - 1) // page_size,
}
-# Include other CRUD routes from base router
-router.include_router(base_router.router)
+
+@router.get(
+ "/{tariff_fraction_id}",
+ response_model=TariffFractionResponseDTO,
+ summary="Get Tariff Fraction by ID",
+ description="Get a specific tariff fraction by ID",
+)
+async def get_tariff_fraction(
+ tariff_fraction_id: int,
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ item = TariffFractionService.get_by_id(db, tariff_fraction_id)
+ if not item:
+ from fastapi import HTTPException
+ raise HTTPException(status_code=404, detail="Tariff fraction not found")
+ return TariffFractionResponseDTO.model_validate(item)
+
+
+@router.post(
+ "/",
+ response_model=TariffFractionResponseDTO,
+ summary="Create Tariff Fraction",
+ description="Create a new tariff fraction (admin only)",
+ status_code=201,
+)
+async def create_tariff_fraction(
+ data: TariffFractionCreateDTO,
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ item = TariffFractionService.create(db, data)
+ return TariffFractionResponseDTO.model_validate(item)
+
+
+@router.put(
+ "/{tariff_fraction_id}",
+ response_model=TariffFractionResponseDTO,
+ summary="Update Tariff Fraction",
+ description="Update an existing tariff fraction (admin only)",
+)
+async def update_tariff_fraction(
+ tariff_fraction_id: int,
+ data: TariffFractionUpdateDTO,
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ item = TariffFractionService.update(db, tariff_fraction_id, data)
+ if not item:
+ from fastapi import HTTPException
+ raise HTTPException(status_code=404, detail="Tariff fraction not found")
+ return TariffFractionResponseDTO.model_validate(item)
+
+
+@router.delete(
+ "/{tariff_fraction_id}",
+ summary="Delete Tariff Fraction",
+ description="Delete a tariff fraction (admin only)",
+ status_code=204,
+)
+async def delete_tariff_fraction(
+ tariff_fraction_id: int,
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ success = TariffFractionService.delete(db, tariff_fraction_id)
+ if not success:
+ from fastapi import HTTPException
+ raise HTTPException(status_code=404, detail="Tariff fraction not found")
diff --git a/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/service.py
index 56162ae4..3dd13594 100644
--- a/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/service.py
+++ b/backend/api/v1/modules/a76/general_catalogs/tariff_fractions/service.py
@@ -1,5 +1,6 @@
"""
Service para fracciones arancelarias
+Catálogo de referencia global (no tenant-scoped)
"""
from typing import List, Optional, Tuple, Dict, Any
@@ -15,23 +16,18 @@ logger = logging.getLogger(__name__)
class TariffFractionService:
- """Service para gestionar fracciones arancelarias"""
+ """Service para gestionar fracciones arancelarias (catálogo global)"""
@staticmethod
def get_all(
db: Session,
- tenant_id: int,
- company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[TariffFraction], int]:
"""Obtiene todas las fracciones arancelarias con filtros opcionales"""
- query = db.query(TariffFraction).filter(
- TariffFraction.tenant_id == tenant_id,
- TariffFraction.company_id == company_id,
- )
+ query = db.query(TariffFraction)
# Aplicar filtros
if filters:
@@ -67,18 +63,12 @@ class TariffFractionService:
def get_by_id(
db: Session,
tariff_fraction_id: int,
- tenant_id: int,
- company_id: int,
) -> Optional[TariffFraction]:
"""Obtiene una fracción arancelaria por ID"""
return (
db.query(TariffFraction)
- .filter(
- TariffFraction.id == tariff_fraction_id,
- TariffFraction.tenant_id == tenant_id,
- TariffFraction.company_id == company_id,
- )
+ .filter(TariffFraction.id == tariff_fraction_id)
.first()
)
@@ -86,18 +76,12 @@ class TariffFractionService:
def get_by_code(
db: Session,
code: str,
- tenant_id: int,
- company_id: int,
) -> Optional[TariffFraction]:
"""Obtiene una fracción arancelaria por código"""
return (
db.query(TariffFraction)
- .filter(
- TariffFraction.code == code,
- TariffFraction.tenant_id == tenant_id,
- TariffFraction.company_id == company_id,
- )
+ .filter(TariffFraction.code == code)
.first()
)
@@ -105,16 +89,12 @@ class TariffFractionService:
def create(
db: Session,
tariff_fraction_data: TariffFractionCreateDTO,
- tenant_id: int,
- company_id: int,
) -> TariffFraction:
"""Crea una nueva fracción arancelaria"""
try:
tariff_fraction = TariffFraction(
**tariff_fraction_data.model_dump(),
- tenant_id=tenant_id,
- company_id=company_id,
)
db.add(tariff_fraction)
db.commit()
@@ -133,13 +113,11 @@ class TariffFractionService:
db: Session,
tariff_fraction_id: int,
tariff_fraction_data: TariffFractionUpdateDTO,
- tenant_id: int,
- company_id: int,
) -> Optional[TariffFraction]:
"""Actualiza una fracción arancelaria existente"""
tariff_fraction = TariffFractionService.get_by_id(
- db, tariff_fraction_id, tenant_id, company_id
+ db, tariff_fraction_id
)
if not tariff_fraction:
@@ -165,13 +143,11 @@ class TariffFractionService:
def delete(
db: Session,
tariff_fraction_id: int,
- tenant_id: int,
- company_id: int,
) -> bool:
"""Elimina una fracción arancelaria"""
tariff_fraction = TariffFractionService.get_by_id(
- db, tariff_fraction_id, tenant_id, company_id
+ db, tariff_fraction_id
)
if not tariff_fraction:
diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py
index 1bc8f8a0..67da3db4 100644
--- a/backend/api/v1/modules/a76/invoices/models.py
+++ b/backend/api/v1/modules/a76/invoices/models.py
@@ -674,6 +674,32 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
Boolean, default=False
) # SETRATAPROCESOCTM / Se trata de proceso CTM
+ # Continuation Tab Fields
+ equipment_reviewed: Mapped[Optional[bool]] = mapped_column(
+ Boolean, default=False
+ ) # Fue revisado el equipo
+ is_subdivision: Mapped[Optional[bool]] = mapped_column(
+ Boolean, default=False
+ ) # Sub división
+ acts_as_cd: Mapped[Optional[bool]] = mapped_column(
+ Boolean, default=False
+ ) # Funge como CD
+ pedimento_arrived: Mapped[Optional[bool]] = mapped_column(
+ Boolean, default=False
+ ) # Llegó el pedimento
+ green_light_mx: Mapped[Optional[bool]] = mapped_column(
+ Boolean, default=False
+ ) # Semáforo verde México
+ green_light_us: Mapped[Optional[bool]] = mapped_column(
+ Boolean, default=False
+ ) # Semáforo verde USA
+ red_light_mx: Mapped[Optional[bool]] = mapped_column(
+ Boolean, default=False
+ ) # Semáforo rojo México
+ red_light_us: Mapped[Optional[bool]] = mapped_column(
+ Boolean, default=False
+ ) # Semáforo rojo USA
+
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="logistics")
diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py
index 444f7a61..af111dc8 100644
--- a/backend/api/v1/modules/a76/invoices/schemas.py
+++ b/backend/api/v1/modules/a76/invoices/schemas.py
@@ -345,6 +345,15 @@ class InvoiceLogisticsBase(BaseModel):
None, max_length=20, description="Payment receipt number"
)
is_ctm_process: Optional[bool] = Field(False, description="Is CTM process")
+ # Continuation Tab Fields
+ equipment_reviewed: Optional[bool] = Field(False, description="Equipment reviewed")
+ is_subdivision: Optional[bool] = Field(False, description="Is subdivision")
+ acts_as_cd: Optional[bool] = Field(False, description="Acts as CD")
+ pedimento_arrived: Optional[bool] = Field(False, description="Pedimento arrived")
+ green_light_mx: Optional[bool] = Field(False, description="Green light Mexico")
+ green_light_us: Optional[bool] = Field(False, description="Green light USA")
+ red_light_mx: Optional[bool] = Field(False, description="Red light Mexico")
+ red_light_us: Optional[bool] = Field(False, description="Red light USA")
class InvoiceSalesDetailsBase(BaseModel):
diff --git a/backend/api/v1/modules/a76/items/line_descriptions/models.py b/backend/api/v1/modules/a76/items/line_descriptions/models.py
index beeb9392..39e09560 100644
--- a/backend/api/v1/modules/a76/items/line_descriptions/models.py
+++ b/backend/api/v1/modules/a76/items/line_descriptions/models.py
@@ -1,5 +1,5 @@
from typing import Optional, TYPE_CHECKING
-from sqlalchemy import Boolean, String, Text, ForeignKey
+from sqlalchemy import Boolean, String, Text, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
@@ -39,5 +39,13 @@ class LineDescription(Base):
lot: Mapped[Optional[str]] = mapped_column(String(254)) # LOTE
entry_number: Mapped[Optional[str]] = mapped_column(String(50)) # NUMENTRADA/NUMERODEENTRADA
+ # Eighth rule and A31 fields
+ eighth_rule_fraction: Mapped[Optional[str]] = mapped_column(String(20)) # Eighth Rule Fraction
+ eighth_rule_line: Mapped[Optional[int]] = mapped_column(Integer) # Eighth Rule Line
+ consider_a31: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # Consider in A31
+
+ # Machinery location
+ machinery_location: Mapped[Optional[str]] = mapped_column(String(200)) # Machinery and equipment location
+
# Relationship (one-to-one)
line: Mapped["LineItem"] = relationship(back_populates="description")
\ No newline at end of file
diff --git a/backend/api/v1/modules/a76/items/line_descriptions/schemas.py b/backend/api/v1/modules/a76/items/line_descriptions/schemas.py
index 2f0d1317..56965c36 100644
--- a/backend/api/v1/modules/a76/items/line_descriptions/schemas.py
+++ b/backend/api/v1/modules/a76/items/line_descriptions/schemas.py
@@ -26,6 +26,14 @@ class LineDescriptionBase(BaseModel):
# Lot and entry tracking
lot: Optional[str] = Field(None, max_length=254, description="Lot (LOTE)")
entry_number: Optional[str] = Field(None, max_length=50, description="Entry number (NUMENTRADA/NUMERODEENTRADA)")
+
+ # Eighth rule and A31 fields
+ eighth_rule_fraction: Optional[str] = Field(None, max_length=20, description="Eighth Rule Fraction")
+ eighth_rule_line: Optional[int] = Field(None, description="Eighth Rule Line")
+ consider_a31: Optional[bool] = Field(False, description="Consider in A31")
+
+ # Machinery location
+ machinery_location: Optional[str] = Field(None, max_length=200, description="Machinery and equipment location")
class LineDescriptionCreate(LineDescriptionBase):
diff --git a/backend/api/v1/modules/a76/items/line_items/models.py b/backend/api/v1/modules/a76/items/line_items/models.py
index ad85b357..03100e4b 100644
--- a/backend/api/v1/modules/a76/items/line_items/models.py
+++ b/backend/api/v1/modules/a76/items/line_items/models.py
@@ -41,15 +41,15 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
component_part_number_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")
) # NUMPARTECOM
- class_id: Mapped[Optional[str]] = mapped_column(
+ class_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("a76.classes.id")
) # CLASE
# Unit of measure
- unit_of_measure: Mapped[Optional[str]] = mapped_column(
+ unit_of_measure: Mapped[Optional[int]] = mapped_column(
ForeignKey("a76.units_of_measure.id")
) # UNIDADMEDIDA/UNIMED
- alternate_unit: Mapped[Optional[str]] = mapped_column(
+ alternate_unit: Mapped[Optional[int]] = mapped_column(
ForeignKey("a76.units_of_measure.id")
) # UNIMEDALTERNA
uma_key: Mapped[Optional[str]] = mapped_column(String(2)) # CLAVEUMA
@@ -173,11 +173,15 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
back_populates="line", cascade="all, delete-orphan", uselist=False
)
class_info: Mapped[Optional["Class"]] = relationship(
- foreign_keys=[class_id], viewonly=True
+ "api.v1.modules.a76.classes.models.Class",
+ foreign_keys=[class_id],
+ viewonly=True
)
unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
- foreign_keys=[unit_of_measure], viewonly=True
+ "api.v1.modules.a76.general_catalogs.units_of_measure.models.UnitOfMeasure",
+ foreign_keys=[unit_of_measure],
+ viewonly=True
)
fa_data: Mapped[Optional["FaLineItem"]] = relationship(
- "FaLineItem", back_populates="master_info", uselist=False
+ "FaLineItem", back_populates="master_info", uselist=False, cascade="all, delete"
)
diff --git a/backend/api/v1/modules/a76/items/line_items/schemas.py b/backend/api/v1/modules/a76/items/line_items/schemas.py
index 1bf42d4e..0bea3647 100644
--- a/backend/api/v1/modules/a76/items/line_items/schemas.py
+++ b/backend/api/v1/modules/a76/items/line_items/schemas.py
@@ -53,24 +53,12 @@ class LineItemBase(BaseModel):
)
class_id: Optional[int] = Field(None, description="Class code")
- @field_validator(
- "unit_of_measure",
- "alternate_unit",
- mode="before",
- )
- @classmethod
- def convert_to_string(cls, v):
- """Convert integers to strings for FK fields"""
- if v is not None and not isinstance(v, str):
- return str(v)
- return v
-
# Unit of measure
- unit_of_measure: Optional[str] = Field(
- None, max_length=10, description="Unit of measure"
+ unit_of_measure: Optional[int] = Field(
+ None, description="Unit of measure"
)
- alternate_unit: Optional[str] = Field(
- None, max_length=10, description="Alternate unit"
+ alternate_unit: Optional[int] = Field(
+ None, description="Alternate unit"
)
uma_key: Optional[str] = Field(None, max_length=2, description="UMA key")
auxiliary_unit: Optional[str] = Field(
diff --git a/frontend/src/lib/api/dashboard/a76/items.ts b/frontend/src/lib/api/dashboard/a76/items.ts
index abfbf846..60cec7d3 100644
--- a/frontend/src/lib/api/dashboard/a76/items.ts
+++ b/frontend/src/lib/api/dashboard/a76/items.ts
@@ -68,6 +68,12 @@ export interface LineDescriptions {
brand?: string;
model?: string;
has_serial?: boolean;
+ lot?: string;
+ entry_number?: string;
+ eighth_rule_fraction?: string;
+ eighth_rule_line?: number;
+ consider_a31?: boolean;
+ machinery_location?: string;
}
export interface LineReferences {
@@ -125,23 +131,28 @@ export interface LineItem {
identifier?: string;
// Unit of Measure
- unit_of_measure?: string;
- alternate_unit?: string;
+ unit_of_measure?: number;
+ alternate_unit?: number;
// Permits
permit_number?: string;
page_line?: string;
has_certificate?: boolean;
certificate_number?: string;
+ octave_permit?: string;
// Flags
is_subitem?: boolean;
includes_subitems?: boolean;
tax_payment?: boolean;
+ is_military_mcia?: boolean;
// Payment
payment_method?: string;
igi_amount?: number;
+
+ // Additional notes
+ wildcard_field?: string;
// Computed fields from class_info relation
class_code?: string;
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/class-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/class-dialog.svelte
new file mode 100644
index 00000000..fb7c8435
--- /dev/null
+++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/class-dialog.svelte
@@ -0,0 +1,170 @@
+
+
+ {error}
+
+
+
+
+
+
+ {#each filteredCountries as country, i}
+ Clave M3
+ Clave Mexicana
+ Descripción Español
+ Clave Americana
+ Descripción Inglés
+ handleSelect(country)}
+ >
+
+ {/each}
+ {#if filteredCountries.length === 0}
+ {country.m3_key || ''}
+ {country.mex_key || ''}
+ {country.description_es || ''}
+ {country.ame_key || ''}
+ {country.description_en || ''}
+
+
+ {/if}
+
+
+ No se encontraron resultados
+
+