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 c576ba68..60cec7d3 100644 --- a/frontend/src/lib/api/dashboard/a76/items.ts +++ b/frontend/src/lib/api/dashboard/a76/items.ts @@ -70,6 +70,10 @@ export interface LineDescriptions { 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 { @@ -127,8 +131,8 @@ 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; @@ -141,10 +145,14 @@ export interface LineItem { 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/item-configuration.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte index 937b4232..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 @@ -72,10 +72,11 @@
- +
- +
+

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

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 83a1ba37..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 @@ -6,6 +6,7 @@ import UnitOfMeasureDialog from './unit-of-measure-dialog.svelte'; import CountryDialog from './country-dialog.svelte'; import TariffFractionDialog from './tariff-fraction-dialog.svelte'; + import ClassDialog from './class-dialog.svelte'; import type { LineItem, LineQuantities, LineFinancials, LineCustoms } from '$lib/api/dashboard/a76/items'; let { @@ -20,12 +21,26 @@ customs: LineCustoms; } = $props(); + let showClassDialog = $state(false); let showUnitDialog = $state(false); let showCountryDialog = $state(false); let showFractionDialog = $state(false); + // Initialize from existing data + $effect(() => { + if (lineItem.class_code) { + // Do nothing, it's already set + } + }); + + function handleClassSelect(classItem: any) { + lineItem.class_id = classItem.id; + // Store the code in the lineItem for display + (lineItem as any).class_code = classItem.class_code; + } + function handleUnitSelect(unit: any) { - lineItem.unit_of_measure = unit.code; + lineItem.unit_of_measure = unit.id; } function handleCountrySelect(country: any) { @@ -37,6 +52,7 @@ } + @@ -47,8 +63,27 @@
- - + +
+ + +
+ {#if (lineItem as any).class_code} +

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

+ {/if}
@@ -60,7 +95,7 @@
- +
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 0f514eba..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 @@ -94,7 +94,7 @@
- +
@@ -102,7 +102,7 @@
- +
@@ -132,18 +132,18 @@
- +
- +
- +
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 31542f67..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 @@ -18,6 +18,7 @@ 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 9660f5b3..0cab46f7 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts +++ b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts @@ -158,7 +158,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 => { @@ -220,13 +220,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' + } + } + ); + } +};