Partidas por tipo de factura en importacion

This commit is contained in:
2026-03-13 17:38:30 -05:00
parent dfa44aaff7
commit dada17b54c
15 changed files with 1015 additions and 56 deletions

View File

@@ -32,6 +32,7 @@ class IdentifierDetailBase(BaseModel):
part_line: Optional[int] = Field(None, description="Part Line") part_line: Optional[int] = Field(None, description="Part Line")
identifier_code: Optional[str] = Field( identifier_code: Optional[str] = Field(
None, max_length=2, description="Identifier Code") None, max_length=2, description="Identifier Code")
item_line_id: Optional[int] = Field(None, description="Item Line ID")
module: Optional[str] = Field(None, max_length=20, description="Module") module: Optional[str] = Field(None, max_length=20, description="Module")
complement1: Optional[str] = Field( complement1: Optional[str] = Field(
None, max_length=50, description="Complement 1") None, max_length=50, description="Complement 1")

View File

@@ -1,10 +1,13 @@
from typing import Optional from typing import Optional, TYPE_CHECKING
from sqlalchemy import Integer, String, UniqueConstraint, ForeignKey from sqlalchemy import Integer, String, UniqueConstraint, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.items.models import LineItem
class Identifier(Base, TenantScopedMixin, TimestampMixin): class Identifier(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "identifiers" __tablename__ = "identifiers"
__table_args__ = ( __table_args__ = (
@@ -44,6 +47,8 @@ class IdentifierDetail(Base, TenantScopedMixin, TimestampMixin):
Integer, nullable=True) # LINEAPARTIDA Integer, nullable=True) # LINEAPARTIDA
identifier_code: Mapped[Optional[str]] = mapped_column( identifier_code: Mapped[Optional[str]] = mapped_column(
String(2), ForeignKey("a76.identifiers.code"), nullable=True) # ID String(2), ForeignKey("a76.identifiers.code"), nullable=True) # ID
item_line_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("a76.item_lines.id"), nullable=True)
module: Mapped[Optional[str]] = mapped_column( module: Mapped[Optional[str]] = mapped_column(
String(20), nullable=True) # MODULO String(20), nullable=True) # MODULO
complement1: Mapped[Optional[str]] = mapped_column( complement1: Mapped[Optional[str]] = mapped_column(
@@ -54,3 +59,4 @@ class IdentifierDetail(Base, TenantScopedMixin, TimestampMixin):
String(50), nullable=True) # COMPLEMENTO3 String(50), nullable=True) # COMPLEMENTO3
identifier: Mapped["Identifier"] = relationship(back_populates="details") identifier: Mapped["Identifier"] = relationship(back_populates="details")
line: Mapped[Optional["LineItem"]] = relationship(back_populates="identifiers")

View File

@@ -4,7 +4,7 @@ SQLAlchemy v2 - Annex 24 Compliance
""" """
from datetime import datetime from datetime import datetime
from typing import Optional, TYPE_CHECKING from typing import Optional, List, TYPE_CHECKING
from core.database import Base from core.database import Base
from decimal import Decimal from decimal import Decimal
from sqlalchemy import Boolean, Date, String, Integer, Numeric, SmallInteger, ForeignKey from sqlalchemy import Boolean, Date, String, Integer, Numeric, SmallInteger, ForeignKey
@@ -24,6 +24,7 @@ if TYPE_CHECKING:
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
from api.v1.modules.a76.parts.models import Part from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a76.invoices.models import InvoiceHeader from api.v1.modules.a76.invoices.models import InvoiceHeader
from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail
# ============================================================================ # ============================================================================
# CORE ENTITIES # CORE ENTITIES
@@ -216,10 +217,14 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
) )
fa_data: Mapped[Optional["FaLineItem"]] = relationship( fa_data: Mapped[Optional["FaLineItem"]] = relationship(
"FaLineItem", "FaLineItem",
back_populates="master_info",
cascade="all, delete-orphan", cascade="all, delete-orphan",
uselist=False, uselist=False,
) )
identifiers: Mapped[List["IdentifierDetail"]] = relationship(
"IdentifierDetail",
back_populates="line",
cascade="all, delete-orphan",
)
part_info: Mapped[Optional["Part"]] = relationship( part_info: Mapped[Optional["Part"]] = relationship(
"Part", "Part",
foreign_keys=[part_number_id], foreign_keys=[part_number_id],

View File

@@ -37,6 +37,12 @@ from .line_references.schemas import (
LineReferenceResponse, LineReferenceResponse,
) )
from api.v1.modules.a76.general_catalogs.identifiers.dto import (
IdentifierDetailCreate,
IdentifierDetailUpdate,
IdentifierDetailResponse,
)
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a24.fa.fa_item_lines.dto import ( from api.v1.modules.a24.fa.fa_item_lines.dto import (
FaLineItemCreateDTO, FaLineItemCreateDTO,
FaLineItemUpdateDTO, FaLineItemUpdateDTO,
@@ -238,6 +244,9 @@ class LineItemCreate(LineItemBase):
series: Optional[list[SerieCreate]] = Field( series: Optional[list[SerieCreate]] = Field(
None, description="Series data for this line (multiple per line)" None, description="Series data for this line (multiple per line)"
) )
identifiers: Optional[list[IdentifierDetailCreate]] = Field(
None, description="Identifiers for this line"
)
class LineItemUpdate(LineItemBase): class LineItemUpdate(LineItemBase):
@@ -267,6 +276,9 @@ class LineItemUpdate(LineItemBase):
series: Optional[list[SerieUpdate]] = Field( series: Optional[list[SerieUpdate]] = Field(
None, description="Series data for this line (replace all)" None, description="Series data for this line (replace all)"
) )
identifiers: Optional[list[IdentifierDetailUpdate]] = Field(
None, description="Identifiers for this line"
)
class LineItemResponse(LineItemBase): class LineItemResponse(LineItemBase):
@@ -295,6 +307,7 @@ class LineItemResponse(LineItemBase):
reference: Optional[LineReferenceResponse] = None reference: Optional[LineReferenceResponse] = None
fa_data: Optional[FaLineItemResponseDTO] = None fa_data: Optional[FaLineItemResponseDTO] = None
series: Optional[list[SerieResponse]] = None series: Optional[list[SerieResponse]] = None
identifiers: Optional[list[IdentifierDetailResponse]] = None
# Fields populated from relationships # Fields populated from relationships
class_code: Optional[str] = None class_code: Optional[str] = None

View File

@@ -19,6 +19,9 @@ class Serie(Base, TenantScopedMixin, TimestampMixin):
brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA
expo_brad: Mapped[Optional[str]] = mapped_column(String(50)) # MARCAEXPO expo_brad: Mapped[Optional[str]] = mapped_column(String(50)) # MARCAEXPO
number_id: Mapped[Optional[str]] = mapped_column(String(25)) # NUMIDEXPO number_id: Mapped[Optional[str]] = mapped_column(String(25)) # NUMIDEXPO
import_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURAIMPO
import_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEAIMPO
image_path: Mapped[Optional[str]] = mapped_column(String(255)) # PATH DE IMAGEN (MEX)

View File

@@ -10,6 +10,9 @@ class SerieBase(BaseModel):
brand: Optional[str] = Field(None, max_length=50, description="Brand (MARCA)") brand: Optional[str] = Field(None, max_length=50, description="Brand (MARCA)")
expo_brad: Optional[str] = Field(None, max_length=50, description="Expo brand (MARCAEXPO)") expo_brad: Optional[str] = Field(None, max_length=50, description="Expo brand (MARCAEXPO)")
number_id: Optional[str] = Field(None, max_length=25, description="Number ID (NUMIDEXPO)") number_id: Optional[str] = Field(None, max_length=25, description="Number ID (NUMIDEXPO)")
import_invoice: Optional[str] = Field(None, max_length=15, description="Import invoice (FACTURAIMPO)")
import_line: Optional[int] = Field(None, description="Import line (LINEAIMPO)")
image_path: Optional[str] = Field(None, max_length=255, description="Image path (IMAGEPATHMEX)")
class SerieCreate(SerieBase): class SerieCreate(SerieBase):

View File

@@ -40,6 +40,7 @@ from .models import LineItem
from .series.models import Serie from .series.models import Serie
from api.v1.modules.a76.invoices.models import InvoiceHeader from api.v1.modules.a76.invoices.models import InvoiceHeader
from api.v1.modules.a76.parts.models import Part from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -195,6 +196,26 @@ class ItemService:
serie_dict["row"] = 1 serie_dict["row"] = 1
db.add(Serie(**serie_dict)) db.add(Serie(**serie_dict))
# Identifier Detail data
if hasattr(line_data, "identifiers") and line_data.identifiers:
id_list = (
line_data.identifiers
if isinstance(line_data.identifiers, list)
else [line_data.identifiers]
)
for d in id_list:
id_dict = (
d.model_dump(exclude_unset=True)
if hasattr(d, "model_dump")
else (dict(d) if isinstance(d, dict) else {})
)
if not id_dict:
continue
id_dict["item_line_id"] = line.id
id_dict["tenant_id"] = tenant_id
id_dict["company_id"] = company_id
db.add(IdentifierDetail(**id_dict))
@staticmethod @staticmethod
def _attach_series(db: Session, item: LineItem) -> None: def _attach_series(db: Session, item: LineItem) -> None:
"""Query and attach all Serie rows for this item as a list.""" """Query and attach all Serie rows for this item as a list."""
@@ -206,6 +227,16 @@ class ItemService:
) )
item.series = list(series) item.series = list(series)
@staticmethod
def _attach_identifiers(db: Session, item: LineItem) -> None:
"""Query and attach all IdentifierDetail rows for this item."""
identifiers = (
db.query(IdentifierDetail)
.filter(IdentifierDetail.item_line_id == item.id)
.all()
)
item.identifiers = list(identifiers)
@staticmethod @staticmethod
def get_by_id( def get_by_id(
db: Session, item_id: int, tenant_id: int, company_id: int db: Session, item_id: int, tenant_id: int, company_id: int
@@ -232,6 +263,7 @@ class ItemService:
) )
if result: if result:
ItemService._attach_series(db, result) ItemService._attach_series(db, result)
ItemService._attach_identifiers(db, result)
return result return result
@staticmethod @staticmethod
@@ -285,6 +317,7 @@ class ItemService:
items = query.offset(skip).limit(limit).all() items = query.offset(skip).limit(limit).all()
for item in items: for item in items:
ItemService._attach_series(db, item) ItemService._attach_series(db, item)
ItemService._attach_identifiers(db, item)
return items, total return items, total
@staticmethod @staticmethod
@@ -318,6 +351,7 @@ class ItemService:
items = query.offset(skip).limit(limit).all() items = query.offset(skip).limit(limit).all()
for item in items: for item in items:
ItemService._attach_series(db, item) ItemService._attach_series(db, item)
ItemService._attach_identifiers(db, item)
return items, total return items, total
@staticmethod @staticmethod
@@ -443,6 +477,7 @@ class ItemService:
db.commit() db.commit()
db.refresh(db_item) db.refresh(db_item)
ItemService._attach_series(db, db_item) ItemService._attach_series(db, db_item)
ItemService._attach_identifiers(db, db_item)
return db_item return db_item
except IntegrityError as e: except IntegrityError as e:
@@ -591,6 +626,7 @@ class ItemService:
).delete() ).delete()
db.query(FaLineItem).filter(FaLineItem.id == db_item.id).delete() db.query(FaLineItem).filter(FaLineItem.id == db_item.id).delete()
db.query(Serie).filter(Serie.line_item_id == db_item.id).delete() db.query(Serie).filter(Serie.line_item_id == db_item.id).delete()
db.query(IdentifierDetail).filter(IdentifierDetail.item_line_id == db_item.id).delete()
db.flush() db.flush()
# Create new nested data # Create new nested data
@@ -604,6 +640,7 @@ class ItemService:
db.commit() db.commit()
db.refresh(db_item) db.refresh(db_item)
ItemService._attach_series(db, db_item) ItemService._attach_series(db, db_item)
ItemService._attach_identifiers(db, db_item)
return db_item return db_item
except HTTPException: except HTTPException:

View File

@@ -35,6 +35,33 @@ export interface IdentifierListResponse {
pages: number; pages: number;
} }
export interface IdentifierDetail {
id: number;
invoice_consecutive: number | null;
part_line: number | null;
identifier_code: string | null;
module: string | null;
complement1: string | null;
complement2: string | null;
complement3: string | null;
item_line_id: number | null;
company_id: number;
tenant_id: number;
}
export interface IdentifierDetailCreate {
invoice_consecutive?: number | null;
part_line?: number | null;
identifier_code?: string | null;
module?: string | null;
complement1?: string | null;
complement2?: string | null;
complement3?: string | null;
item_line_id?: number | null;
}
export interface IdentifierDetailUpdate extends Partial<IdentifierDetailCreate> { }
export async function getIdentifiers( export async function getIdentifiers(
page = 1, page = 1,
pageSize = 50, pageSize = 50,
@@ -72,3 +99,28 @@ export async function deleteIdentifier(
): Promise<ApiResponse<void>> { ): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/identifiers/${id}/?company_id=${companyId}`); return await api.delete(`/v1/a76/identifiers/${id}/?company_id=${companyId}`);
} }
/**
* API for Identifier Details
*/
export async function createIdentifierDetail(
data: IdentifierDetailCreate,
companyId: number
): Promise<ApiResponse<IdentifierDetail>> {
return await api.post(`/v1/a76/identifiers/details/?company_id=${companyId}`, data);
}
export async function updateIdentifierDetail(
id: number,
data: IdentifierDetailUpdate,
companyId: number
): Promise<ApiResponse<IdentifierDetail>> {
return await api.put(`/v1/a76/identifiers/details/${id}/?company_id=${companyId}`, data);
}
export async function deleteIdentifierDetail(
id: number,
companyId: number
): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/identifiers/details/${id}/?company_id=${companyId}`);
}

View File

@@ -88,6 +88,10 @@ export interface LineReferences {
serie_id?: number; serie_id?: number;
} }
import type {
IdentifierDetail
} from './general_catalogs/identifiers';
export interface Serie { export interface Serie {
id?: number; id?: number;
line_item_id?: number; line_item_id?: number;
@@ -98,6 +102,9 @@ export interface Serie {
brand?: string; brand?: string;
expo_brad?: string; expo_brad?: string;
number_id?: string; number_id?: string;
import_invoice?: string;
import_line?: number;
image_path?: string;
} }
export interface FaLineItem { export interface FaLineItem {
@@ -207,6 +214,7 @@ export interface Item {
reference?: LineReferences; reference?: LineReferences;
fa_data?: FaLineItem; // Fixed Asset specific data fa_data?: FaLineItem; // Fixed Asset specific data
series?: Serie[]; // Series data (multiple per line) series?: Serie[]; // Series data (multiple per line)
identifiers?: IdentifierDetail[]; // Identifiers for this line
} }
export interface ItemListResponse { export interface ItemListResponse {

View File

@@ -0,0 +1,115 @@
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog";
import * as Table from "$lib/components/ui/table";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Search, Loader2 } from "lucide-svelte";
import { getIdentifiers, type Identifier } from "$lib/api/dashboard/a76/general_catalogs/identifiers";
import { onMount } from "svelte";
import { companyStore } from '$lib/stores/company.svelte';
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (identifier: Identifier) => void;
} = $props();
let identifiers = $state<Identifier[]>([]);
let loading = $state(false);
let searchTerm = $state("");
async function loadIdentifiers() {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) return;
loading = true;
try {
// Using a large limit for now to avoid complex pagination in the selector
const res = await getIdentifiers(1, 1000, companyId);
if (res.data) {
identifiers = res.data.items || [];
}
} catch (error) {
console.error("Error loading identifiers:", error);
} finally {
loading = false;
}
}
onMount(() => {
loadIdentifiers();
});
const filteredIdentifiers = $derived(
identifiers.filter(i =>
i.code.toLowerCase().includes(searchTerm.toLowerCase()) ||
(i.description?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false)
)
);
function handleSelect(identifier: Identifier) {
onSelect(identifier);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[600px] h-[600px] flex flex-col p-0 text-xs">
<Dialog.Header class="px-6 pt-6 pb-4 shrink-0">
<Dialog.Title>Seleccionar Identificador</Dialog.Title>
<Dialog.Description>
Busca y selecciona un identificador del catálogo (Apéndice 8).
</Dialog.Description>
</Dialog.Header>
<div class="px-6 pb-4 shrink-0">
<div class="relative">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por clave o descripción..."
class="pl-9 h-9 text-xs"
bind:value={searchTerm}
/>
</div>
</div>
<div class="flex-1 overflow-y-auto px-6 pb-6">
{#if loading}
<div class="flex h-full items-center justify-center">
<Loader2 class="h-6 w-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-16">Clave</Table.Head>
<Table.Head>Descripción</Table.Head>
<Table.Head class="w-16">Nivel</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each filteredIdentifiers as item}
<Table.Row
class="cursor-pointer hover:bg-muted"
onclick={() => handleSelect(item)}
>
<Table.Cell class="font-bold">{item.code}</Table.Cell>
<Table.Cell>{item.description || '-'}</Table.Cell>
<Table.Cell>{item.level || '-'}</Table.Cell>
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={3} class="h-24 text-center text-muted-foreground">
No se encontraron identificadores.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -216,7 +216,7 @@
{ value: 'generales', label: 'General', visible: true }, { value: 'generales', label: 'General', visible: true },
{ value: 'continuacion', label: 'Continuación', visible: true }, { value: 'continuacion', label: 'Continuación', visible: true },
{ value: 'series', label: 'Series', visible: true }, { value: 'series', label: 'Series', visible: true },
{ value: 'etiquetado', label: 'Etiquetado', visible: true }, { value: 'etiquetado', label: 'Etiquetado', visible: visibility.showLabelingTab },
{ value: 'identificadores', label: 'IDs', visible: visibility.showIdentifiersTab } { value: 'identificadores', label: 'IDs', visible: visibility.showIdentifiersTab }
].filter((tab) => tab.visible)); ].filter((tab) => tab.visible));
const tabListStyle = $derived(`grid-template-columns: repeat(${visibleTabs.length || 1}, minmax(0, 1fr));`); const tabListStyle = $derived(`grid-template-columns: repeat(${visibleTabs.length || 1}, minmax(0, 1fr));`);
@@ -544,13 +544,20 @@
/> />
</Tabs.Content> </Tabs.Content>
<Tabs.Content value="etiquetado" class="m-0 focus-visible:outline-none"> {#if visibility.showLabelingTab}
<TabLabeling bind:descriptions={editingItem.description!} /> <Tabs.Content value="etiquetado" class="m-0 focus-visible:outline-none">
</Tabs.Content> <TabLabeling bind:lineItem={editingItem} bind:descriptions={editingItem.description!} {visibility} />
</Tabs.Content>
{/if}
{#if visibility.showIdentifiersTab} {#if visibility.showIdentifiersTab}
<Tabs.Content value="identificadores" class="m-0 focus-visible:outline-none"> <Tabs.Content value="identificadores" class="m-0 focus-visible:outline-none">
<TabIdentifiers bind:lineItem={editingItem} /> <TabIdentifiers
bind:lineItem={editingItem}
invoiceConsecutive={invoice?.id}
invoiceNumber={invoice?.invoice_number ?? ''}
{visibility}
/>
</Tabs.Content> </Tabs.Content>
{/if} {/if}
</div> </div>

View File

@@ -1,26 +1,376 @@
<script lang="ts"> <script lang="ts">
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import type { Item } from '$lib/api/dashboard/a76/items'; import { Button } from '$lib/components/ui/button';
import * as Table from '$lib/components/ui/table';
import * as Dialog from '$lib/components/ui/dialog';
import { Plus, Pencil, Trash2, Search, Image as ImageIcon, Upload } from 'lucide-svelte';
import type { Item, Serie } from '$lib/api/dashboard/a76/items';
import type { IdentifierDetail } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
import IdentifierCatalogSelector from './identifier-catalog-selector.svelte';
import { companyStore } from '$lib/stores/company.svelte';
let { lineItem = $bindable() }: { lineItem: Partial<Item> } = $props(); let {
lineItem = $bindable(),
invoiceConsecutive = undefined,
invoiceNumber = '',
visibility = { showMexicanIdEnhanced: false }
}: {
lineItem: Partial<Item>,
invoiceConsecutive?: number,
invoiceNumber?: string,
visibility?: any
} = $props();
// Initialize identifiers if not present
if (!lineItem.identifiers) {
lineItem.identifiers = [];
}
// Initialize series if not present
if (!lineItem.series) {
lineItem.series = [];
}
let showModal = $state(false);
let showMexModal = $state(false);
let showCatalogSelector = $state(false);
let isEditing = $state(false);
let editingIndex = $state(-1);
// State for standard Identifiers
let currentDetail = $state<Partial<IdentifierDetail>>({
invoice_consecutive: invoiceConsecutive || null,
part_line: lineItem.line_number || null,
module: 'SCAF-IT',
identifier_code: '',
complement1: '',
complement2: '',
complement3: ''
});
// State for MEX Assets
let currentMexAsset = $state<Partial<Serie>>({
number_id: '',
import_invoice: invoiceNumber || '',
import_line: lineItem.line_number ?? undefined,
image_path: ''
});
// Standard ID Functions
function openInsert() {
isEditing = false;
currentDetail = {
invoice_consecutive: invoiceConsecutive ?? null,
part_line: lineItem.line_number ?? null,
module: 'SCAF-IT',
identifier_code: '',
complement1: '',
complement2: '',
complement3: ''
};
showModal = true;
}
function openEdit(index: number) {
isEditing = true;
editingIndex = index;
currentDetail = { ...lineItem.identifiers![index] };
showModal = true;
}
function deleteDetail(index: number) {
lineItem.identifiers = lineItem.identifiers!.filter((_, i) => i !== index);
}
function saveDetail() {
if (!lineItem.identifiers) lineItem.identifiers = [];
if (isEditing) {
lineItem.identifiers[editingIndex] = currentDetail as IdentifierDetail;
} else {
lineItem.identifiers = [...lineItem.identifiers, currentDetail as IdentifierDetail];
}
showModal = false;
}
// MEX Asset Functions
function openMexInsert() {
isEditing = false;
currentMexAsset = {
number_id: '',
import_invoice: invoiceNumber || '',
import_line: lineItem.line_number ?? undefined,
image_path: ''
};
showMexModal = true;
}
function openMexEdit(index: number) {
isEditing = true;
editingIndex = index;
currentMexAsset = { ...lineItem.series![index] };
showMexModal = true;
}
function deleteMexAsset(index: number) {
lineItem.series = lineItem.series!.filter((_, i) => i !== index);
}
function saveMexAsset() {
if (!lineItem.series) lineItem.series = [];
if (isEditing) {
lineItem.series[editingIndex] = currentMexAsset as Serie;
} else {
lineItem.series = [...lineItem.series, currentMexAsset as Serie];
}
showMexModal = false;
}
function handleCatalogSelect(identifier: any) {
currentDetail.identifier_code = identifier.code;
showCatalogSelector = false;
}
// File selection simulation for image_path
function triggerFileSelect() {
// In a real scenario, this would trigger an input type="file"
// For now we'll just simulate setting a path/name
const simulatedPath = `assets/img_${Date.now()}.jpg`;
currentMexAsset.image_path = simulatedPath;
}
</script> </script>
<fieldset class="border rounded-md p-3 space-y-3"> <div class="space-y-4">
<legend class="text-xs font-semibold px-2 uppercase">Identifiers</legend> {#if visibility.showMexicanIdEnhanced}
<!-- MEX Specific Layout -->
<fieldset class="border rounded-md p-3">
<legend class="text-xs font-semibold px-2 uppercase flex items-center gap-2">
Activos / Num. Etiquetado (MEX)
</legend>
<div class="space-y-2"> <div class="flex justify-end mb-2">
<Label for="identificador1" class="text-xs">Main Identifier:</Label> <Button size="sm" variant="outline" class="h-8 text-xs gap-1" onclick={openMexInsert}>
<Input id="identificador1" bind:value={lineItem.identifier} class="h-8 text-sm" /> <Plus class="h-3 w-3" />
</div> Insertar Activo
</Button>
</div>
<div class="space-y-2"> <div class="border rounded-md overflow-hidden bg-white dark:bg-zinc-900">
<Label for="notas_identificadores" class="text-xs">Additional Identifiers / Notes:</Label> <Table.Root>
<textarea <Table.Header class="bg-gray-50 dark:bg-zinc-800">
id="notas_identificadores" <Table.Row class="h-8">
bind:value={lineItem.wildcard_field} <Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Asset Number</Table.Head>
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" <Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Num. Factura</Table.Head>
placeholder="Additional identifiers or notes..." <Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Línea</Table.Head>
></textarea> <Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8 text-center">Imagen</Table.Head>
</div> <Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8 w-20 text-center">Acciones</Table.Head>
</fieldset> </Table.Row>
</Table.Header>
<Table.Body>
{#if lineItem.series && lineItem.series.length > 0}
{#each lineItem.series as asset, index}
<Table.Row class="h-8 hover:bg-gray-50 dark:hover:bg-zinc-800/50 transition-colors">
<Table.Cell class="py-1 text-xs font-semibold">{asset.number_id}</Table.Cell>
<Table.Cell class="py-1 text-xs">{asset.import_invoice || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs">{asset.import_line || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs text-center">
{#if asset.image_path}
<div class="flex justify-center">
<ImageIcon class="h-4 w-4 text-blue-500" />
</div>
{:else}
<span class="text-gray-400">-</span>
{/if}
</Table.Cell>
<Table.Cell class="py-1 text-xs">
<div class="flex items-center justify-center gap-1">
<Button size="icon" variant="ghost" class="h-6 w-6 text-blue-500 hover:text-blue-600 hover:bg-blue-50" onclick={() => openMexEdit(index)}>
<Pencil class="h-3 w-3" />
</Button>
<Button size="icon" variant="ghost" class="h-6 w-6 text-red-500 hover:text-red-600 hover:bg-red-50" onclick={() => deleteMexAsset(index)}>
<Trash2 class="h-3 w-3" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={5} class="h-20 text-center text-gray-400 text-xs italic">
No hay activos registrados.
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</fieldset>
{:else}
<!-- Standard ID Layout -->
<fieldset class="border rounded-md p-3">
<legend class="text-xs font-semibold px-2 uppercase flex items-center gap-2">
Tabla de Identificadores
</legend>
<div class="flex justify-end mb-2">
<Button size="sm" variant="outline" class="h-8 text-xs gap-1" onclick={openInsert}>
<Plus class="h-3 w-3" />
Insertar
</Button>
</div>
<div class="border rounded-md overflow-hidden bg-white dark:bg-zinc-900">
<Table.Root>
<Table.Header class="bg-gray-100 dark:bg-zinc-800">
<Table.Row class="h-8">
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Clave</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Compl. 1</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Compl. 2</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8">Compl. 3</Table.Head>
<Table.Head class="text-[10px] uppercase font-bold text-gray-600 dark:text-gray-300 h-8 w-20 text-center">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if lineItem.identifiers && lineItem.identifiers.length > 0}
{#each lineItem.identifiers as idDetail, index}
<Table.Row class="h-8 hover:bg-gray-50 dark:hover:bg-zinc-800/50 transition-colors">
<Table.Cell class="py-1 text-xs font-semibold">{idDetail.identifier_code}</Table.Cell>
<Table.Cell class="py-1 text-xs">{idDetail.complement1 || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs">{idDetail.complement2 || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs">{idDetail.complement3 || '-'}</Table.Cell>
<Table.Cell class="py-1 text-xs">
<div class="flex items-center justify-center gap-1">
<Button size="icon" variant="ghost" class="h-6 w-6 text-blue-500 hover:text-blue-600 hover:bg-blue-50" onclick={() => openEdit(index)}>
<Pencil class="h-3 w-3" />
</Button>
<Button size="icon" variant="ghost" class="h-6 w-6 text-red-500 hover:text-red-600 hover:bg-red-50" onclick={() => deleteDetail(index)}>
<Trash2 class="h-3 w-3" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={5} class="h-20 text-center text-gray-400 text-xs italic">
No hay identificadores registrados.
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</fieldset>
{/if}
</div>
<!-- Modal Standard ID -->
<Dialog.Root bind:open={showModal}>
<Dialog.Content class="sm:max-w-[500px]">
<Dialog.Header>
<Dialog.Title>{isEditing ? 'Editar' : 'Insertar'} Identificador</Dialog.Title>
<Dialog.Description>
Ingrese los detalles del identificador para esta partida.
</Dialog.Description>
</Dialog.Header>
<div class="grid grid-cols-2 gap-4 py-4">
<div class="space-y-2">
<Label class="text-xs">Consecutivo Factura</Label>
<Input bind:value={currentDetail.invoice_consecutive} readonly class="h-8 text-xs bg-gray-50" />
</div>
<div class="space-y-2">
<Label class="text-xs">Partida (Número)</Label>
<Input bind:value={currentDetail.part_line} readonly class="h-8 text-xs bg-gray-50" />
</div>
<div class="space-y-2">
<Label class="text-xs">Módulo</Label>
<Input bind:value={currentDetail.module} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label class="text-xs">Clave</Label>
<div class="flex gap-1">
<Input bind:value={currentDetail.identifier_code} class="h-8 text-xs font-bold" placeholder="Clave" />
<Button size="icon" variant="outline" class="h-8 w-8 shrink-0" onclick={() => showCatalogSelector = true}>
<Search class="h-3 w-3" />
</Button>
</div>
</div>
<div class="col-span-2 grid grid-cols-3 gap-2">
<div class="space-y-2">
<Label class="text-xs">Complemento 1</Label>
<Input bind:value={currentDetail.complement1} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label class="text-xs">Complemento 2</Label>
<Input bind:value={currentDetail.complement2} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label class="text-xs">Complemento 3</Label>
<Input bind:value={currentDetail.complement3} class="h-8 text-xs" />
</div>
</div>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => showModal = false} class="h-8 text-xs">Cancelar</Button>
<Button onclick={saveDetail} class="h-8 text-xs">Guardar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<!-- Modal MEX Assets -->
<Dialog.Root bind:open={showMexModal}>
<Dialog.Content class="sm:max-w-[450px]">
<Dialog.Header>
<Dialog.Title>{isEditing ? 'Editar' : 'Insertar'} Asset Tag (Etiquetado)</Dialog.Title>
<Dialog.Description>
Detalles del etiquetado de activos para Compras Mexicanas.
</Dialog.Description>
</Dialog.Header>
<div class="space-y-4 py-4">
<!-- Info Section -->
<div class="bg-gray-50 dark:bg-zinc-800 p-3 rounded-md grid grid-cols-2 gap-3 border">
<div class="space-y-1">
<Label class="text-[10px] uppercase text-gray-500">Número de Factura</Label>
<p class="text-xs font-bold">{currentMexAsset.import_invoice || '-'}</p>
</div>
<div class="space-y-1">
<Label class="text-[10px] uppercase text-gray-500">Línea de Partida</Label>
<p class="text-xs font-bold">{currentMexAsset.import_line || '-'}</p>
</div>
</div>
<!-- Input Section -->
<div class="space-y-3">
<div class="space-y-2">
<Label class="text-xs">Asset Number</Label>
<Input bind:value={currentMexAsset.number_id} placeholder="Ingrese número de activo..." class="h-9 text-xs" />
</div>
<div class="space-y-2">
<Label class="text-xs">Imagen del Archivo</Label>
<div class="flex items-center gap-2">
<div class="flex-1 h-9 px-3 py-2 border rounded-md bg-white dark:bg-zinc-900 text-xs truncate italic text-gray-500">
{currentMexAsset.image_path || 'Ningún archivo seleccionado'}
</div>
<Button size="sm" variant="secondary" class="h-9 gap-1 text-xs" onclick={triggerFileSelect}>
<Upload class="h-3 w-3" />
Subir
</Button>
</div>
</div>
</div>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => showMexModal = false} class="h-8 text-xs">Cancelar</Button>
<Button onclick={saveMexAsset} class="h-8 text-xs">Guardar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
<IdentifierCatalogSelector
bind:open={showCatalogSelector}
onSelect={handleCatalogSelect}
/>

View File

@@ -1,32 +1,256 @@
<script lang="ts"> <script lang="ts">
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import type { LineDescriptions } from '$lib/api/dashboard/a76/items'; import { Button } from '$lib/components/ui/button';
import * as Table from '$lib/components/ui/table';
import { Plus, Pencil, Trash2, Folder } from 'lucide-svelte';
import type { Item, LineDescriptions, Serie } from '$lib/api/dashboard/a76/items';
import ValuationMethodSelector from './valuation-method-selector.svelte';
let { descriptions = $bindable() }: { descriptions: LineDescriptions } = $props(); let {
lineItem = $bindable(),
descriptions = $bindable(),
visibility
}: {
lineItem: Partial<Item>,
descriptions: LineDescriptions,
visibility: any
} = $props();
// Ensure series is an array
if (!lineItem.series) {
lineItem.series = [];
}
let valuationSelectorOpen = $state(false);
// Asset management state
let selectedAssetIndex = $state<number | null>(null);
let editingAsset = $state<Serie>({
row: 0,
number_id: '',
import_invoice: '',
import_line: undefined
});
function handleValuationMethodSelect(method: { key: string }) {
lineItem.valuation_method = method.key;
}
function addAsset() {
const newAsset: Serie = {
row: (lineItem.series?.length || 0) + 1,
number_id: '',
import_invoice: '',
import_line: undefined
};
lineItem.series = [...(lineItem.series || []), newAsset];
editAsset((lineItem.series || []).length - 1);
}
function editAsset(index: number) {
selectedAssetIndex = index;
editingAsset = { ...(lineItem.series?.[index] as Serie) };
}
function saveAsset() {
if (selectedAssetIndex !== null && lineItem.series) {
const updatedSeries = [...lineItem.series];
updatedSeries[selectedAssetIndex] = { ...editingAsset };
lineItem.series = updatedSeries;
selectedAssetIndex = null;
}
}
function deleteAsset(index: number) {
if (lineItem.series) {
lineItem.series = lineItem.series.filter((_, i) => i !== index);
// Re-index rows
lineItem.series = lineItem.series.map((s, i) => ({ ...s, row: i + 1 }));
if (selectedAssetIndex === index) {
selectedAssetIndex = null;
} else if (selectedAssetIndex !== null && selectedAssetIndex > index) {
selectedAssetIndex--;
}
}
}
function cancelAssetEdit() {
selectedAssetIndex = null;
}
</script> </script>
<fieldset class="border rounded-md p-3 space-y-3"> <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<legend class="text-xs font-semibold px-2 uppercase">Labeling</legend> <!-- Left Side: Labeling & Valuation -->
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700 uppercase">Labeling & Valuation</legend>
<div class="grid grid-cols-2 gap-4"> {#if visibility.showLabelingStandard}
<div class="space-y-2"> <div class="grid grid-cols-2 gap-2">
<Label for="numero_etiqueta" class="text-xs">Label Number:</Label> <div class="space-y-1">
<Input id="numero_etiqueta" bind:value={descriptions.lot} class="h-8 text-sm" /> <Label for="numero_etiqueta" class="text-xs">Label Number:</Label>
</div> <Input id="numero_etiqueta" bind:value={descriptions.lot} class="h-7 text-xs" />
<div class="space-y-2"> </div>
<Label for="tipo_etiqueta" class="text-xs">Label Type:</Label> <div class="space-y-1">
<Input id="tipo_etiqueta" bind:value={descriptions.entry_number} class="h-8 text-sm" /> <Label for="tipo_etiqueta" class="text-xs">Label Type:</Label>
</div> <Input id="tipo_etiqueta" bind:value={descriptions.entry_number} class="h-7 text-xs" />
</div> </div>
</div>
{/if}
<div class="space-y-2"> <div class="grid grid-cols-2 gap-2">
<Label for="observaciones_etiqueta" class="text-xs">Observations:</Label> {#if visibility.showLabelingEnhanced}
<textarea {#if visibility.showLabelingQuantity}
id="observaciones_etiqueta" <div class="space-y-1">
bind:value={descriptions.additional_info_spanish} <Label for="cantidad_importar" class="text-xs">Cantidad a importar:</Label>
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" <Input
placeholder="Labeling observations..." id="cantidad_importar"
></textarea> type="number"
</div> bind:value={lineItem.quantity!.quantity}
</fieldset> class="h-7 text-xs"
/>
</div>
{/if}
<div class="space-y-1">
<Label for="valor_det" class="text-xs">Valor Det:</Label>
<Input
id="valor_det"
type="number"
step="0.00000001"
bind:value={lineItem.valuation_determined_value}
class="h-7 text-xs"
/>
</div>
{/if}
</div>
{#if visibility.showLabelingEnhanced}
<div class="space-y-1">
<Label for="metodos_valoracion" class="text-xs">Métodos de valoración:</Label>
<div class="flex gap-1">
<Input
id="metodos_valoracion"
bind:value={lineItem.valuation_method}
class="h-7 text-xs flex-1"
placeholder="Seleccione..."
/>
<Button
size="icon"
variant="outline"
class="h-7 w-7"
onclick={() => valuationSelectorOpen = true}
>
<Folder class="h-3 w-3" />
</Button>
</div>
</div>
{#if visibility.showUsageReason}
<div class="space-y-1">
<Label for="motivo_uso" class="text-xs">Motivo de uso:</Label>
<Input
id="motivo_uso"
bind:value={lineItem.valuation_reason}
class="h-7 text-xs"
placeholder="Especifique motivo..."
/>
</div>
{/if}
{/if}
{#if visibility.showLabelingStandard}
<div class="space-y-1">
<Label for="observaciones_etiqueta" class="text-xs">Observations:</Label>
<textarea
id="observaciones_etiqueta"
bind:value={descriptions.additional_info_spanish}
class="flex min-h-[60px] w-full rounded-md border border-input bg-background px-3 py-1.5 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="Labeling observations..."
></textarea>
</div>
{/if}
</fieldset>
{#if visibility.showLabelingEnhanced}
<!-- Right Side: Assets Table -->
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700 uppercase">Assets / Series</legend>
<div class="flex justify-end">
<Button size="sm" variant="outline" class="h-7 text-xs px-2" onclick={addAsset}>
<Plus class="h-3 h-3 mr-1" /> Insertar Activo
</Button>
</div>
<div class="rounded-md border overflow-hidden max-h-[180px] overflow-y-auto">
<Table.Root>
<Table.Header>
<Table.Row class="bg-muted/50 h-7">
<Table.Head class="w-8 text-center text-[10px] p-0 px-1">#</Table.Head>
<Table.Head class="text-[10px] p-0 px-1">Asset Num</Table.Head>
<Table.Head class="text-[10px] p-0 px-1">Factura</Table.Head>
<Table.Head class="text-[10px] p-0 px-1">Línea</Table.Head>
<Table.Head class="w-14 text-right text-[10px] p-0 px-2">Acc</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each lineItem.series || [] as asset, i}
<Table.Row class="hover:bg-muted/50 h-7">
<Table.Cell class="text-center text-[10px] p-0 px-1 font-medium">{asset.row || i+1}</Table.Cell>
<Table.Cell class="text-[10px] p-0 px-1 truncate max-w-[60px]">{asset.number_id || '-'}</Table.Cell>
<Table.Cell class="text-[10px] p-0 px-1 truncate max-w-[60px]">{asset.import_invoice || '-'}</Table.Cell>
<Table.Cell class="text-[10px] p-0 px-1">{asset.import_line || '-'}</Table.Cell>
<Table.Cell class="text-right p-0 px-2">
<div class="flex justify-end gap-0.5">
<Button variant="ghost" size="icon" class="h-5 w-5" onclick={() => editAsset(i)}>
<Pencil class="h-2.5 w-2.5" />
</Button>
<Button variant="ghost" size="icon" class="h-5 w-5 text-destructive" onclick={() => deleteAsset(i)}>
<Trash2 class="h-2.5 w-2.5" />
</Button>
</div>
</Table.Cell>
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={5} class="h-12 text-center text-muted-foreground text-[10px]">
No hay activos.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
{#if selectedAssetIndex !== null}
<div class="p-2 border rounded bg-muted/20 space-y-2">
<div class="text-[10px] font-semibold uppercase">Editar #{editingAsset.row}</div>
<div class="grid grid-cols-2 gap-2">
<div class="space-y-0.5">
<Label class="text-[10px]">Asset Number</Label>
<Input bind:value={editingAsset.number_id} class="h-6 text-[10px]" />
</div>
<div class="space-y-0.5">
<Label class="text-[10px]">Factura Impo</Label>
<Input bind:value={editingAsset.import_invoice} class="h-6 text-[10px]" />
</div>
<div class="space-y-0.5">
<Label class="text-[10px]">Linea Impo</Label>
<Input type="number" bind:value={editingAsset.import_line} class="h-6 text-[10px]" />
</div>
</div>
<div class="flex justify-end gap-1.5 pt-1">
<Button size="sm" variant="ghost" class="h-6 text-[10px] px-2" onclick={cancelAssetEdit}>Can</Button>
<Button size="sm" class="h-6 text-[10px] px-2" onclick={saveAsset}>Guar</Button>
</div>
</div>
{/if}
</fieldset>
{/if}
</div>
<ValuationMethodSelector
bind:open={valuationSelectorOpen}
onSelect={handleValuationMethodSelect}
/>

View File

@@ -0,0 +1,108 @@
<script lang="ts">
import * as Dialog from "$lib/components/ui/dialog";
import * as Table from "$lib/components/ui/table";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Search, Loader2 } from "lucide-svelte";
import { valuationMethodsApi, type ValuationMethod } from "$lib/api/dashboard/reference_data/valuation_methods";
import { onMount } from "svelte";
let {
open = $bindable(false),
onSelect
}: {
open: boolean;
onSelect: (method: ValuationMethod) => void;
} = $props();
let methods = $state<ValuationMethod[]>([]);
let loading = $state(false);
let searchTerm = $state("");
async function loadMethods() {
loading = true;
try {
const res = await valuationMethodsApi.list(1, 100);
if (res.data) {
methods = res.data.items || [];
}
} catch (error) {
console.error("Error loading valuation methods:", error);
} finally {
loading = false;
}
}
onMount(() => {
loadMethods();
});
const filteredMethods = $derived(
methods.filter(m =>
m.key.toLowerCase().includes(searchTerm.toLowerCase()) ||
m.description.toLowerCase().includes(searchTerm.toLowerCase())
)
);
function handleSelect(method: ValuationMethod) {
onSelect(method);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[500px] h-[600px] flex flex-col p-0">
<Dialog.Header class="px-6 pt-6 pb-4 shrink-0">
<Dialog.Title>Seleccionar Método de Valoración</Dialog.Title>
<Dialog.Description>
Busca y selecciona un método de valoración de la lista.
</Dialog.Description>
</Dialog.Header>
<div class="px-6 pb-4 shrink-0">
<div class="relative">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por clave o descripción..."
class="pl-9 h-9"
bind:value={searchTerm}
/>
</div>
</div>
<div class="flex-1 overflow-y-auto px-6 pb-6">
{#if loading}
<div class="flex h-full items-center justify-center">
<Loader2 class="h-6 w-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-20">Clave</Table.Head>
<Table.Head>Descripción</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each filteredMethods as method}
<Table.Row
class="cursor-pointer hover:bg-muted"
onclick={() => handleSelect(method)}
>
<Table.Cell class="font-medium">{method.key}</Table.Cell>
<Table.Cell>{method.description}</Table.Cell>
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={2} class="h-24 text-center text-muted-foreground">
No se encontraron métodos de valoración.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -31,6 +31,18 @@ export interface InvoiceItemVisibility {
showContinuationConsiderA31: boolean; showContinuationConsiderA31: boolean;
/** Continuación tab: Extra Description in Spanish. */ /** Continuación tab: Extra Description in Spanish. */
showContinuationExtraDescription: boolean; showContinuationExtraDescription: boolean;
/** Etiquetado tab: Quantity, Valuation, Assets Table. */
showLabelingEnhanced: boolean;
/** Identificadores tab: Special Assets table for MEX. */
showMexicanIdEnhanced: boolean;
/** Etiquetado tab visibility. */
showLabelingTab: boolean;
/** Etiquetado tab: Usage Reason field. */
showUsageReason: boolean;
/** Etiquetado tab: Standard fields (Label No, Type, Observations). */
showLabelingStandard: boolean;
/** Etiquetado tab: Quantity field. */
showLabelingQuantity: boolean;
} }
const defaultVisibility: InvoiceItemVisibility = { const defaultVisibility: InvoiceItemVisibility = {
@@ -50,7 +62,13 @@ const defaultVisibility: InvoiceItemVisibility = {
showContinuationOwnOmitAnnex: true, showContinuationOwnOmitAnnex: true,
showContinuationLotEntry: true, showContinuationLotEntry: true,
showContinuationConsiderA31: true, showContinuationConsiderA31: true,
showContinuationExtraDescription: true showContinuationExtraDescription: true,
showLabelingEnhanced: false,
showMexicanIdEnhanced: false,
showLabelingTab: true,
showUsageReason: false,
showLabelingStandard: true,
showLabelingQuantity: false
}; };
function normalizeInvoiceType(invoiceType?: string | null): string { function normalizeInvoiceType(invoiceType?: string | null): string {
@@ -126,14 +144,21 @@ export function getVisibility(
return { return {
...defaultVisibility, ...defaultVisibility,
showCrTrackingHeader: false, showCrTrackingHeader: false,
showFdaFcc: false showFdaFcc: false,
showLabelingEnhanced: true,
showLabelingQuantity: true
}; };
case 'CR': case 'CR':
return { return {
...defaultVisibility, ...defaultVisibility,
showEighthRule: false, showEighthRule: false,
showValuationFields: true showIdentifiersTab: true,
showValuationFields: true,
showUsageReason: true,
showLabelingEnhanced: true,
showLabelingQuantity: false,
showLabelingStandard: false
}; };
case 'REP': case 'REP':
@@ -165,7 +190,9 @@ export function getVisibility(
showEighthRule: false, showEighthRule: false,
showFdaFcc: false, showFdaFcc: false,
showCertificateOfOrigin: false, showCertificateOfOrigin: false,
showIdentifiersTab: false, showIdentifiersTab: true,
showMexicanIdEnhanced: true,
showLabelingTab: false,
showContinuationIgi: false, showContinuationIgi: false,
showContinuationLocation: true, showContinuationLocation: true,
showContinuationMilitary: false, showContinuationMilitary: false,