feature/pestana-series-en-partidas
This commit is contained in:
@@ -42,6 +42,11 @@ from api.v1.modules.a24.fa.fa_item_lines.dto import (
|
||||
FaLineItemUpdateDTO,
|
||||
FaLineItemResponseDTO,
|
||||
)
|
||||
from .series.schemas import (
|
||||
SerieCreate,
|
||||
SerieUpdate,
|
||||
SerieResponse,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -230,6 +235,9 @@ class LineItemCreate(LineItemBase):
|
||||
fa_data: Optional[FaLineItemCreateDTO] = Field(
|
||||
None, description="Fixed Asset data for this line"
|
||||
)
|
||||
series: Optional[list[SerieCreate]] = Field(
|
||||
None, description="Series data for this line (multiple per line)"
|
||||
)
|
||||
|
||||
|
||||
class LineItemUpdate(LineItemBase):
|
||||
@@ -256,6 +264,9 @@ class LineItemUpdate(LineItemBase):
|
||||
fa_data: Optional[FaLineItemUpdateDTO] = Field(
|
||||
None, description="Fixed Asset data for this line"
|
||||
)
|
||||
series: Optional[list[SerieUpdate]] = Field(
|
||||
None, description="Series data for this line (replace all)"
|
||||
)
|
||||
|
||||
|
||||
class LineItemResponse(LineItemBase):
|
||||
@@ -283,6 +294,7 @@ class LineItemResponse(LineItemBase):
|
||||
description: Optional[LineDescriptionResponse] = None
|
||||
reference: Optional[LineReferenceResponse] = None
|
||||
fa_data: Optional[FaLineItemResponseDTO] = None
|
||||
series: Optional[list[SerieResponse]] = None
|
||||
|
||||
# Fields populated from relationships
|
||||
class_code: Optional[str] = None
|
||||
|
||||
27
backend/api/v1/modules/a76/items/series/schemas.py
Normal file
27
backend/api/v1/modules/a76/items/series/schemas.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
|
||||
class SerieBase(BaseModel):
|
||||
row: Optional[int] = Field(None, description="Serie row number (RENGLON)")
|
||||
serial_numbers: Optional[str] = Field(None, max_length=50, description="Serial number (SERIEEXPO)")
|
||||
model: Optional[str] = Field(None, max_length=50, description="Model (MODELOEXPO)")
|
||||
sub_model: Optional[str] = Field(None, max_length=50, description="Sub model (SUBMODELOEXPO)")
|
||||
brand: Optional[str] = Field(None, max_length=50, description="Brand (MARCA)")
|
||||
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)")
|
||||
|
||||
|
||||
class SerieCreate(SerieBase):
|
||||
pass
|
||||
|
||||
|
||||
class SerieUpdate(SerieBase):
|
||||
pass
|
||||
|
||||
|
||||
class SerieResponse(SerieBase):
|
||||
id: int
|
||||
line_item_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -35,6 +35,7 @@ from .line_descriptions.models import LineDescription
|
||||
from .line_references.models import LineReference
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from .models import LineItem
|
||||
from .series.models import Serie
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
|
||||
@@ -170,12 +171,45 @@ class ItemService:
|
||||
)
|
||||
db.add(FaLineItem(**fa_dict))
|
||||
|
||||
# Serie data (list: multiple series per line)
|
||||
if hasattr(line_data, "series") and line_data.series:
|
||||
series_list = (
|
||||
line_data.series
|
||||
if isinstance(line_data.series, list)
|
||||
else [line_data.series]
|
||||
)
|
||||
for s in series_list:
|
||||
serie_dict = (
|
||||
s.model_dump(exclude_unset=True)
|
||||
if hasattr(s, "model_dump")
|
||||
else (dict(s) if isinstance(s, dict) else {})
|
||||
)
|
||||
if not serie_dict:
|
||||
continue
|
||||
serie_dict["line_item_id"] = line.id
|
||||
serie_dict["tenant_id"] = tenant_id
|
||||
serie_dict["company_id"] = company_id
|
||||
if serie_dict.get("row") is None:
|
||||
serie_dict["row"] = 1
|
||||
db.add(Serie(**serie_dict))
|
||||
|
||||
@staticmethod
|
||||
def _attach_series(db: Session, item: LineItem) -> None:
|
||||
"""Query and attach all Serie rows for this item as a list."""
|
||||
series = (
|
||||
db.query(Serie)
|
||||
.filter(Serie.line_item_id == item.id)
|
||||
.order_by(Serie.row, Serie.id)
|
||||
.all()
|
||||
)
|
||||
item.series = list(series)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, item_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[LineItem]:
|
||||
"""Get an item by ID with tenant/company validation"""
|
||||
return (
|
||||
result = (
|
||||
db.query(LineItem)
|
||||
.options(
|
||||
joinedload(LineItem.financial),
|
||||
@@ -194,6 +228,9 @@ class ItemService:
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if result:
|
||||
ItemService._attach_series(db, result)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
@@ -244,6 +281,8 @@ class ItemService:
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
for item in items:
|
||||
ItemService._attach_series(db, item)
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
@@ -275,6 +314,8 @@ class ItemService:
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
for item in items:
|
||||
ItemService._attach_series(db, item)
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
@@ -359,6 +400,7 @@ class ItemService:
|
||||
"description",
|
||||
"reference",
|
||||
"fa_data",
|
||||
"series",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -383,6 +425,7 @@ class ItemService:
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
ItemService._attach_series(db, db_item)
|
||||
return db_item
|
||||
|
||||
except IntegrityError as e:
|
||||
@@ -489,6 +532,7 @@ class ItemService:
|
||||
"description",
|
||||
"reference",
|
||||
"fa_data",
|
||||
"series",
|
||||
},
|
||||
exclude_unset=True,
|
||||
)
|
||||
@@ -512,6 +556,7 @@ class ItemService:
|
||||
LineReference.item_line_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.flush()
|
||||
|
||||
# Create new nested data
|
||||
@@ -524,6 +569,7 @@ class ItemService:
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
ItemService._attach_series(db, db_item)
|
||||
return db_item
|
||||
|
||||
except HTTPException:
|
||||
|
||||
@@ -88,6 +88,18 @@ export interface LineReferences {
|
||||
serie_id?: number;
|
||||
}
|
||||
|
||||
export interface Serie {
|
||||
id?: number;
|
||||
line_item_id?: number;
|
||||
row?: number;
|
||||
serial_numbers?: string;
|
||||
model?: string;
|
||||
sub_model?: string;
|
||||
brand?: string;
|
||||
expo_brad?: string;
|
||||
number_id?: string;
|
||||
}
|
||||
|
||||
export interface FaLineItem {
|
||||
id?: number;
|
||||
tenant_id?: number;
|
||||
@@ -183,7 +195,8 @@ export interface Item {
|
||||
quantity?: LineQuantities;
|
||||
description?: LineDescriptions;
|
||||
reference?: LineReferences;
|
||||
fa_data?: FaLineItem; // Fixed Asset specific data
|
||||
fa_data?: FaLineItem; // Fixed Asset specific data
|
||||
series?: Serie[]; // Series data (multiple per line)
|
||||
}
|
||||
|
||||
export interface ItemListResponse {
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
|
||||
// Acceso directo a la primera línea para evitar repeticiones en el HTML
|
||||
let line = $derived(editingItem);
|
||||
|
||||
// Ensure series is always an array when sheet is open
|
||||
$effect(() => {
|
||||
if (open && editingItem) {
|
||||
if (!Array.isArray(editingItem.series)) {
|
||||
editingItem.series = editingItem.series != null ? [editingItem.series] : [];
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Sheet.Root bind:open={open}>
|
||||
@@ -149,7 +158,12 @@
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="series" class="m-0 focus-visible:outline-none">
|
||||
<TabSeries bind:descriptions={editingItem.description!} />
|
||||
<TabSeries
|
||||
bind:descriptions={editingItem.description!}
|
||||
bind:series={editingItem.series!}
|
||||
lineItem={editingItem}
|
||||
{invoice}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="etiquetado" class="m-0 focus-visible:outline-none">
|
||||
|
||||
@@ -1,24 +1,328 @@
|
||||
<script lang="ts">
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import type { LineDescriptions } from '$lib/api/dashboard/a76/items';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Plus, Pencil } from 'lucide-svelte';
|
||||
import type { Item, LineDescriptions, Serie } from '$lib/api/dashboard/a76/items';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
|
||||
let { descriptions = $bindable() }: { descriptions: LineDescriptions } = $props();
|
||||
let {
|
||||
descriptions = $bindable(),
|
||||
series = $bindable(),
|
||||
lineItem,
|
||||
invoice
|
||||
}: {
|
||||
descriptions: LineDescriptions;
|
||||
series: Serie[] | Serie;
|
||||
lineItem: Partial<Item>;
|
||||
invoice: Invoice | null;
|
||||
} = $props();
|
||||
|
||||
// Normalize to array for display and mutations
|
||||
const seriesList = $derived(
|
||||
Array.isArray(series) ? series : series != null ? [series] : []
|
||||
);
|
||||
|
||||
let selectedSeriesIndex = $state<number | null>(null);
|
||||
|
||||
// Ensure series is array in parent when we mutate (for bindable)
|
||||
function ensureSeriesArray(): Serie[] {
|
||||
const arr = Array.isArray(series) ? series : series != null ? [series] : [];
|
||||
if (!Array.isArray(series)) {
|
||||
series = arr;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function addNewSerie() {
|
||||
const arr = ensureSeriesArray();
|
||||
const newSerie: Serie = {
|
||||
row: arr.length + 1,
|
||||
serial_numbers: '',
|
||||
model: '',
|
||||
sub_model: '',
|
||||
number_id: ''
|
||||
};
|
||||
series = [...arr, newSerie];
|
||||
selectedSeriesIndex = (series as Serie[]).length - 1;
|
||||
}
|
||||
|
||||
function selectForEdit(index: number) {
|
||||
selectedSeriesIndex = index;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedSeriesIndex = null;
|
||||
}
|
||||
|
||||
// Current serie being edited (reference into the array)
|
||||
const currentSerie = $derived(
|
||||
selectedSeriesIndex !== null && seriesList[selectedSeriesIndex] != null
|
||||
? seriesList[selectedSeriesIndex]
|
||||
: null
|
||||
);
|
||||
|
||||
// Ensure descriptions.has_serial has default
|
||||
$effect(() => {
|
||||
if (descriptions && descriptions.has_serial === undefined) {
|
||||
descriptions.has_serial = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure current serie has defaults for form fields
|
||||
$effect(() => {
|
||||
const s = currentSerie;
|
||||
if (s) {
|
||||
if (s.serial_numbers === undefined) s.serial_numbers = '';
|
||||
if (s.model === undefined) s.model = '';
|
||||
if (s.sub_model === undefined) s.sub_model = '';
|
||||
if (s.number_id === undefined) s.number_id = '';
|
||||
if (s.row === undefined) s.row = 1;
|
||||
}
|
||||
});
|
||||
|
||||
const hasSerial = $derived(Boolean(descriptions?.has_serial));
|
||||
const invoiceNumber = $derived(invoice?.invoice_number || '');
|
||||
const invoiceLine = $derived(lineItem?.line_number != null ? String(lineItem.line_number) : '');
|
||||
const partNumber = $derived((lineItem as any)?.part_number_display || lineItem?.part_number || '');
|
||||
|
||||
// Restricción cantidad: no más series que la cantidad de la partida (paridad CSV DEF/EXPO)
|
||||
const maxSeriesAllowed = $derived(
|
||||
lineItem?.quantity?.quantity != null && Number(lineItem.quantity.quantity) >= 0
|
||||
? Number(lineItem.quantity.quantity)
|
||||
: Infinity
|
||||
);
|
||||
const canAddNewSerie = $derived(hasSerial && seriesList.length < maxSeriesAllowed);
|
||||
|
||||
// Normalizar texto: comas y saltos de línea → espacio, colapsar espacios (SACARCOMASENTERS)
|
||||
function normalizeSerieText(val: string): string {
|
||||
if (val == null) return '';
|
||||
return String(val)
|
||||
.replace(/,/g, ' ')
|
||||
.replace(/\r\n/g, ' ')
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/\r/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function handleSerieInput(
|
||||
e: Event & { currentTarget: HTMLInputElement },
|
||||
field: 'serial_numbers' | 'model' | 'sub_model' | 'number_id'
|
||||
) {
|
||||
const s = currentSerie;
|
||||
if (!s) return;
|
||||
const raw = e.currentTarget.value;
|
||||
const normalized = normalizeSerieText(raw);
|
||||
(s as any)[field] = normalized;
|
||||
if (normalized !== raw) {
|
||||
e.currentTarget.value = normalized;
|
||||
e.currentTarget.setSelectionRange(normalized.length, normalized.length);
|
||||
}
|
||||
}
|
||||
|
||||
function clampRowToInteger() {
|
||||
const s = currentSerie;
|
||||
if (!s) return;
|
||||
const n = Number(s.row);
|
||||
const clamped = Math.max(1, Math.floor(Number.isFinite(n) ? n : 1));
|
||||
s.row = clamped;
|
||||
}
|
||||
</script>
|
||||
|
||||
<fieldset class="border rounded-md p-3 space-y-3">
|
||||
<legend class="text-xs font-semibold px-2 uppercase">Serial Numbers</legend>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="series" class="text-xs">Serial Numbers:</Label>
|
||||
<textarea
|
||||
id="series"
|
||||
bind:value={descriptions.extra_description}
|
||||
class="flex min-h-[100px] 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"
|
||||
placeholder="Enter serial numbers, one per line..."
|
||||
></textarea>
|
||||
<legend class="text-xs font-semibold px-2 uppercase">Series</legend>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<Checkbox id="has_serial" bind:checked={descriptions.has_serial} />
|
||||
<Label for="has_serial" class="text-xs font-normal">Lleva serie (LLEVASERIE)</Label>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-7 text-xs"
|
||||
onclick={addNewSerie}
|
||||
disabled={!canAddNewSerie}
|
||||
>
|
||||
<Plus class="w-3.5 h-3.5 mr-1" />
|
||||
Nueva serie
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Table of all series for this item -->
|
||||
<div class="rounded-md border overflow-hidden">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row class="bg-muted/50">
|
||||
<Table.Head class="w-12 text-center text-xs">Línea</Table.Head>
|
||||
<Table.Head class="text-xs">Serie</Table.Head>
|
||||
<Table.Head class="text-xs">Modelo</Table.Head>
|
||||
<Table.Head class="text-xs">Sub modelo</Table.Head>
|
||||
<Table.Head class="text-xs">Núm. ID</Table.Head>
|
||||
<Table.Head class="w-[80px] text-right text-xs">Acciones</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if seriesList.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={6} class="py-4 text-center text-muted-foreground text-xs">
|
||||
No hay series registradas. Usa "Nueva serie" para agregar una.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each seriesList as serie, i (i)}
|
||||
<Table.Row
|
||||
class="cursor-pointer hover:bg-muted/50 {selectedSeriesIndex === i
|
||||
? 'bg-primary/10 dark:bg-primary/20'
|
||||
: ''} {!hasSerial ? 'opacity-70' : ''}"
|
||||
onclick={() => hasSerial && selectForEdit(i)}
|
||||
>
|
||||
<Table.Cell class="text-center text-xs font-medium">{serie.row ?? i + 1}</Table.Cell>
|
||||
<Table.Cell class="text-xs max-w-[120px] truncate" title={serie.serial_numbers}>
|
||||
{serie.serial_numbers || '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-xs max-w-[100px] truncate" title={serie.model}>
|
||||
{serie.model || '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-xs max-w-[100px] truncate" title={serie.sub_model}>
|
||||
{serie.sub_model || '-'}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-xs">{serie.number_id || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
disabled={!hasSerial}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (hasSerial) selectForEdit(i);
|
||||
}}
|
||||
>
|
||||
<Pencil class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<!-- Form for selected or new serie -->
|
||||
{#if currentSerie && selectedSeriesIndex !== null}
|
||||
<div class="mt-3 pt-3 border-t space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
{selectedSeriesIndex >= seriesList.length - 1 && !currentSerie?.id
|
||||
? 'Nueva serie'
|
||||
: `Editar serie (línea ${currentSerie.row ?? selectedSeriesIndex + 1})`}
|
||||
</span>
|
||||
<Button type="button" variant="ghost" size="sm" class="h-6 text-xs" onclick={clearSelection}>
|
||||
Cerrar formulario
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="invoice_number" class="text-xs">NUMERO FACTURA:</Label>
|
||||
<div
|
||||
id="invoice_number"
|
||||
class="flex h-8 w-full items-center rounded-md border border-input bg-muted/40 px-3 text-sm text-foreground"
|
||||
>
|
||||
{invoiceNumber || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="invoice_line" class="text-xs">LINEA FACTURA:</Label>
|
||||
<div
|
||||
id="invoice_line"
|
||||
class="flex h-8 w-full items-center rounded-md border border-input bg-muted/40 px-3 text-sm text-foreground"
|
||||
>
|
||||
{invoiceLine || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="series_row" class="text-xs">LINEA SERIE:</Label>
|
||||
<Input
|
||||
id="series_row"
|
||||
bind:value={currentSerie.row}
|
||||
class="h-8 text-sm"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
disabled={!hasSerial}
|
||||
onblur={clampRowToInteger}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="serial_number" class="text-xs">SERIE:</Label>
|
||||
<Input
|
||||
id="serial_number"
|
||||
value={currentSerie.serial_numbers ?? ''}
|
||||
class="h-8 text-sm"
|
||||
maxlength={50}
|
||||
placeholder="Número de serie..."
|
||||
disabled={!hasSerial}
|
||||
oninput={(e) => handleSerieInput(e, 'serial_numbers')}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="series_model" class="text-xs">MODELO:</Label>
|
||||
<Input
|
||||
id="series_model"
|
||||
value={currentSerie.model ?? ''}
|
||||
class="h-8 text-sm"
|
||||
maxlength={50}
|
||||
placeholder="Modelo..."
|
||||
disabled={!hasSerial}
|
||||
oninput={(e) => handleSerieInput(e, 'model')}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="part_number" class="text-xs">NUM PARTE:</Label>
|
||||
<div
|
||||
id="part_number"
|
||||
class="flex h-8 w-full items-center rounded-md border border-input bg-muted/40 px-3 text-sm text-foreground"
|
||||
>
|
||||
{partNumber || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="sub_model" class="text-xs">SUB MODELO:</Label>
|
||||
<Input
|
||||
id="sub_model"
|
||||
value={currentSerie.sub_model ?? ''}
|
||||
class="h-8 text-sm"
|
||||
maxlength={50}
|
||||
placeholder="Sub modelo..."
|
||||
disabled={!hasSerial}
|
||||
oninput={(e) => handleSerieInput(e, 'sub_model')}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="number_id" class="text-xs">NUMERO ID:</Label>
|
||||
<Input
|
||||
id="number_id"
|
||||
value={currentSerie.number_id ?? ''}
|
||||
class="h-8 text-sm"
|
||||
maxlength={25}
|
||||
placeholder="Número ID..."
|
||||
disabled={!hasSerial}
|
||||
oninput={(e) => handleSerieInput(e, 'number_id')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="text-xs text-muted-foreground">
|
||||
You can enter multiple serial numbers, one per line
|
||||
{#if !hasSerial}
|
||||
Los datos capturados se conservan, pero la edición queda deshabilitada mientras "Lleva serie" esté apagado.
|
||||
{/if}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -315,7 +315,8 @@
|
||||
},
|
||||
reference: {
|
||||
serie_id: undefined
|
||||
}
|
||||
},
|
||||
series: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -544,6 +545,8 @@
|
||||
selectedItem = lineData.full_item;
|
||||
// Deep clone and normalize numeric values
|
||||
editingItem = normalizeItemData(JSON.parse(JSON.stringify(lineData.full_item)));
|
||||
if (!editingItem.series) editingItem.series = [];
|
||||
else if (!Array.isArray(editingItem.series)) editingItem.series = [editingItem.series];
|
||||
// Guardar una copia del estado original para restaurar al cancelar
|
||||
originalItemData = JSON.parse(JSON.stringify(editingItem));
|
||||
// Enrich with descriptive data
|
||||
|
||||
@@ -46,6 +46,8 @@ export function cleanLineData(line: any) {
|
||||
delete cleaned.part_description_en;
|
||||
delete cleaned.unit_code;
|
||||
delete cleaned.unit_description;
|
||||
delete cleaned.includes_subitems;
|
||||
delete cleaned.payment_method_description;
|
||||
|
||||
// Only delete part_number if it's the string code from UI, but schema expects int ID.
|
||||
// In this codebase, if part_number is populated from existing data, it's an ID.
|
||||
@@ -58,12 +60,27 @@ export function cleanLineData(line: any) {
|
||||
if (!hasValues(cleaned.customs)) delete cleaned.customs;
|
||||
}
|
||||
|
||||
if (cleaned.fa_data) {
|
||||
delete cleaned.fa_data.includes_subitems;
|
||||
if (!hasValues(cleaned.fa_data)) delete cleaned.fa_data;
|
||||
}
|
||||
|
||||
// Remove empty nested objects
|
||||
if (cleaned.financial && !hasValues(cleaned.financial)) delete cleaned.financial;
|
||||
if (cleaned.quantity && !hasValues(cleaned.quantity)) delete cleaned.quantity;
|
||||
if (cleaned.description && !hasValues(cleaned.description)) delete cleaned.description;
|
||||
if (cleaned.reference && !hasValues(cleaned.reference)) delete cleaned.reference;
|
||||
if (cleaned.fa_data && !hasValues(cleaned.fa_data)) delete cleaned.fa_data;
|
||||
if (cleaned.series != null) {
|
||||
const arr = Array.isArray(cleaned.series) ? cleaned.series : [cleaned.series];
|
||||
cleaned.series = arr
|
||||
.map((s: any) => {
|
||||
if (!s || typeof s !== 'object') return null;
|
||||
const { id, line_item_id, ...rest } = s;
|
||||
return hasValues(rest) ? rest : null;
|
||||
})
|
||||
.filter((s: any) => s != null);
|
||||
if (cleaned.series.length === 0) delete cleaned.series;
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user