Merge pull request 'fix/item-validations' (#142) from fix/item-validations into development
Reviewed-on: ADUANASOFT/anexo76#142
This commit is contained in:
@@ -58,7 +58,7 @@ def validate_update(
|
||||
|
||||
# Columna A: Pedimento (si no viene en CSV, usar el existente)
|
||||
if invoice_data.compliance_mx.pedimento_id:
|
||||
invoice_data.compliance_mx.pedimento_id = clean_str(invoice_data.compliance_mx.pedimento_id)
|
||||
invoice_data.compliance_mx.pedimento_id = invoice_data.compliance_mx.pedimento_id
|
||||
else:
|
||||
invoice_data.compliance_mx.pedimento_id = existing_invoice.compliance_mx.pedimento_id if existing_invoice.compliance_mx else None
|
||||
|
||||
|
||||
@@ -3,36 +3,31 @@ from core.exceptions import ErrorCollector
|
||||
from ..line_items import models
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
def item_exists(
|
||||
db: Session,
|
||||
item_line: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
):
|
||||
|
||||
def item_exists(db: Session, item_line: int, tenant_id: int, company_id: int):
|
||||
item_exists = (
|
||||
db.query(models.LineItem.id)
|
||||
db.query(models.LineItem)
|
||||
.filter(
|
||||
models.LineItem.line_number == item_line,
|
||||
models.LineItem.tenant_id == tenant_id,
|
||||
models.LineItem.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
return item_exists
|
||||
|
||||
|
||||
def count_items(db: Session, invoice_id: int, tenant_id: int, company_id: int):
|
||||
count = (
|
||||
db.query(func.count())
|
||||
.select_from(models.Item)
|
||||
.filter(
|
||||
models.Item.invoice_id == invoice_id,
|
||||
models.Item.tenant_id == tenant_id,
|
||||
models.Item.company_id == company_id,
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
if item_exists:
|
||||
return item_exists
|
||||
return None
|
||||
|
||||
def count_items(
|
||||
db: Session,
|
||||
invoice_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
):
|
||||
count = db.query(func.count()).select_from(models.Item).filter(
|
||||
models.Item.invoice_id == invoice_id,
|
||||
models.Item.tenant_id == tenant_id,
|
||||
models.Item.company_id == company_id,
|
||||
).scalar()
|
||||
|
||||
return count
|
||||
return count
|
||||
|
||||
@@ -42,9 +42,7 @@ def validate_common(
|
||||
invoice: InvoiceHeader = invoice_exists_by_id(
|
||||
db, invoice_id, tenant_id, company_id, errors
|
||||
)
|
||||
line_item: LineItem = item_exists(
|
||||
db, line.line_number, tenant_id, company_id
|
||||
)
|
||||
line_item: LineItem = item_exists(db, line.line_number, tenant_id, company_id)
|
||||
|
||||
fecha_factura = invoice.invoice_date if invoice else None
|
||||
fraction = None
|
||||
@@ -181,13 +179,15 @@ def validate_common(
|
||||
fraction = line.customs.fraction if line.customs.fraction else fraction
|
||||
|
||||
country = line.customs.origin_country
|
||||
if line_item:
|
||||
if line_item and line_item.customs:
|
||||
country = (
|
||||
line_item.customs.fraction if line_item.customs.origin_country else country
|
||||
line_item.customs.origin_country
|
||||
if line_item.customs.origin_country
|
||||
else country
|
||||
)
|
||||
|
||||
fraction_type = line.customs.fraction_type.upper()
|
||||
if line_item:
|
||||
if line_item and line_item.customs:
|
||||
fraction_type = (
|
||||
line_item.customs.fraction_type
|
||||
if line_item.customs.fraction_type
|
||||
@@ -195,7 +195,7 @@ def validate_common(
|
||||
)
|
||||
|
||||
sector = line.customs.sector
|
||||
if line_item:
|
||||
if line_item and line_item.customs:
|
||||
sector = line_item.customs.sector if line_item.customs.sector else sector
|
||||
|
||||
country_m3 = db.query(Country.m3_key).filter(Country.m3_key == country).scalar()
|
||||
|
||||
@@ -206,8 +206,8 @@ def validate_create(
|
||||
net_weight_input = line.quantity.net_weight or Decimal("0")
|
||||
|
||||
# Determinar si la unidad de medida es de peso
|
||||
unit_is_kgs = line.unit_of_measure and line.unit_of_measure.upper() == "KGS"
|
||||
unit_is_lbs = line.unit_of_measure and line.unit_of_measure.upper() == "LB"
|
||||
unit_is_kgs = line.unit_of_measure and line.unit_of_measure == "24" #KGS
|
||||
unit_is_lbs = line.unit_of_measure and line.unit_of_measure == "25" #LBS
|
||||
|
||||
# Calcular peso neto en kilogramos (estándar interno)
|
||||
if unit_is_kgs:
|
||||
@@ -237,11 +237,11 @@ def validate_create(
|
||||
package_weight_unit = Decimal("0")
|
||||
|
||||
# Obtener peso unitario del bulto si existe
|
||||
if line.quantity.package_key:
|
||||
if line.quantity.package_id:
|
||||
package: Package = (
|
||||
db.query(Package)
|
||||
.filter(
|
||||
Package.key == line.quantity.package_key,
|
||||
Package.id == line.quantity.package_id,
|
||||
Package.tenant_id == tenant_id,
|
||||
Package.company_id == company_id,
|
||||
)
|
||||
@@ -278,22 +278,22 @@ def validate_create(
|
||||
# ==========================================
|
||||
# ASIGNAR DESCRIPCIÓN DE BULTOS
|
||||
# ==========================================
|
||||
if package_quantity and package_quantity > 0 and line.quantity.package_key:
|
||||
if package_quantity and package_quantity > 0 and line.quantity.package_id:
|
||||
package: Package = (
|
||||
db.query(Package)
|
||||
.filter(
|
||||
Package.key == line.quantity.package_key,
|
||||
Package.id == line.quantity.package_id,
|
||||
Package.tenant_id == tenant_id,
|
||||
Package.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if package:
|
||||
line.quantity.package_description = package.description_es
|
||||
line.description.package_description = package.description_es
|
||||
else:
|
||||
line.quantity.package_quantity = 0
|
||||
line.quantity.package_key = None
|
||||
line.quantity.package_description = None
|
||||
line.quantity.package_id = None
|
||||
line.description.package_description = None
|
||||
|
||||
# ==========================================
|
||||
# ASIGNAR FRACCIÓN AMERICANA POR DEFECTO
|
||||
|
||||
@@ -24,7 +24,8 @@ class LineDescription(Base):
|
||||
description_english: Mapped[Optional[str]] = mapped_column(String(4999)) # DESCRIPCIONI
|
||||
extra_description: Mapped[Optional[str]] = mapped_column(Text) # DESCRIPCIONEEXTRA
|
||||
part_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONPARTE
|
||||
class_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONCLASE
|
||||
class_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONCLASE
|
||||
package_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONBULTO
|
||||
|
||||
# Product attributes
|
||||
brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA
|
||||
|
||||
@@ -13,6 +13,7 @@ class LineDescriptionBase(BaseModel):
|
||||
extra_description: Optional[str] = Field(None, description="Extra description (DESCRIPCIONEEXTRA)")
|
||||
part_description: Optional[str] = Field(None, max_length=500, description="Part description (DESCRIPCIONPARTE)")
|
||||
class_description: Optional[str] = Field(None, max_length=500, description="Class description (DESCRIPCIONCLASE)")
|
||||
package_description: Optional[str] = Field(None, max_length=500, description="Package description (DESCRIPCIONBULTO)")
|
||||
|
||||
# Product attributes
|
||||
brand: Optional[str] = Field(None, max_length=50, description="Brand (MARCA)")
|
||||
|
||||
@@ -17,6 +17,7 @@ from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSec
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
|
||||
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
|
||||
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
|
||||
from .dtos.pedimentos import PedimentosResponse
|
||||
|
||||
from .schemas import PedimentoCatalogsResponse, PedimentoCreationResponse, PedimentoEditionResponse
|
||||
|
||||
@@ -111,10 +112,13 @@ class PedimentoCatalogService:
|
||||
|
||||
if not pedimento:
|
||||
return None
|
||||
|
||||
# Convert SQLAlchemy object to Pydantic DTO
|
||||
pedimento_dto = PedimentosResponse.model_validate(pedimento)
|
||||
|
||||
return PedimentoEditionResponse(
|
||||
**catalogs.model_dump(),
|
||||
is_create=False,
|
||||
pedimento=pedimento,
|
||||
pedimento=pedimento_dto,
|
||||
pedimento_id=pedimento_id
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Consolidated schemas for Pedimento catalog responses
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Any
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Import DTOs for catalog items
|
||||
@@ -11,6 +11,7 @@ from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSec
|
||||
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
|
||||
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
|
||||
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
|
||||
from .dtos.pedimentos import PedimentosResponse
|
||||
|
||||
|
||||
class PedimentoCatalogsResponse(BaseModel):
|
||||
@@ -33,5 +34,5 @@ class PedimentoEditionResponse(PedimentoCatalogsResponse):
|
||||
"""Response for editing an existing pedimento (catalogs + pedimento data)"""
|
||||
|
||||
is_create: bool = False
|
||||
pedimento: Optional[Any] = None # Will be PedimentosResponse but avoiding circular import
|
||||
pedimento: Optional[PedimentosResponse] = None
|
||||
pedimento_id: Optional[int] = None
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const data = await response.json();
|
||||
if (Array.isArray(data)) {
|
||||
countries = data;
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
@@ -80,7 +80,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-4xl w-full max-h-[85vh] p-0 flex flex-col">
|
||||
<Dialog.Content class="!max-w-[70vw] w-[70vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGO DE PAISES</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -88,7 +88,11 @@
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input bind:value={searchTerm} placeholder="Buscando..." class="flex-1 h-9" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +110,9 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Clave M3</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave M3</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Clave Mexicana</th
|
||||
>
|
||||
@@ -125,11 +131,11 @@
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
onclick={() => handleSelect(country)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r">{country.m3_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.mex_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.description_es || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{country.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{country.description_en || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.m3_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.mex_key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{country.description_es || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-center">{country.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{country.description_en || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredCountries.length === 0}
|
||||
@@ -143,9 +149,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400"
|
||||
>
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<<
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as Sheet from '$lib/components/ui/sheet';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { Loader2, Package, Save, X, FileText } from 'lucide-svelte';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import type { Item } from '$lib/api/dashboard/a76/items';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosPestanasItemFa } from '$lib/config/shortcuts/dashboard/invoices/item/fixed_asset';
|
||||
|
||||
// Child components
|
||||
import MainData from './main-data.svelte';
|
||||
@@ -26,7 +24,6 @@
|
||||
invoice,
|
||||
onSave,
|
||||
onCancel,
|
||||
|
||||
isTargetingPreset = false,
|
||||
isSaving = false
|
||||
}: {
|
||||
@@ -36,87 +33,39 @@
|
||||
invoice: Invoice | null;
|
||||
onSave: () => void;
|
||||
onCancel?: () => void;
|
||||
|
||||
isTargetingPreset?: boolean;
|
||||
isSaving?: boolean;
|
||||
} = $props();
|
||||
|
||||
// Acceso directo a la primera línea para evitar repeticiones en el HTML
|
||||
let line = $derived(editingItem.lines?.[0]);
|
||||
|
||||
let activeTab = $state('generales');
|
||||
|
||||
const tabMapping: Record<string, string> = {
|
||||
'tab1': 'generales',
|
||||
'tab2': 'continuacion',
|
||||
'tab3': 'series',
|
||||
'tab4': 'etiquetado',
|
||||
'tab5': 'identificadores'
|
||||
};
|
||||
|
||||
useShortcuts(
|
||||
'Invoice Item Form (Fixed Asset)',
|
||||
obtenerAtajosPestanasItemFa({
|
||||
cambiarPestana: (target) => {
|
||||
const tab = tabMapping[target];
|
||||
if (tab) activeTab = tab;
|
||||
},
|
||||
manejarGuardar: onSave,
|
||||
manejarCancelar: () => onCancel?.()
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={open}>
|
||||
<Dialog.Content class="sm:max-w-6xl max-h-[90vh] p-0 overflow-hidden z-[100] [&>button]:hidden">
|
||||
<div class="bg-white dark:bg-zinc-950 border-b dark:border-zinc-800 px-4 py-3 shadow-sm flex items-start justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-zinc-900 p-1 rounded">
|
||||
<Package class="w-3.5 h-3.5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<Dialog.Title class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 leading-tight flex items-center gap-2">
|
||||
{#if isTargetingPreset}
|
||||
{isEditMode ? 'Editar Item de Plantilla' : 'Nuevo Item para Plantilla'}
|
||||
{:else}
|
||||
<Sheet.Root bind:open={open}>
|
||||
<Sheet.Content side="right" class="w-full sm:max-w-[95vw] lg:max-w-[85vw] xl:max-w-[75vw] p-0 flex flex-col h-full bg-slate-50 dark:bg-black">
|
||||
|
||||
<header class="bg-white dark:bg-zinc-950 border-b dark:border-zinc-800 px-3 py-1.5 shadow-sm shrink-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="bg-zinc-900 p-1 rounded">
|
||||
<Package class="w-3.5 h-3.5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<Sheet.Title class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 leading-tight">
|
||||
{isEditMode ? 'Editar Partida' : 'Nueva Partida - Activo Fijo'}
|
||||
{/if}
|
||||
{#if editingItem.lines && editingItem.lines.length > 1}
|
||||
<span class="px-1.5 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/30 text-[10px] text-blue-700 dark:text-blue-300 font-bold border border-blue-200 dark:border-blue-800">
|
||||
{editingItem.lines.length} lines
|
||||
</span>
|
||||
{/if}
|
||||
</Dialog.Title>
|
||||
{#if !isTargetingPreset}
|
||||
</Sheet.Title>
|
||||
<p class="text-[10px] text-muted-foreground flex items-center gap-1.5">
|
||||
Factura: <span class="font-medium text-zinc-700 dark:text-zinc-300">{invoice?.invoice_number || 'N/A'}</span>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-[10px] text-muted-foreground flex items-center gap-1.5">
|
||||
<span class="font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wider">Modo Plantilla</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
<Button variant="outline" size="sm" onclick={() => onCancel?.()} disabled={isSaving} class="h-7 text-xs px-2">
|
||||
Cancelar
|
||||
<Button variant="ghost" size="icon" onclick={() => onCancel?.()} class="h-7 w-7 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<X class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button size="sm" onclick={onSave} disabled={isSaving} class="h-7 text-xs px-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white">
|
||||
{#if isSaving}
|
||||
<Loader2 class="w-3 h-3 mr-1 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
<Save class="w-3 h-3 mr-1" />
|
||||
{isEditMode ? 'Actualizar' : 'Crear'}
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="p-3 overflow-y-auto max-h-[calc(90vh-88px)] bg-slate-50/70 dark:bg-black">
|
||||
<div class="flex-1 overflow-y-auto px-2 py-1.5">
|
||||
<div class="space-y-2">
|
||||
|
||||
{#if line}
|
||||
@@ -152,7 +101,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs.Root bind:value={activeTab} class="w-full">
|
||||
<Tabs.Root value="generales" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-5 bg-zinc-100 dark:bg-zinc-800/50 rounded p-0.5 gap-0.5">
|
||||
<Tabs.Trigger value="generales" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
|
||||
General
|
||||
@@ -216,5 +165,23 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<footer class="bg-white dark:bg-zinc-950 border-t border-zinc-200 dark:border-zinc-800 px-3 py-1.5 shadow-sm shrink-0">
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<Button variant="outline" size="sm" onclick={() => onCancel?.()} disabled={isSaving} class="h-7 text-xs px-2">
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button size="sm" onclick={onSave} disabled={isSaving} class="h-7 text-xs px-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white">
|
||||
{#if isSaving}
|
||||
<Loader2 class="w-3 h-3 mr-1 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
<Save class="w-3 h-3 mr-1" />
|
||||
{isEditMode ? 'Actualizar' : 'Crear'}
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
@@ -24,12 +24,11 @@
|
||||
|
||||
const filteredParts = $derived(
|
||||
searchQuery
|
||||
? parts.filter(
|
||||
(p) =>
|
||||
p.part_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description_spanish?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description_english?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
? parts.filter(p =>
|
||||
p.part_number?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description_spanish?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.description_english?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: parts
|
||||
);
|
||||
|
||||
@@ -53,12 +52,15 @@
|
||||
|
||||
isSearching = true;
|
||||
try {
|
||||
const response = await fetch(`/api-sveltekit/parts?company_id=${activeCompanyId}&limit=100`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
const response = await fetch(
|
||||
`/api-sveltekit/parts?company_id=${activeCompanyId}&limit=100`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Error al buscar números de parte');
|
||||
@@ -85,9 +87,8 @@
|
||||
function handleScroll(e: Event) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
const threshold = 100;
|
||||
const scrolledToBottom =
|
||||
target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||
|
||||
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||
|
||||
if (scrolledToBottom && displayedParts.length < filteredParts.length) {
|
||||
currentPage++;
|
||||
loadMoreParts();
|
||||
@@ -103,10 +104,12 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-5xl w-full max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Content class="!max-w-[50vw] w-[50vw] max-h-[80vh] flex flex-col">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Seleccionar Número de Parte</Dialog.Title>
|
||||
<Dialog.Description>Busca y selecciona un número de parte para la partida</Dialog.Description>
|
||||
<Dialog.Description>
|
||||
Busca y selecciona un número de parte para la partida
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex gap-2 mb-4">
|
||||
@@ -145,18 +148,15 @@
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each displayedParts as part}
|
||||
<Table.Row
|
||||
class="cursor-pointer hover:bg-muted/50"
|
||||
onclick={() => handleSelect(part)}
|
||||
>
|
||||
<Table.Row class="cursor-pointer hover:bg-muted/50" onclick={() => handleSelect(part)}>
|
||||
<Table.Cell class="font-medium">{part.part_number}</Table.Cell>
|
||||
<Table.Cell>{part.description_spanish || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-muted-foreground"
|
||||
>{part.description_english || '-'}</Table.Cell
|
||||
>
|
||||
<Table.Cell class="text-muted-foreground">{part.description_english || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-muted-foreground">{part.part_class || '-'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button variant="ghost" size="sm" class="h-8">Seleccionar</Button>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
Seleccionar
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
import type { LineFinancials, LineQuantities } from '$lib/api/dashboard/a76/items';
|
||||
|
||||
let { financials = $bindable(), quantities = $bindable() }: { financials: LineFinancials; quantities: LineQuantities } = $props();
|
||||
|
||||
// Helper function to safely format numbers
|
||||
function formatNumber(value: any, decimals: number = 8): string {
|
||||
const num = Number(value);
|
||||
return isNaN(num) ? '0.00000000' : num.toFixed(decimals);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -10,18 +16,18 @@
|
||||
|
||||
<div class="text-xs font-semibold">RETURN QUANTITY SUB-ITEMS</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>Temporary: <span class="text-gray-900 dark:text-gray-100">{quantities.quantity_temp_export?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Temporary: <span class="text-gray-900 dark:text-gray-100">{formatNumber(quantities.quantity_temp_export)}</span></div>
|
||||
<div>Replacement or Change: <span class="text-gray-900 dark:text-gray-100">0.00000000</span></div>
|
||||
<div>Definitive: <span class="text-gray-900 dark:text-gray-100">{quantities.quantity_returned?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Returned Values: <span class="text-gray-900 dark:text-gray-100">{financials.value_returned_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div class="col-span-2">Returned Values: <span class="text-gray-900 dark:text-gray-100">{financials.value_returned_mxn?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Definitive: <span class="text-gray-900 dark:text-gray-100">{formatNumber(quantities.quantity_returned)}</span></div>
|
||||
<div>Returned Values: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_returned_usd)}</span></div>
|
||||
<div class="col-span-2">Returned Values: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_returned_mxn)}</span></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs pt-2 border-t">
|
||||
<div class="font-semibold">WEIGHTS (KILOS)</div>
|
||||
<div class="font-semibold">WEIGHTS (Pounds)</div>
|
||||
<div>Net: <span class="text-gray-900 dark:text-gray-100">{quantities.net_weight?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Net: <span class="text-gray-900 dark:text-gray-100">{formatNumber(quantities.net_weight)}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">0.00000000</span></div>
|
||||
<div>Whole: <span class="text-gray-900 dark:text-gray-100">{quantities.gross_weight?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Whole: <span class="text-gray-900 dark:text-gray-100">{formatNumber(quantities.gross_weight)}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">0.00000000</span></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -33,13 +39,13 @@
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div class="font-semibold">(Dollars)</div>
|
||||
<div class="font-semibold">(Pesos)</div>
|
||||
<div>Cost: <span class="text-gray-900 dark:text-gray-100">{financials.unit_cost_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">{financials.unit_cost_mxn?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div>Value: <span class="text-gray-900 dark:text-gray-100">{financials.value_usd?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">{financials.value_mxn?.toFixed(8) || '0.00000000'}</span></div>
|
||||
<div class="text-xs">Capture Cost: <span class="text-gray-900 dark:text-gray-100">{financials.unit_cost_capture?.toFixed(8) || '0.00000000'}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
<div class="text-xs">Capture Value: <span class="text-gray-900 dark:text-gray-100">{financials.value_usd?.toFixed(8) || '0.00000000'}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
<div class="text-xs">Customs Value: <span class="text-gray-900 dark:text-gray-100">{financials.customs_value_usd?.toFixed(8) || '0.00000000'}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
<div>Cost: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.unit_cost_usd)}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.unit_cost_mxn)}</span></div>
|
||||
<div>Value: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_usd)}</span></div>
|
||||
<div><span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_mxn)}</span></div>
|
||||
<div class="text-xs">Capture Cost: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.unit_cost_capture)}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
<div class="text-xs">Capture Value: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.value_usd)}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
<div class="text-xs">Customs Value: <span class="text-gray-900 dark:text-gray-100">{formatNumber(financials.customs_value_usd)}</span> <span class="text-gray-900 dark:text-gray-100">USD</span></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
let loadingMore = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
|
||||
// Pagination state
|
||||
let currentPage = $state(1);
|
||||
let totalPages = $state(1);
|
||||
@@ -34,7 +34,7 @@
|
||||
loadingMore = true;
|
||||
}
|
||||
error = '';
|
||||
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
@@ -45,10 +45,10 @@
|
||||
const response = await fetch(`/api-sveltekit/tariff-fractions?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.items && Array.isArray(data.items)) {
|
||||
if (append) {
|
||||
fractions = [...fractions, ...data.items];
|
||||
@@ -77,10 +77,10 @@
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
if (!scrollContainer || loading || loadingMore || !hasMore) return;
|
||||
|
||||
|
||||
const target = e.target as HTMLDivElement;
|
||||
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
||||
|
||||
|
||||
// Load more when within 200px of bottom
|
||||
if (scrollBottom < 200) {
|
||||
loadFractions(currentPage + 1, true);
|
||||
@@ -118,7 +118,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-4xl w-full max-h-[80vh] flex flex-col px-6">
|
||||
<Dialog.Content class="!max-w-[80vw] w-[80vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">FRACCIONES ARANCELARIAS</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -137,7 +137,11 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div bind:this={scrollContainer} onscroll={handleScroll} class="flex-1 overflow-auto px-6 py-4">
|
||||
<div
|
||||
bind:this={scrollContainer}
|
||||
onscroll={handleScroll}
|
||||
class="flex-1 overflow-auto px-6 py-4"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
@@ -151,12 +155,18 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white sticky top-0">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Código</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Fracción</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Código</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Fracción</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">NICO</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>NICO</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold">UMT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -192,7 +202,9 @@
|
||||
{/if}
|
||||
|
||||
{#if !hasMore && fractions.length > 0}
|
||||
<div class="text-center py-4 text-sm text-zinc-500">Todos los resultados cargados</div>
|
||||
<div class="text-center py-4 text-sm text-zinc-500">
|
||||
Todos los resultados cargados
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-4xl w-full max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Content class="!max-w-[70vw] w-[70vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGOS DE UNIDADES DE MEDIDA</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
@@ -91,7 +91,11 @@
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input bind:value={searchTerm} placeholder="Buscando..." class="flex-1 h-9" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,7 +113,9 @@
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">U.M.</th>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>U.M.</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción Español</th
|
||||
>
|
||||
@@ -150,9 +156,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400"
|
||||
>
|
||||
<div class="mt-4 flex items-center justify-between text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="px-3 py-1 border rounded hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
||||
<<
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as Sheet from '$lib/components/ui/sheet';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
@@ -60,25 +60,29 @@
|
||||
// Initialize missing nested objects if they don't exist
|
||||
$effect(() => {
|
||||
if (open && editingItem) {
|
||||
if (!editingItem.lines) editingItem.lines = [{}];
|
||||
if (!editingItem.lines[0].quantity) editingItem.lines[0].quantity = {};
|
||||
if (!editingItem.lines[0].financial) editingItem.lines[0].financial = {};
|
||||
if (!editingItem.lines[0].customs) editingItem.lines[0].customs = {};
|
||||
if (!editingItem.lines[0].description) editingItem.lines[0].description = {};
|
||||
if (!editingItem.lines) editingItem.lines = [{ line_number: 1 } as any];
|
||||
if (editingItem.lines[0] && !editingItem.lines[0].quantity)
|
||||
editingItem.lines[0].quantity = {} as any;
|
||||
if (editingItem.lines[0] && !editingItem.lines[0].financial)
|
||||
editingItem.lines[0].financial = {} as any;
|
||||
if (editingItem.lines[0] && !editingItem.lines[0].customs)
|
||||
editingItem.lines[0].customs = {} as any;
|
||||
if (editingItem.lines[0] && !editingItem.lines[0].description)
|
||||
editingItem.lines[0].description = {} as any;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-5xl max-h-[90vh] p-0 overflow-hidden z-[100] [&>button]:hidden">
|
||||
<Sheet.Root bind:open>
|
||||
<Sheet.Content side="right" class="w-[50vw] overflow-hidden p-0 sm:max-w-none">
|
||||
<div
|
||||
class="px-6 py-4 border-b bg-white dark:bg-zinc-950 flex items-start justify-between gap-3"
|
||||
class="flex items-start justify-between gap-3 border-b bg-white px-6 py-4 dark:bg-zinc-950"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<Dialog.Title class="text-lg font-semibold">
|
||||
<Sheet.Title class="text-lg font-semibold">
|
||||
{isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - SCAII (Inventario)
|
||||
</Dialog.Title>
|
||||
<Dialog.Description class="text-sm text-muted-foreground">
|
||||
</Sheet.Title>
|
||||
<Sheet.Description class="text-sm text-muted-foreground">
|
||||
{isEditMode
|
||||
? 'Modifica los campos del inventario y guarda los cambios.'
|
||||
: 'Completa la información del nuevo item de inventario.'}
|
||||
@@ -87,13 +91,13 @@
|
||||
{editingItem.lines.length} items en esta partida
|
||||
</Badge>
|
||||
{/if}
|
||||
</Dialog.Description>
|
||||
</Sheet.Description>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" onclick={() => onCancel?.()} disabled={isSaving}>Cancelar</Button>
|
||||
<Button onclick={onSave} disabled={isSaving}>
|
||||
{#if isSaving}
|
||||
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
{isEditMode ? 'Guardar Cambios' : 'Agregar Item'}
|
||||
@@ -102,7 +106,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 overflow-auto max-h-[calc(90vh-96px)] bg-slate-50/60 dark:bg-black">
|
||||
<div class="max-h-[calc(90vh-96px)] overflow-auto bg-slate-50/60 p-6 dark:bg-black">
|
||||
<Tabs.Root bind:value={activeTab} class="mt-0">
|
||||
<Tabs.List class="grid w-full grid-cols-4">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
@@ -112,13 +116,13 @@
|
||||
</Tabs.List>
|
||||
|
||||
<!-- Tab: General -->
|
||||
<Tabs.Content value="general" class="space-y-4 mt-4">
|
||||
<Tabs.Content value="general" class="mt-4 space-y-4">
|
||||
<!-- Información de la Factura (Solo lectura) -->
|
||||
{#if !isTargetingPreset}
|
||||
<div class="rounded-lg border bg-muted/50 p-4 space-y-3">
|
||||
<div class="space-y-3 rounded-lg border bg-muted/50 p-4">
|
||||
<h4 class="text-sm font-medium">Información de la Factura (SCAII - Inventario)</h4>
|
||||
{#if !invoice?.id}
|
||||
<div class="text-sm text-amber-600 bg-amber-50 dark:bg-amber-950/20 p-3 rounded">
|
||||
<div class="rounded bg-amber-50 p-3 text-sm text-amber-600 dark:bg-amber-950/20">
|
||||
⚠️ Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la
|
||||
factura.
|
||||
</div>
|
||||
@@ -139,7 +143,7 @@
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<span class="text-muted-foreground">Sistema:</span>
|
||||
<span class="ml-2 font-medium bg-blue-100 dark:bg-blue-900/30 px-2 py-1 rounded"
|
||||
<span class="ml-2 rounded bg-blue-100 px-2 py-1 font-medium dark:bg-blue-900/30"
|
||||
>SCAII (Inventory)</span
|
||||
>
|
||||
</div>
|
||||
@@ -207,7 +211,7 @@
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Clasificación -->
|
||||
<Tabs.Content value="clasificacion" class="space-y-4 mt-4">
|
||||
<Tabs.Content value="clasificacion" class="mt-4 space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="tariff_fraction">Fracción Arancelaria</Label>
|
||||
@@ -223,21 +227,21 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="product_type">Tipo de Producto</Label>
|
||||
{#if line}
|
||||
{#if line?.description}
|
||||
<Input
|
||||
id="product_type"
|
||||
placeholder="Materia prima, producto terminado, etc."
|
||||
bind:value={line.description.extra_description_2}
|
||||
bind:value={(line.description as any).extra_description_2}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="material_type">Tipo de Material</Label>
|
||||
{#if line}
|
||||
{#if line?.description}
|
||||
<Input
|
||||
id="material_type"
|
||||
placeholder="Metal, plástico, etc."
|
||||
bind:value={line.description.extra_description_3}
|
||||
bind:value={(line.description as any).extra_description_3}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -262,7 +266,7 @@
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Cantidades -->
|
||||
<Tabs.Content value="cantidades" class="space-y-4 mt-4">
|
||||
<Tabs.Content value="cantidades" class="mt-4 space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
@@ -350,7 +354,7 @@
|
||||
id="packages"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
bind:value={line.quantity.packages}
|
||||
bind:value={(line.quantity as any).packages}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -360,7 +364,7 @@
|
||||
<Input
|
||||
id="package_type"
|
||||
placeholder="Caja, pallet, etc."
|
||||
bind:value={line.quantity.package_type}
|
||||
bind:value={(line.quantity as any).package_type}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -374,7 +378,7 @@
|
||||
id="imported_quantity"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
bind:value={line.quantity.quantity_imported}
|
||||
bind:value={(line.quantity as any).quantity_imported}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -385,7 +389,8 @@
|
||||
id="remaining_quantity"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={(line.quantity.quantity || 0) - (line.quantity.quantity_imported || 0)}
|
||||
value={(line.quantity.quantity || 0) -
|
||||
((line.quantity as any).quantity_imported || 0)}
|
||||
disabled
|
||||
/>
|
||||
{/if}
|
||||
@@ -395,7 +400,7 @@
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Otros -->
|
||||
<Tabs.Content value="otros" class="space-y-4 mt-4">
|
||||
<Tabs.Content value="otros" class="mt-4 space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="brand">Marca</Label>
|
||||
@@ -435,7 +440,7 @@
|
||||
{#if line?.description}
|
||||
<textarea
|
||||
id="observations"
|
||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-background 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 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="Notas adicionales sobre el inventario..."
|
||||
bind:value={line.description.extra_description}
|
||||
></textarea>
|
||||
@@ -445,5 +450,5 @@
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
|
||||
@@ -120,7 +120,7 @@ class CompanyStore {
|
||||
* @param company - La compañía a establecer como activa
|
||||
* @param silent - Si es true, no dispara el evento companyChanged (para inicialización)
|
||||
*/
|
||||
setActiveCompany(company: Company, silent: boolean = false) {
|
||||
async setActiveCompany(company: Company, silent: boolean = false) {
|
||||
const previousCompanyId = this._activeCompany?.id;
|
||||
this._activeCompany = company;
|
||||
|
||||
@@ -130,8 +130,20 @@ class CompanyStore {
|
||||
}
|
||||
|
||||
// Guardar en cookie para acceso desde el servidor (SSR)
|
||||
if (typeof document !== 'undefined') {
|
||||
document.cookie = `active_company_id=${company.id}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
|
||||
// Usar el endpoint del servidor para garantizar que la cookie esté disponible en SSR
|
||||
if (browser) {
|
||||
try {
|
||||
await fetch('/api-sveltekit/company/set-active', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ companyId: company.id }),
|
||||
credentials: 'include'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting active company cookie:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Despachar evento personalizado solo si:
|
||||
|
||||
@@ -36,7 +36,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
|
||||
try {
|
||||
const fetchUrl = `${baseUrl}v1/a76/classes?${queryString}`;
|
||||
console.log('Fetching classes from:', fetchUrl);
|
||||
|
||||
const response = await fetch(
|
||||
fetchUrl,
|
||||
|
||||
@@ -32,7 +32,6 @@ export const GET: RequestHandler = async ({ cookies, url, params }) => {
|
||||
|
||||
try {
|
||||
const fetchUrl = `${baseUrl}v1/a76/classes/${id}?company_id=${companyId}`;
|
||||
console.log('Fetching class from:', fetchUrl);
|
||||
|
||||
const response = await fetch(
|
||||
fetchUrl,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* API route para establecer la compañía activa en una cookie
|
||||
*/
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ cookies, request }) => {
|
||||
try {
|
||||
const { companyId } = await request.json();
|
||||
|
||||
if (!companyId || typeof companyId !== 'number') {
|
||||
return json({ error: 'Invalid company ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Establecer la cookie desde el servidor
|
||||
cookies.set('active_company_id', companyId.toString(), {
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 días
|
||||
sameSite: 'lax',
|
||||
httpOnly: false, // Permitir acceso desde JavaScript
|
||||
secure: process.env.NODE_ENV === 'production'
|
||||
});
|
||||
|
||||
return json({ success: true, companyId });
|
||||
} catch (error) {
|
||||
console.error('Error setting active company:', error);
|
||||
return json({ error: 'Internal server error' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -35,8 +35,7 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
try {
|
||||
const fetchUrl = `${baseUrl}v1/a76/parts?${queryString}`;
|
||||
console.log('Fetching parts from:', fetchUrl);
|
||||
const fetchUrl = `${baseUrl}v1/a76/parts?${queryString}`;
|
||||
|
||||
const response = await fetch(
|
||||
fetchUrl,
|
||||
|
||||
@@ -31,8 +31,7 @@ export const GET: RequestHandler = async ({ cookies, url, params }) => {
|
||||
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
||||
|
||||
try {
|
||||
const fetchUrl = `${baseUrl}v1/a76/parts/${id}?company_id=${companyId}`;
|
||||
console.log('Fetching part from:', fetchUrl);
|
||||
const fetchUrl = `${baseUrl}v1/a76/parts/${id}?company_id=${companyId}`;
|
||||
|
||||
const response = await fetch(
|
||||
fetchUrl,
|
||||
|
||||
@@ -20,8 +20,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
|
||||
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,
|
||||
@@ -35,8 +33,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
);
|
||||
|
||||
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), {
|
||||
|
||||
@@ -36,8 +36,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
|
||||
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,
|
||||
@@ -51,8 +49,6 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
);
|
||||
|
||||
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), {
|
||||
|
||||
@@ -31,8 +31,7 @@ export const GET: RequestHandler = async ({ cookies, params, url }) => {
|
||||
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
||||
|
||||
try {
|
||||
const fetchUrl = `${baseUrl}v1/a76/units-of-measure/${id}?company_id=${companyId}`;
|
||||
console.log('Fetching unit of measure from:', fetchUrl);
|
||||
const fetchUrl = `${baseUrl}v1/a76/units-of-measure/${id}?company_id=${companyId}`;
|
||||
|
||||
const response = await fetch(
|
||||
fetchUrl,
|
||||
|
||||
@@ -187,8 +187,7 @@
|
||||
selectedInvoiceId = null;
|
||||
} else {
|
||||
selectedInvoiceId = invoice.id;
|
||||
}
|
||||
console.log('Selected Invoice ID:', selectedInvoiceId);
|
||||
}
|
||||
}
|
||||
|
||||
const selectedInvoice = $derived(
|
||||
|
||||
@@ -222,29 +222,199 @@
|
||||
return { ...skeleton, ...filtered };
|
||||
}
|
||||
|
||||
// Función para mapear la factura existente a los formData
|
||||
function mapInvoiceToTopFields(invoice: any) {
|
||||
if (!invoice) return topFieldsSkeleton;
|
||||
|
||||
let operationType: string | null = null;
|
||||
if (invoice.operation_type) {
|
||||
operationType = invoice.operation_type;
|
||||
} else if (data.filters?.operation_type !== undefined) {
|
||||
operationType = data.filters.operation_type ?? null;
|
||||
}
|
||||
|
||||
return {
|
||||
is_pedimento_pending: invoice.compliance_mx?.is_pedimento_pending || false,
|
||||
pedimento_id: invoice.compliance_mx?.pedimento_id || '',
|
||||
remesa: invoice.compliance_mx?.remesa || '',
|
||||
invoice_number: invoice.invoice_number || '',
|
||||
invoice_date: invoice.invoice_date || new Date().toISOString().split('T')[0],
|
||||
emission_date: invoice.emission_date || new Date().toISOString().split('T')[0],
|
||||
operation_type: operationType,
|
||||
invoice_type: invoice.invoice_type || (data.filters?.invoice_type ?? ''),
|
||||
fecha_pedimento_del: '',
|
||||
fecha_pedimento_al: '',
|
||||
clave_pedimento: '',
|
||||
regimen_pedimento: ''
|
||||
};
|
||||
}
|
||||
|
||||
function mapInvoiceToGeneral(invoice: any) {
|
||||
if (!invoice) return generalSkeleton;
|
||||
|
||||
return {
|
||||
provider_header: invoice.compliance_mx?.provider_header || 'proveedor',
|
||||
provider_id: invoice.compliance_mx?.provider_id || null,
|
||||
sold_to_header: invoice.compliance_mx?.sold_to_header || 'consignado_a',
|
||||
sold_to_id: invoice.compliance_mx?.sold_to_id || null,
|
||||
shipped_to_header: invoice.compliance_mx?.shipped_to_header || 'enviado_a',
|
||||
shipped_to_id: invoice.compliance_mx?.shipped_to_id || null,
|
||||
customs_broker_id: invoice.compliance_mx?.customs_broker_id || null,
|
||||
customs_broker_us_id: invoice.compliance_mx?.customs_broker_us_id || null,
|
||||
currency_type: invoice.financials?.currency_type || '',
|
||||
currency: invoice.financials?.currency || 'foreign',
|
||||
exchange_rate: invoice.financials?.exchange_rate || null,
|
||||
weight_type: 'kgs',
|
||||
iva_factor: invoice.financials?.iva_factor || null,
|
||||
carrier_id: invoice.logistics?.carrier_id || null,
|
||||
transport_id: invoice.logistics?.transport_id || '',
|
||||
driver_name: invoice.logistics?.driver_name || '',
|
||||
transport_type: invoice.logistics?.transport_type || '',
|
||||
transport_num: invoice.logistics?.vehicle_num || '',
|
||||
aduana: invoice.compliance_mx?.aduana || '',
|
||||
document_type: invoice.document_type || ''
|
||||
};
|
||||
}
|
||||
|
||||
function mapInvoiceToObservations(invoice: any) {
|
||||
if (!invoice) return observationSkeleton;
|
||||
|
||||
return {
|
||||
observation_es: invoice.observation_es || '',
|
||||
observation_en: invoice.observation_en || '',
|
||||
freight: invoice.financials?.freight || null,
|
||||
insurance_value: invoice.financials?.insurance_value || null,
|
||||
insurance: invoice.financials?.insurance || null,
|
||||
packaging: invoice.financials?.packaging || null,
|
||||
other_increments: invoice.financials?.other_increments || null,
|
||||
total_increments_mn: invoice.financials?.total_increments_mn || null,
|
||||
total_increments_me: invoice.financials?.total_increments_me || null,
|
||||
incoterm: invoice.logistics?.incoterm || null,
|
||||
enclosure: invoice.compliance_mx?.enclosure || null,
|
||||
num_seals: null,
|
||||
movement_type: invoice.compliance_mx?.movement_type || '',
|
||||
alternate_invoice: invoice.alternate_invoice || '',
|
||||
valuation_method: invoice.compliance_mx?.value_method || null
|
||||
};
|
||||
}
|
||||
|
||||
function mapInvoiceToItems(invoice: any) {
|
||||
if (!invoice) return ensureItemsFormData(null);
|
||||
|
||||
return {
|
||||
items: invoice.items || []
|
||||
};
|
||||
}
|
||||
|
||||
function mapInvoiceToOthers(invoice: any) {
|
||||
if (!invoice) return othersSkeleton;
|
||||
|
||||
return {
|
||||
comments_status: invoice.comments_status || '',
|
||||
transport_mode: invoice.logistics?.transport_mode || 'TRUCK',
|
||||
is_mixed: invoice.compliance_mx?.is_mixed || false,
|
||||
print_stamp: invoice.print_stamp || false,
|
||||
rule_3121_parties_ii: invoice.compliance_mx?.rule_3121_parties_ii || false,
|
||||
related_doc_id: invoice.related_doc_id || null,
|
||||
code_signature: invoice.compliance_mx?.code_signature || '',
|
||||
electronic_signature: invoice.compliance_mx?.electronic_signature || '',
|
||||
mandatory_person: invoice.compliance_mx?.mandatory_person || '',
|
||||
contingency_mode: invoice.compliance_mx?.contingency_mode || false,
|
||||
cove: invoice.compliance_mx?.cove || '',
|
||||
operation_num: invoice.compliance_mx?.operation_num || '',
|
||||
adendas: invoice.compliance_mx?.adendas || '',
|
||||
observations_vu: invoice.compliance_mx?.observations_vu || '',
|
||||
certified_number: invoice.compliance_mx?.certified_number || '',
|
||||
bill_number: invoice.logistics?.bill_number || '',
|
||||
guide_number: invoice.logistics?.guide_number || '',
|
||||
shipment_number: invoice.logistics?.shipment_number || '',
|
||||
option_iv18: invoice.compliance_mx?.option_iv18 || '',
|
||||
delivered_status: invoice.delivered_status || false,
|
||||
received_by: invoice.received_by || '',
|
||||
delivery_date: invoice.delivery_date || ''
|
||||
};
|
||||
}
|
||||
|
||||
function mapInvoiceToContinuation(invoice: any) {
|
||||
if (!invoice) return continuationSkeleton;
|
||||
|
||||
return {
|
||||
numero_tipo_transporte: invoice.logistics?.numero_tipo_transporte || '',
|
||||
es_ferrocarril: invoice.logistics?.es_ferrocarril || 'no',
|
||||
numero_bl: invoice.logistics?.numero_bl || '',
|
||||
cantidad_guias_embarque: invoice.logistics?.cantidad_guias_embarque || null,
|
||||
destino_origen: invoice.logistics?.destino_origen || '',
|
||||
puerto_entrada: invoice.logistics?.puerto_entrada || '',
|
||||
vehicle_data: invoice.logistics?.vehicle_data || '',
|
||||
fue_revisado_equipo: invoice.logistics?.fue_revisado_equipo || false,
|
||||
sub_division: invoice.compliance_mx?.subdivision || false,
|
||||
funge_como_cd: invoice.logistics?.acts_as_cd || false,
|
||||
llego_pedimento: invoice.compliance_mx?.llego_pedimento || false,
|
||||
errores_facturacion: invoice.errores_facturacion || [],
|
||||
semaforo_verde_aduana_mexicana: invoice.compliance_mx?.semaforo_verde_aduana_mexicana || false,
|
||||
semaforo_verde_aduana_americana: invoice.compliance_mx?.semaforo_verde_aduana_americana || false,
|
||||
semaforo_rojo_aduana_mexicana: invoice.compliance_mx?.semaforo_rojo_aduana_mexicana || false,
|
||||
semaforo_rojo_aduana_americana: invoice.compliance_mx?.semaforo_rojo_aduana_americana || false,
|
||||
is_mixed: invoice.compliance_mx?.is_mixed || false,
|
||||
reason_export: invoice.compliance_mx?.reason_export || '1',
|
||||
purchase_order: invoice.purchase_order || '',
|
||||
payment_terms: invoice.payment_terms || '',
|
||||
handling_fees: invoice.financials?.handling_fees || 0,
|
||||
cfdi_uuid: invoice.cfdi_uuid || '',
|
||||
path_pdf: invoice.path_pdf || '',
|
||||
path_xml: invoice.path_xml || ''
|
||||
};
|
||||
}
|
||||
|
||||
// Referencias a los componentes de formulario para obtener sus datos
|
||||
// Si estamos en modo edición (!data.isCreate) y tenemos una factura, usarla
|
||||
// Si estamos en modo creación, usar defaultSettings
|
||||
let InvoiceTopFieldsFormData = $state<any>(
|
||||
mergeDefaults(topFieldsSkeleton, data.defaultSettings?.InvoiceTopFieldsFormData)
|
||||
!data.isCreate && data.invoice
|
||||
? mapInvoiceToTopFields(data.invoice)
|
||||
: mergeDefaults(topFieldsSkeleton, data.defaultSettings?.InvoiceTopFieldsFormData)
|
||||
);
|
||||
let generalFormData = $state<any>(
|
||||
mergeDefaults(generalSkeleton, data.defaultSettings?.generalFormData)
|
||||
!data.isCreate && data.invoice
|
||||
? mapInvoiceToGeneral(data.invoice)
|
||||
: mergeDefaults(generalSkeleton, data.defaultSettings?.generalFormData)
|
||||
);
|
||||
let observationFormData = $state<any>(
|
||||
mergeDefaults(observationSkeleton, data.defaultSettings?.observationFormData)
|
||||
!data.isCreate && data.invoice
|
||||
? mapInvoiceToObservations(data.invoice)
|
||||
: mergeDefaults(observationSkeleton, data.defaultSettings?.observationFormData)
|
||||
);
|
||||
let itemsFormData = $state<any>(
|
||||
!data.isCreate && data.invoice
|
||||
? mapInvoiceToItems(data.invoice)
|
||||
: ensureItemsFormData(data.defaultSettings?.itemsFormData)
|
||||
);
|
||||
let itemsFormData = $state<any>(ensureItemsFormData(data.defaultSettings?.itemsFormData));
|
||||
let othersFormData = $state<any>(
|
||||
mergeDefaults(othersSkeleton, data.defaultSettings?.othersFormData)
|
||||
!data.isCreate && data.invoice
|
||||
? mapInvoiceToOthers(data.invoice)
|
||||
: mergeDefaults(othersSkeleton, data.defaultSettings?.othersFormData)
|
||||
);
|
||||
let continuationFormData = $state<any>(
|
||||
mergeDefaults(continuationSkeleton, data.defaultSettings?.continuationFormData)
|
||||
!data.isCreate && data.invoice
|
||||
? mapInvoiceToContinuation(data.invoice)
|
||||
: mergeDefaults(continuationSkeleton, data.defaultSettings?.continuationFormData)
|
||||
);
|
||||
|
||||
// Estados para saber si existen datos previos
|
||||
let observationExists = $state(!!data.defaultSettings?.observationFormData);
|
||||
let itemsExists = $state(!!data.defaultSettings?.itemsFormData?.items?.length);
|
||||
let othersExists = $state(!!data.defaultSettings?.othersFormData);
|
||||
let continuationExists = $state(!!data.defaultSettings?.continuationFormData);
|
||||
let observationExists = $state(
|
||||
!data.isCreate ? !!data.invoice : !!data.defaultSettings?.observationFormData
|
||||
);
|
||||
let itemsExists = $state(
|
||||
!data.isCreate
|
||||
? !!(data.invoice?.items && data.invoice.items.length > 0)
|
||||
: !!data.defaultSettings?.itemsFormData?.items?.length
|
||||
);
|
||||
let othersExists = $state(
|
||||
!data.isCreate ? !!data.invoice : !!data.defaultSettings?.othersFormData
|
||||
);
|
||||
let continuationExists = $state(
|
||||
!data.isCreate ? !!data.invoice : !!data.defaultSettings?.continuationFormData
|
||||
);
|
||||
|
||||
let calculatedExchangeRate = $state<number | null>(
|
||||
data.invoice?.financials?.exchange_rate ?? null
|
||||
@@ -314,8 +484,7 @@
|
||||
const actualResponse = response as any;
|
||||
const items = actualResponse.data?.items || [];
|
||||
|
||||
if (items.length === 0) {
|
||||
console.log('No exchange rate found for', date);
|
||||
if (items.length === 0) {
|
||||
if (!uiStore.isExchangeRateDialogOpen) {
|
||||
missingExchangeRateDate = date;
|
||||
showExchangeRateDialog = true;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Dialog } from "bits-ui";
|
||||
console.log("Dialog is:", Dialog);
|
||||
try {
|
||||
console.log("Dialog.Root is:", Dialog.Root);
|
||||
} catch (e) {
|
||||
console.log("Error accessing Dialog.Root:", e.message);
|
||||
}
|
||||
Reference in New Issue
Block a user