feat: Add new fields for invoice logistics and line items, enhance UI for class selection
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Search, Loader2 } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSelect
|
||||
}: {
|
||||
open?: boolean;
|
||||
onSelect?: (classItem: any) => void;
|
||||
} = $props();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let isSearching = $state(false);
|
||||
let classes = $state<any[]>([]);
|
||||
let displayedClasses = $state<any[]>([]);
|
||||
let currentPage = $state(1);
|
||||
let itemsPerPage = 10;
|
||||
|
||||
const filteredClasses = $derived(
|
||||
searchQuery
|
||||
? classes.filter(c =>
|
||||
c.class_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.description_es?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: classes
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
searchClasses();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
currentPage = 1;
|
||||
loadMoreClasses();
|
||||
});
|
||||
|
||||
async function searchClasses() {
|
||||
const activeCompanyId = companyStore?.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
toast.error('No hay compañía activa');
|
||||
return;
|
||||
}
|
||||
|
||||
isSearching = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api-sveltekit/classes?company_id=${activeCompanyId}&limit=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al buscar clases');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
classes = data.data?.items || [];
|
||||
loadMoreClasses();
|
||||
} catch (error) {
|
||||
console.error('Error searching classes:', error);
|
||||
toast.error('Error al buscar clases');
|
||||
classes = [];
|
||||
} finally {
|
||||
isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadMoreClasses() {
|
||||
const start = 0;
|
||||
const end = currentPage * itemsPerPage;
|
||||
displayedClasses = filteredClasses.slice(start, end);
|
||||
}
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
const threshold = 100;
|
||||
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||
|
||||
if (scrolledToBottom && displayedClasses.length < filteredClasses.length) {
|
||||
currentPage++;
|
||||
loadMoreClasses();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(classItem: any) {
|
||||
if (onSelect) {
|
||||
onSelect(classItem);
|
||||
}
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-4xl max-h-[80vh] flex flex-col">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Clase</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Busca y selecciona una clase para la partida
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex gap-2 mb-4">
|
||||
<div class="relative flex-1">
|
||||
<Search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar por código o descripción..."
|
||||
bind:value={searchQuery}
|
||||
class="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto border rounded-md" onscroll={handleScroll}>
|
||||
{#if isSearching}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<Loader2 class="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
{:else}
|
||||
<Table.Root>
|
||||
<Table.Header class="sticky top-0 bg-background z-10">
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[120px]">Código</Table.Head>
|
||||
<Table.Head>Descripción</Table.Head>
|
||||
<Table.Head class="w-[100px]">U.M.</Table.Head>
|
||||
<Table.Head class="w-[100px]"></Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if displayedClasses.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={4} class="text-center py-8 text-muted-foreground">
|
||||
No se encontraron clases
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each displayedClasses as classItem}
|
||||
<Table.Row class="cursor-pointer hover:bg-muted/50" onclick={() => handleSelect(classItem)}>
|
||||
<Table.Cell class="font-medium">{classItem.class_code}</Table.Cell>
|
||||
<Table.Cell>{classItem.description_es || classItem.description_en || '-'}</Table.Cell>
|
||||
<Table.Cell>{classItem.unit_of_measure || '-'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button size="sm" variant="ghost" onclick={() => handleSelect(classItem)}>
|
||||
Seleccionar
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -72,10 +72,11 @@
|
||||
<!-- Descriptions -->
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="num_parte" class="text-xs">Part Number:</Label>
|
||||
<Label for="num_parte" class="text-xs">Part Number ID (opcional):</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="num_parte" bind:value={lineItem.part_number_id} class="h-7 text-xs" />
|
||||
<Input id="num_parte" type="number" bind:value={lineItem.part_number_id} class="h-7 text-xs" placeholder="Dejar vacío si no aplica" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">ID de número de parte existente en catálogo</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
|
||||
@@ -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 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ClassDialog bind:open={showClassDialog} onSelect={handleClassSelect} />
|
||||
<UnitOfMeasureDialog bind:open={showUnitDialog} onSelect={handleUnitSelect} />
|
||||
<CountryDialog bind:open={showCountryDialog} onSelect={handleCountrySelect} />
|
||||
<TariffFractionDialog bind:open={showFractionDialog} onSelect={handleFractionSelect} />
|
||||
@@ -47,8 +63,27 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Class - Full Width -->
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="clase" class="text-xs font-medium">* Class:</Label>
|
||||
<Input id="clase" bind:value={lineItem.class_id} class="h-8 text-xs" />
|
||||
<Label for="clase" class="text-xs font-medium">* Clase (ID):</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
id="clase"
|
||||
type="number"
|
||||
bind:value={lineItem.class_id}
|
||||
class="h-8 text-xs flex-1"
|
||||
placeholder="ID de clase (1-6)"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
onclick={() => (showClassDialog = true)}
|
||||
>
|
||||
<Folder class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if (lineItem as any).class_code}
|
||||
<p class="text-xs text-muted-foreground">Código: {(lineItem as any).class_code}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Quantity and U.M. on the same row -->
|
||||
@@ -60,7 +95,7 @@
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs font-medium">U.M.:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="um" bind:value={lineItem.unit_of_measure} class="h-8 text-xs flex-1" placeholder="U.M." />
|
||||
<Input id="um" type="number" bind:value={lineItem.unit_of_measure} class="h-8 text-xs flex-1" placeholder="ID de U.M." />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
@@ -117,7 +152,9 @@
|
||||
<Label for="tipo_tarifa" class="text-xs font-medium">* Tariff Type:</Label>
|
||||
<select id="tipo_tarifa" bind:value={customs.fraction_type} class="flex h-8 w-auto min-w-[140px] rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background">
|
||||
<option value="GENERAL">GENERAL</option>
|
||||
<option value="PREFERENCIAL">PREFERENCIAL</option>
|
||||
<option value="PROSEC">PROSEC</option>
|
||||
<option value="ALADI">ALADI</option>
|
||||
<option value="TLCS">TLCS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="space-y-0.5">
|
||||
<Label for="localizacion_maquinaria" class="text-xs">Machinery and equipment location:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="localizacion_maquinaria" class="h-6 text-xs" />
|
||||
<Input id="localizacion_maquinaria" bind:value={descriptions.machinery_location} class="h-6 text-xs" />
|
||||
</div>
|
||||
<Label for="localizacion_maquinaria" class="text-xs">Location variable</Label>
|
||||
</div>
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
<!-- Military Equipment -->
|
||||
<div class="flex items-center space-x-1.5 ">
|
||||
<Checkbox id="equipo_militar" />
|
||||
<Checkbox id="equipo_militar" bind:checked={lineItem.is_military_mcia} />
|
||||
<Label for="equipo_militar" class="text-xs font-normal">Enable if Item Contains Military Equipment</Label>
|
||||
</div>
|
||||
|
||||
@@ -132,18 +132,18 @@
|
||||
<div class="grid grid-cols-4 gap-2 items-end">
|
||||
<div class="space-y-0.5 col-span-3">
|
||||
<Label for="fraccion_regla_octava" class="text-xs">Eighth Rule Fraction:</Label>
|
||||
<Input id="fraccion_regla_octava" value="0000.00.00" class="h-6 text-xs" />
|
||||
<Input id="fraccion_regla_octava" bind:value={descriptions.eighth_rule_fraction} class="h-6 text-xs" />
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
<Label for="linea_regla" class="text-xs">Line:</Label>
|
||||
<Input id="linea_regla" type="number" min="0" value="0" class="h-6 text-xs text-right" />
|
||||
<Input id="linea_regla" type="number" min="0" bind:value={descriptions.eighth_rule_line} class="h-6 text-xs text-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consider in a31 -->
|
||||
<div class="flex items-center space-x-1.5 ">
|
||||
<Checkbox id="a31" />
|
||||
<Checkbox id="a31" bind:checked={descriptions.consider_a31} />
|
||||
<Label for="a31" class="text-xs font-normal">Consider in A31</Label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<Label for="notas_identificadores" class="text-xs">Additional Identifiers / Notes:</Label>
|
||||
<textarea
|
||||
id="notas_identificadores"
|
||||
bind:value={lineItem.wildcard_field}
|
||||
class="flex min-h-[120px] w-full rounded-md border-2 border-input dark:border-zinc-600 bg-background dark:text-white px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 dark:focus-visible:border-zinc-400 dark:focus-visible:ring-zinc-400/50"
|
||||
placeholder="Additional identifiers or notes..."
|
||||
></textarea>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
91
frontend/src/routes/api-sveltekit/classes/+server.ts
Normal file
91
frontend/src/routes/api-sveltekit/classes/+server.ts
Normal file
@@ -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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user