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 @@ + + + + + + Seleccionar Clase + + Busca y selecciona una clase para la partida + + + +
+
+ + +
+
+ +
+ {#if isSearching} +
+ +
+ {:else} + + + + Código + Descripción + U.M. + + + + + {#if displayedClasses.length === 0} + + + No se encontraron clases + + + {:else} + {#each displayedClasses as classItem} + handleSelect(classItem)}> + {classItem.class_code} + {classItem.description_es || classItem.description_en || '-'} + {classItem.unit_of_measure || '-'} + + + + + {/each} + {/if} + + + {/if} +
+ + + + +
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/country-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/country-dialog.svelte new file mode 100644 index 00000000..ddb6b92d --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/country-dialog.svelte @@ -0,0 +1,178 @@ + + + + + + CATALOGO DE PAISES + + +
+
+ + +
+
+ +
+ {#if loading} +
+ +
+ {:else if error} +
+

{error}

+
+ {:else} +
+ + + + + + + + + + + + {#each filteredCountries as country, i} + handleSelect(country)} + > + + + + + + + {/each} + {#if filteredCountries.length === 0} + + + + {/if} + +
Clave M3Clave MexicanaDescripción EspañolClave AmericanaDescripción Inglés
{country.m3_key || ''}{country.mex_key || ''}{country.description_es || ''}{country.ame_key || ''}{country.description_en || ''}
+ No se encontraron resultados +
+
+ +
+
+ + + + +
+
+ {/if} +
+ +
+ +
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte index 43237574..5c76d561 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte @@ -35,7 +35,7 @@
- Is + Is
- Contains Sub-Items + Contains Sub-Items
- +
- +
+

ID de número de parte existente en catálogo

diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte index 194af261..a091ac75 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte @@ -1,155 +1,183 @@ - - - - - - Temporary Import Item - - - Order Number: {invoice?.invoice_number || 'N/A'} | Line: currentline - - + + +
+
+
+
+ +
+
+ + {isEditMode ? 'Editar Partida' : 'Nueva Partida - Activo Fijo'} + +

+ Factura: {invoice?.invoice_number || 'N/A'} +

+
+
+ +
+
-
- -
- -
- {#if editingItem.lines && editingItem.lines.length > 0} - - {/if} -
+
+
+ + {#if line} +
+
+
+
+

Datos Principales

+
+
+ +
+
+
- - {#if editingItem.lines && editingItem.lines.length > 0} - +
+
+
+

Configuración

+
+
+ +
+
+
+
+ + + + + General + + + Continuación + + + Series + + + Etiquetado + + + IDs + + + +
+ +
+ + +
+
+ + + + + + + + + + + + + + + + +
+
+ {:else} +
+ +

Cargando datos de la partida...

+
{/if} -
+
+
- - - - 1) General - 2) Continuation - 3) Series - 4) Labeling - 5) Identifiers - - - - -
- {#if editingItem.lines && editingItem.lines.length > 0} - - - {/if} -
-
- - - - {#if editingItem.lines && editingItem.lines.length > 0} - +
+
+ + +
+
- - - {#if editingItem.lines && editingItem.lines.length > 0} - - {/if} - - - - - {#if editingItem.lines && editingItem.lines.length > 0} - - {/if} - - - - - {#if editingItem.lines && editingItem.lines.length > 0} - - {/if} - -
-
- - -
- - -
-
-
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte index a2522f3a..dc253760 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte @@ -1,6 +1,12 @@ -
- Main Data - - -
-
- -
- -
-
-
+ + + + - -
-
+
+ Main Data + +
+ +
+ +
+ + +
+ {#if (lineItem as any).class_code} +

Código: {(lineItem as any).class_code}

+ {/if} +
+ + +
-
+ +
-
- -
+ +
-
- -
-
+ +
- USD + USD
-
+ +
- + +
-
- -
-
+ +
- + +
-
- - -
-
- -
- + +
+
+ + +
+ + +
+ Advalorem: + + {customs.advalorem || '0'} +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte index 7cf2ad92..d3c6f785 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte @@ -57,7 +57,7 @@
- KILOS + KILOS
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte index 3c37fa81..27b66319 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte @@ -10,40 +10,36 @@
RETURN QUANTITY SUB-ITEMS
-
Temporary: {quantities.quantity_temp_export?.toFixed(8) || '0.00000000'}
-
Replacement or Change: 0.00000000
-
Definitive: {quantities.quantity_returned?.toFixed(8) || '0.00000000'}
-
Returned Values: {financials.value_returned_usd?.toFixed(8) || '0.00000000'}
-
Returned Values: {financials.value_returned_mxn?.toFixed(8) || '0.00000000'}
-
+
Temporary: {quantities.quantity_temp_export?.toFixed(8) || '0.00000000'}
+
Replacement or Change: 0.00000000
+
Definitive: {quantities.quantity_returned?.toFixed(8) || '0.00000000'}
+
Returned Values: {financials.value_returned_usd?.toFixed(8) || '0.00000000'}
+
Returned Values: {financials.value_returned_mxn?.toFixed(8) || '0.00000000'}
+
+
+
WEIGHTS (KILOS)
+
WEIGHTS (Pounds)
+
Net: {quantities.net_weight?.toFixed(8) || '0.00000000'}
+
0.00000000
+
Whole: {quantities.gross_weight?.toFixed(8) || '0.00000000'}
+
0.00000000
+
+
-
-
WEIGHTS (KILOS)
-
WEIGHTS (Pounds)
-
Net: {quantities.net_weight?.toFixed(8) || '0.00000000'}
-
0.00000000
-
Whole: {quantities.gross_weight?.toFixed(8) || '0.00000000'}
-
0.00000000
-
-
- - -
- COSTS AND VALUES - -
-
(Dollars)
-
(Pesos)
-
Cost: {financials.unit_cost_usd?.toFixed(8) || '0.00000000'}
-
{financials.unit_cost_mxn?.toFixed(8) || '0.00000000'}
-
Value: {financials.value_usd?.toFixed(8) || '0.00000000'}
-
{financials.value_mxn?.toFixed(8) || '0.00000000'}
-
- -
-
Capture Cost: {financials.unit_cost_capture?.toFixed(8) || '0.00000000'} USD
-
Capture Value: {financials.value_usd?.toFixed(8) || '0.00000000'} USD
-
Customs Value: {financials.customs_value_usd?.toFixed(8) || '0.00000000'} USD
-
-
+ +
+ COSTS AND VALUES + +
+
(Dollars)
+
(Pesos)
+
Cost: {financials.unit_cost_usd?.toFixed(8) || '0.00000000'}
+
{financials.unit_cost_mxn?.toFixed(8) || '0.00000000'}
+
Value: {financials.value_usd?.toFixed(8) || '0.00000000'}
+
{financials.value_mxn?.toFixed(8) || '0.00000000'}
+
Capture Cost: {financials.unit_cost_capture?.toFixed(8) || '0.00000000'} USD
+
Capture Value: {financials.value_usd?.toFixed(8) || '0.00000000'} USD
+
Customs Value: {financials.customs_value_usd?.toFixed(8) || '0.00000000'} USD
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte index b44702d5..6381f4c9 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte @@ -3,12 +3,14 @@ import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; import { Checkbox } from '$lib/components/ui/checkbox'; - import type { LineItem } from '$lib/api/dashboard/a76/items'; + import type { LineItem, LineDescriptions } from '$lib/api/dashboard/a76/items'; let { - lineItem = $bindable() + lineItem = $bindable(), + descriptions = $bindable() }: { lineItem: LineItem; + descriptions: LineDescriptions; } = $props(); let taxPaidValue = $derived(lineItem.tax_payment ? 'si' : 'no'); @@ -22,32 +24,32 @@ } -
+
-
+
-
-
- TAX PAID +
+
+ TAX PAID -
+ class="flex gap-2"> +
-
+
-
-
+
+
- +
@@ -56,101 +58,102 @@
-
+
- +
-
-
- Has Certificate of Origin? +
+
+ Has Certificate of Origin? -
+ class="flex gap-2"> +
-
+
-
+
- +
-
-
+
+
- +
-
- +
+
-
+
- +
-
+
- +
-
+
-
-
+
+
- +
-
+
- +
-
+
- +
-
- +
+
-
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte index 3a3034c3..dd5d1e5e 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte @@ -9,34 +9,18 @@
Identifiers -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
+
+ +
- +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte index cacb0306..3e28e03e 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte @@ -12,11 +12,11 @@
- +
- +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tariff-fraction-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tariff-fraction-dialog.svelte new file mode 100644 index 00000000..02721e57 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tariff-fraction-dialog.svelte @@ -0,0 +1,218 @@ + + + + + + FRACCIONES ARANCELARIAS + + +
+
+ + +
+

+ Mostrando {fractions.length} de {currentPage * pageSize} resultados +

+
+ +
+ {#if loading} +
+ +
+ {:else if error} +
+

{error}

+
+ {:else} +
+ + + + + + + + + + + + {#each fractions as fraction, i} + handleSelect(fraction)} + > + + + + + + + {/each} + {#if fractions.length === 0 && !loading} + + + + {/if} + +
CódigoFracciónDescripciónNICOUMT
{fraction.code || ''}{fraction.fraction || ''}{fraction.description || ''}{fraction.nico || ''}{fraction.umt || ''}
+ No se encontraron resultados +
+
+ + {#if loadingMore} +
+ + Cargando más... +
+ {/if} + + {#if !hasMore && fractions.length > 0} +
+ Todos los resultados cargados +
+ {/if} + {/if} +
+ +
+ +
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/unit-of-measure-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/unit-of-measure-dialog.svelte new file mode 100644 index 00000000..55dab050 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/unit-of-measure-dialog.svelte @@ -0,0 +1,183 @@ + + + + + + CATALOGOS DE UNIDADES DE MEDIDA + + +
+
+ + +
+
+ +
+ {#if loading} +
+ +
+ {:else if error} +
+

{error}

+
+ {:else} +
+ + + + + + + + + + + + + {#each filteredUnits as unit, i} + handleSelect(unit)} + > + + + + + + + + {/each} + {#if filteredUnits.length === 0} + + + + {/if} + +
U.M.Descripción EspañolAbrév. InglésClave AduanaClave AmericanaClave O.M.A.
{unit.code || ''}{unit.description || ''}{unit.description_en || ''}{unit.customs_code || ''}{unit.american_code || ''}{unit.oma_code || ''}
+ No se encontraron resultados +
+
+ +
+
+ + + + +
+
+ {/if} +
+ +
+ +
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte index d354044a..dd017cfe 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte @@ -83,6 +83,8 @@ if (response.data) { items = response.data.items || []; currentPage = 1; + // Wait for derived state to update before loading items + await new Promise(resolve => setTimeout(resolve, 0)); loadMoreItems(); } } catch (error: any) { @@ -150,6 +152,8 @@ tax_payment: false, payment_method: undefined, igi_amount: undefined, + is_military_mcia: false, + wildcard_field: undefined, // Nested relations financial: { unit_cost_usd: undefined, @@ -191,6 +195,10 @@ brand: undefined, model: undefined, has_serial: false, + eighth_rule_fraction: undefined, + eighth_rule_line: undefined, + consider_a31: false, + machinery_location: undefined, }, reference: { serie_id: undefined, @@ -263,18 +271,61 @@ showDeleteDialog = true; } + // Helper function to check if an object has any meaningful values + function hasValues(obj: any): boolean { + if (!obj || typeof obj !== 'object') return false; + return Object.values(obj).some(val => + val !== undefined && val !== null && val !== '' && + !(typeof val === 'object' && !hasValues(val)) + ); + } + + // Clean nested data before sending to API + function cleanLineData(line: any) { + const cleaned: any = { ...line }; + + // Helper function to convert to number or undefined + const toNumberOrUndefined = (value: any): number | undefined => { + if (value === undefined || value === null || value === '') { + return undefined; + } + const numValue = Number(value); + return (!isNaN(numValue) && isFinite(numValue)) ? numValue : undefined; + }; + + // Convert integer fields + cleaned.part_number_id = toNumberOrUndefined(cleaned.part_number_id); + cleaned.component_part_number_id = toNumberOrUndefined(cleaned.component_part_number_id); + cleaned.class_id = toNumberOrUndefined(cleaned.class_id); + cleaned.unit_of_measure = toNumberOrUndefined(cleaned.unit_of_measure); + cleaned.alternate_unit = toNumberOrUndefined(cleaned.alternate_unit); + + // Remove empty nested objects + if (!hasValues(cleaned.financial)) delete cleaned.financial; + if (!hasValues(cleaned.quantity)) delete cleaned.quantity; + if (!hasValues(cleaned.customs)) delete cleaned.customs; + if (!hasValues(cleaned.description)) delete cleaned.description; + if (!hasValues(cleaned.reference)) delete cleaned.reference; + if (!hasValues(cleaned.fa_data)) delete cleaned.fa_data; + + return cleaned; + } + async function saveNewItem() { if (!invoice?.id || !activeCompanyId) return; isSaving = true; try { + // Clean lines data before sending + const cleanedLines = (editingItem.lines || []).map(cleanLineData); + const response = await itemsApi.create(activeCompanyId, { invoice_id: invoice.id, reference_number: editingItem.reference_number, order: editingItem.order, warehouse: editingItem.warehouse, location: editingItem.location, - lines: editingItem.lines || [] + lines: cleanedLines }); // Recargar items @@ -300,12 +351,15 @@ isSaving = true; try { + // Clean lines data before sending + const cleanedLines = (editingItem.lines || []).map(cleanLineData); + await itemsApi.update(selectedItem.id, activeCompanyId, { reference_number: editingItem.reference_number, order: editingItem.order, warehouse: editingItem.warehouse, location: editingItem.location, - lines: editingItem.lines || [] + lines: cleanedLines }); // Recargar items diff --git a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts index c1a4bd98..a530d691 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts +++ b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts @@ -157,7 +157,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI } // Logistics - payload.logistics = buildLogisticsData(generalFormData, observationFormData); + payload.logistics = buildLogisticsData(generalFormData, observationFormData, continuationFormData); // Eliminar campos undefined para no enviarlos Object.keys(payload).forEach(key => { @@ -219,13 +219,30 @@ function buildFinancialsData(generalFormData: any, observationFormData: any, oth }; } -function buildLogisticsData(generalFormData: any, observationFormData: any) { - // Retornar logistics como objeto único +function buildLogisticsData(generalFormData: any, observationFormData: any, continuationFormData: any) { + // Retornar logistics como objeto único con datos de continuación return { carrier_id: generalFormData?.carrier_id || null, transport_type: generalFormData?.transport_type || 'none', driver_name: generalFormData?.driver_name || null, - vehicle_num: generalFormData?.transport_num || null, + vehicle_num: generalFormData?.transport_num || continuationFormData?.numero_tipo_transporte || null, incoterm: observationFormData?.incoterm || null, + // Campos de continuación mapeados a logistics + transport_num: continuationFormData?.numero_tipo_transporte || null, + is_rail: continuationFormData?.es_ferrocarril === 'si' ? true : false, + bill_number: continuationFormData?.numero_bl || null, + guide_number: continuationFormData?.cantidad_guias_embarque ? String(continuationFormData.cantidad_guias_embarque) : null, + destination_location: continuationFormData?.destino_origen || null, + origin_location: continuationFormData?.puerto_entrada || null, + // Checkboxes de continuación + equipment_reviewed: continuationFormData?.fue_revisado_equipo || false, + is_subdivision: continuationFormData?.sub_division || false, + acts_as_cd: continuationFormData?.funge_como_cd || false, + pedimento_arrived: continuationFormData?.llego_pedimento || false, + // Semáforos de continuación + green_light_mx: continuationFormData?.semaforo_verde_aduana_mexicana || false, + green_light_us: continuationFormData?.semaforo_verde_aduana_americana || false, + red_light_mx: continuationFormData?.semaforo_rojo_aduana_mexicana || false, + red_light_us: continuationFormData?.semaforo_rojo_aduana_americana || false, }; } diff --git a/frontend/src/routes/api-sveltekit/classes/+server.ts b/frontend/src/routes/api-sveltekit/classes/+server.ts new file mode 100644 index 00000000..3fc38241 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/classes/+server.ts @@ -0,0 +1,91 @@ +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ cookies, url }) => { + const token = cookies.get('access_token'); + + // Obtener company_id de la cookie + const companyId = cookies.get('active_company_id'); + + if (!companyId) { + return new Response( + JSON.stringify({ error: 'No company selected' }), + { + status: 400, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + + // Configurar la URL de la API usando las variables de entorno + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = process.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + // Get query parameters and add company_id + const searchParams = new URLSearchParams(url.search); + searchParams.set('company_id', companyId); + const queryString = searchParams.toString(); + + try { + const fetchUrl = `${baseUrl}v1/a76/classes?${queryString}`; + console.log('Fetching classes from:', fetchUrl); + + const response = await fetch( + fetchUrl, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error response from backend:', errorText); + return new Response( + JSON.stringify({ + error: 'Failed to fetch classes', + details: errorText + }), + { + status: response.status, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + + const data = await response.json(); + return new Response(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('Error in classes API route:', error); + return new Response( + JSON.stringify({ + error: 'Internal server error', + message: error instanceof Error ? error.message : 'Unknown error' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } +}; diff --git a/frontend/src/routes/api-sveltekit/tariff-fractions/+server.ts b/frontend/src/routes/api-sveltekit/tariff-fractions/+server.ts new file mode 100644 index 00000000..2704aca5 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/tariff-fractions/+server.ts @@ -0,0 +1,68 @@ +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ cookies, url }) => { + const token = cookies.get('access_token'); + + // Configurar la URL de la API usando las variables de entorno + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = process.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + // Get query parameters - tariff_fractions es catálogo global (no requiere company_id) + const searchParams = new URLSearchParams(url.search); + const queryString = searchParams.toString(); + + try { + const fetchUrl = `${baseUrl}v1/a76/tariff-fractions?${queryString}`; + console.log('Fetching tariff fractions from:', fetchUrl); + console.log('Token:', token ? 'Present' : 'Missing'); + + const response = await fetch( + fetchUrl, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ); + + const data = await response.json(); + console.log('Response status:', response.status); + console.log('Response data:', JSON.stringify(data).substring(0, 200)); + + if (!response.ok) { + return new Response(JSON.stringify(data), { + status: response.status, + headers: { + 'Content-Type': 'application/json' + } + }); + } + + return new Response(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('Error fetching tariff fractions:', error); + return new Response( + JSON.stringify({ error: 'Failed to fetch tariff fractions' }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } +}; diff --git a/frontend/src/routes/api-sveltekit/units-of-measure/+server.ts b/frontend/src/routes/api-sveltekit/units-of-measure/+server.ts new file mode 100644 index 00000000..51dda155 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/units-of-measure/+server.ts @@ -0,0 +1,84 @@ +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ cookies, url }) => { + const token = cookies.get('access_token'); + + // Obtener company_id de la cookie + const companyId = cookies.get('active_company_id'); + + if (!companyId) { + return new Response( + JSON.stringify({ error: 'No company selected' }), + { + status: 400, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + + // Configurar la URL de la API usando las variables de entorno + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = process.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + // Get query parameters and add company_id + const searchParams = new URLSearchParams(url.search); + searchParams.set('company_id', companyId); + const queryString = searchParams.toString(); + + try { + const fetchUrl = `${baseUrl}v1/a76/units-of-measure?${queryString}`; + console.log('Fetching units from:', fetchUrl); + console.log('Token:', token ? 'Present' : 'Missing'); + + const response = await fetch( + fetchUrl, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ); + + const data = await response.json(); + console.log('Response status:', response.status); + console.log('Response data:', JSON.stringify(data).substring(0, 200)); + + if (!response.ok) { + return new Response(JSON.stringify(data), { + status: response.status, + headers: { + 'Content-Type': 'application/json' + } + }); + } + + return new Response(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('Error fetching units of measure:', error); + return new Response( + JSON.stringify({ error: 'Failed to fetch units of measure' }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } +};