# Tutorial · Stock del taller

Tutorial paso a paso sobre el módulo **Stock del taller** en Despiece:
administrar el inventario real de materia prima (tableros · herraje ·
cinta de canto) del taller, con alertas de stock bajo, libro de
movimientos y las dos integraciones con el pedido de materiales
(¿me alcanza? y pedido recibido).

El modelo de ejemplo es `CocinaV9.despiece`. El módulo vive en un
panel sheet full-screen separado de los flujos 2D (BOM/nest) y se
abre con `Shift+T` o el botón `[T]` del topbar, el último del grupo
de documentos (después de `[K]` Clientes).

---

## 1. Activar y abrir el sheet `[T]` Stock

![Topbar con botón [T] y badge de alertas](img/01-overview-stock.jpg)

El módulo es **opt-in**: `stockEnabled` viene en `false`
(`src/stock/types.ts`). Mientras esté apagado, el botón `[T]` del
topbar **no aparece** y tanto `Shift+T` como un click muestran el
aviso *"Módulo Stock desactivado · activalo en Settings → Stock"*.

Para prenderlo: `Configuración → Stock → Activar módulo Stock`. Recién
entonces hay dos formas de abrir el sheet:

- **Atajo `Shift+T`**.
- **Botón `[T]`** del topbar, el último del grupo de documentos
  (Plano de taller · Presupuesto · Cliente · Clientes · **Stock**).

El botón lleva un **badge rojo** (`#tb-stock-badge`) con la cantidad
de items bajo el mínimo; si no hay alertas queda oculto. El click
abre o cierra el sheet, siempre en la pestaña **Tableros**.

> **Aviso de stock bajo.** Con `alertsAsToast` prendido (default), la
> primera evaluación con alertas muestra el aviso *"{n} items bajo el
> mínimo. Abrí Stock [T] para verlos."*. Después solo avisa por filas
> que *recién* cayeron bajo el mínimo, con un throttle de 5 s: banquear
> veinte retazos dispara un aviso, no veinte. Cuando una alerta se
> resuelve no dice nada — el badge que desaparece ya es la señal.

> **Dónde viven los datos.** IDB `despiece-stock` **v3**, con cuatro
> object stores: `boards`, `hardware`, `edgeBanding` y el libro de
> movimientos `movements`. Es inventario **del taller**, no del
> proyecto: no entra en el `.despiece` y vive en este dispositivo. La
> hidratación corre en `bootstrapStock()` (`src/ui/stock/panel.ts`)
> cuando el módulo está prendido, y de nuevo — sin costo — al abrir el
> sheet por primera vez.

## 2. Empty state y las tres pestañas

![Sheet Stock vacío con las tres pestañas](img/02-stock-empty.jpg)

Se abre un **panel sheet full-screen** con header `Stock del taller`.
Arriba a la derecha están todas las acciones del módulo:

| Botón | Qué hace |
|-------|----------|
| `+ Tablero` / `+ Herraje` / `+ Cinta` | Abre el drawer de alta del tab activo |
| `¿Me alcanza?` | Compara el pedido del proyecto abierto contra el stock (§10) |
| `Pedido recibido` | Suma un pedido al stock, una sola vez (§11) |
| `CSV` | Exporta el inventario completo a CSV (§12) |
| `Excel` | Exporta el inventario completo a XLSX (§12) |
| `×` | Cierra el sheet |

Debajo: el input de búsqueda, la banner de alertas (si hay) y las tres
pestañas con su contador:

- **Tableros (n)** — melamina/MDF/OSB típico Argentina/México (formato
  1830×2750 ARG / 1220×2440 MX / 4'×8' imperial).
- **Herraje (n)** — unidades de un código del catálogo de herrajería.
- **Cinta (n)** — rollos de canto por material × espesor × ancho × largo.

La tira de tabs es un `tablist` WAI-ARIA: las flechas, `Home` y `End`
mueven y cambian de pestaña. El tab al abrir es **siempre Tableros**;
el sheet no salta a la pestaña con más alertas.

> **Empty state.** Sin items cargados, la lista muestra *"No hay
> tableros en stock. Probá con "+ Tablero" arriba a la derecha."*
> (o *"No hay herraje en stock."* / *"No hay cinta en stock."*).
> Mientras el módulo lee IDB muestra *"Cargando el stock del
> taller…"*, para que un inventario cargado nunca se lea como vacío.

## 3. Cargar un tablero

![Drawer de alta de tablero](img/03-tableros-form.jpg)

Click en **+ Tablero** abre el **drawer** con un tablero en borrador.
Nada se escribe hasta que apretás **Guardar**: `Cancelar` o `Esc`
descartan el borrador sin dejar rastro.

| Campo | Default | Reglas |
|-------|---------|--------|
| Material | vacío | Obligatorio. `<datalist>` con el catálogo de materiales; también acepta texto libre |
| Espesor (mm) | `18` | Obligatorio, > 0 (rango 1–100, paso 0,5) |
| Largo (mm) | `1830` | Obligatorio, > 0 (rango 1–9999) |
| Ancho (mm) | `2750` | Obligatorio, > 0 (rango 1–9999) |
| Cantidad | `1` | Entero ≥ 0 |
| Mínimo | `defaultMinQuantity` de Configuración | Entero ≥ 0. Umbral de la alerta |
| $ unitario | vacío | ≥ 0, dos decimales. Por tablero |
| Lote | vacío | Trazabilidad de la entrega |
| Proveedor | vacío | "Masisa", "Faplac"… |
| Ubicación | vacío | "Dep.A-Estantería 3" |
| Recibido | **hoy** | Fecha (`YYYY-MM-DD`, local) |
| Notas | vacío | Texto libre |

> **Validación.** El material es obligatorio: sin él, el drawer queda
> abierto con *"Cargá un material."* en el pie y el foco en el campo.
> Las **medidas** se rechazan si no son positivas (no hay default
> seguro para el tamaño físico de un tablero). Las **cantidades** se
> corrigen en silencio: negativo, fracción o basura terminan en un
> entero ≥ 0.

> **Dos compras del mismo tablero son una sola fila.** El bucket de un
> tablero es *material + espesor + formato*: guardar un tablero que ya
> existe **suma** a la cantidad de esa fila en lugar de crear una
> gemela. El `Mínimo` sigue siendo el de la fila que ya estaba (es el
> piso que puso el taller, no una propiedad de la entrega), mientras
> `$ unitario`, `Proveedor` y `Recibido` toman el valor nuevo cuando
> viene. Un tablero del **mismo material y espesor en otro formato**
> es otra fila: es otra pila en el depósito.

El ID interno lleva prefijo `stkb:` (tablero), `stkh:` (herraje) o
`stke:` (cinta) más un UUID completo, para no colisionar con
identificadores de proyecto.

## 4. La lista: columnas, alertas y footer

![Lista de tableros con uno bajo mínimo](img/04-tableros-saved.jpg)

Tras guardar, la fila aparece en una lista de ocho columnas con
encabezado:

**Material** (o **SKU** en Herraje) · **Medidas** · **Cantidad** ·
**Mínimo** · **$ unitario** · **Proveedor** · **Ubicación** ·
**Acciones**.

Cuando `Cantidad < Mínimo` la fila se marca: `Cantidad` y `Mínimo` en
rojo, y la celda de mínimo pasa de `mín. 5` a `mín. 5 faltan 2`.

El **footer** del sheet resume la pestaña activa con tres datos
separados por `·`:

- `12 filas` — o `3 de 12 filas` cuando hay un filtro activo.
- `48 en stock` — la **suma de las cantidades**, no el número de filas.
- `Valorizado $ 384.000,00` — Σ `cantidad × $ unitario`. Si alguna fila
  visible no tiene precio dice `Valorizado $ … (parcial: faltan
  costos)`: el total es un piso, no el inventario completo.

Si hay alertas, arriba de las pestañas aparece una **banner** que dice
*"Hay {n} items bajo el mínimo."* con un botón **Ver solo los que
faltan**. Ese botón es el filtro: deja en la lista únicamente las filas
bajo el mínimo y cambia a **Ver todo el stock**. Si te vas a una
pestaña sin alertas con el filtro puesto, la lista dice *"Ningún item
de esta pestaña está bajo el mínimo."*; y cuando se cubre la última
alerta el filtro se suelta solo, para no dejarte mirando una lista
vacía sin salida.

> **Badge del topbar.** El badge rojo de `[T]` se actualiza en vivo con
> `stockEpoch`, que bumpea en cada alta, edición, ajuste o baja. El
> click en el botón abre o cierra el sheet; no salta a una pestaña en
> particular.

## 5. Cargar herraje

![Drawer de alta de herraje](img/05-herraje-form.jpg)

La pestaña **Herraje** funciona igual que Tableros, con dos
diferencias: el campo principal es **SKU** y no hay medidas.

| Campo | Default | Reglas |
|-------|---------|--------|
| SKU | vacío | Obligatorio, texto libre |
| Cantidad | `1` | Entero ≥ 0. Se muestra como `24 u.` |
| Mínimo | `defaultMinQuantity` | Entero ≥ 0 |
| $ unitario | vacío | ≥ 0. Por unidad |
| Proveedor / Ubicación / Recibido / Notas | igual que tablero | igual |

El bucket del herraje es el **SKU** normalizado: dos altas del mismo
código suman en la misma fila.

> **El SKU es texto libre, no un picker.** Ese campo apunta al código
> del catálogo de herrajería (`HerrajeItem.code`, que la fila de pedido
> llama `skuCode`), pero hoy **nadie valida ni resuelve ese FK**: no
> hay selector de catálogo y un rename o un borrado del lado del
> catálogo deja el código colgado en silencio. Copiá el código tal como
> está en el catálogo si querés que el pedido y el stock se encuentren.

## 6. Cargar cinta de canto (rollos)

![Drawer de alta de cinta](img/06-cinta-form.jpg)

| Campo | Default | Reglas |
|-------|---------|--------|
| Material | vacío | Obligatorio |
| Espesor (mm) | `2` | Obligatorio, > 0 (paso 0,01 — la cinta fina típica es 0,45) |
| Ancho (mm) | `22` | Obligatorio, > 0 (rango 1–999) |
| Largo (m) | `100` | Obligatorio, > 0. Largo **de un rollo** |
| Cantidad | `1` | Entero ≥ 0 — cantidad de **rollos** |
| Mínimo | `defaultMinQuantity` | Entero ≥ 0, en rollos |
| $ unitario | vacío | ≥ 0. **Por rollo** |
| Proveedor / Ubicación / Recibido / Notas | igual que tablero | igual |

> **Cantidad en rollos.** Tableros se cuentan en tableros, herraje en
> unidades y cinta en **rollos**: la lista muestra `4 rollos`. El BOM
> pide metros lineales; los metros que hay en el taller son
> `cantidad × largo del rollo`, y esa conversión la hacen las dos
> integraciones del pedido (§10 y §11).

El bucket de la cinta es *material + espesor + ancho + largo del
rollo*. Un `0,45 × 22` en rollos de 50 m es una fila distinta del mismo
canto en rollos de 100 m, porque son dos compras distintas.

## 7. Buscar y ordenar

![Búsqueda en el sheet Stock](img/07-search.jpg)

El input de arriba filtra la pestaña activa en vivo (debounce ~120 ms)
y cada pestaña recuerda su propio texto. Busca en:

- el **nombre** (material o SKU),
- el **espesor y el formato** — por eso `18` o `1830` encuentran
  tableros, aunque no sean el nombre,
- **proveedor** y **ubicación**,
- el **lote** (solo tableros).

El matching normaliza acentos (`normalizeForSearch`: NFD + strip
diacríticos + minúsculas), así que `"PETIRIBÍ"` matchea `"petiribi"`.
Es sub-string, no fuzzy.

Cuando la búsqueda no encuentra nada, la lista dice *"Ningún item
coincide con la búsqueda. Probá con menos letras."* — un estado
distinto del inventario vacío, para que 200 filas escondidas detrás de
un filtro no se lean como "no tenés nada" e invitar a cargarlo dos
veces.

Los encabezados **Material**, **Cantidad**, **Mínimo** y **$ unitario**
son botones de orden; `Medidas`, `Proveedor`, `Ubicación` y `Acciones`
no ordenan. El ciclo del click es de tres pasos: ordenar → invertir →
volver al orden de carga. `Mínimo` ordena por **déficit** y arranca
descendente, porque la punta urgente de esa columna son los números
grandes. La flecha (`▲` / `▼`) y el `aria-sort` se actualizan en el
lugar, así que el foco se queda en el encabezado que apretaste.

> El atajo `/` **no** aplica acá: el input es local al sheet y no
> comparte el filtro global de la sidebar 2D.

## 8. Editar en el drawer

![Drawer de edición con todos los campos](img/08-drawer-edit.jpg)

**Click en cualquier parte de la fila** — o en el botón `✎` — abre el
**drawer** lateral con los campos del item, pre-rellenados. No es un
modal: es una región dentro del sheet, con header, cuerpo y un pie con
**Cancelar** / **Guardar**. `Enter` dentro de un campo también guarda.

- Los mensajes de error salen **en el pie del drawer**, con el foco en
  el campo que los causó. No van a la barra de estado de la app, que
  queda detrás del sheet y de su scrim.
- Vaciar un campo opcional lo **borra** de verdad (proveedor,
  ubicación, lote, notas, `$ unitario`).
- Guardar persiste a IDB, bumpea `stockEpoch` y — si cambió la
  cantidad — anota el movimiento en el libro (§9).

> **Cambios sin guardar.** `Esc`, el backdrop o el `×` con el drawer
> sucio preguntan *"¿Descartar los cambios?"* con tres salidas: **Seguir
> editando**, **Descartar** y **Guardar**. `Esc` cierra primero el
> drawer y solo después el sheet.

El botón `×` de la fila elimina el item, preguntando *"¿Eliminar este
item del stock?"*. **No hay selección múltiple**: no hay checkbox de
fila, ni borrado en lote, ni "asignar mínimo" masivo.

## 9. Ajuste rápido `±` y el libro de movimientos

![Ajuste rápido con los botones ± de la fila](img/09-ajuste-rapido.jpg)

Cada fila tiene un `−` y un `+` que mueven la cantidad de a uno: es la
operación de todos los días ("usé dos tableros hoy"), que antes eran
cinco interacciones a través del drawer. El `−` queda **deshabilitado
en 0** (el taller no puede tener una pila negativa) y el foco se queda
en el botón que apretaste, o pasa al `+` cuando el `−` se apagó.

Cada escritura del módulo — alta, edición de cantidad, ajuste `±`,
baja, retazo banqueado, pedido recibido — anota una entrada en el
**libro de movimientos** (store `movements` de la IDB v3):

| Campo | Significado |
|-------|-------------|
| `at` | Cuándo (epoch ms) |
| `kind` + `recordId` | Qué fila |
| `delta` | Con signo: `+` entró, `−` salió |
| `reason` | `manual` · `count` · `purchase` · `offcut` · `consumption` · `reversal` |
| `ref` | Motivo en texto libre, o la referencia del pedido / del proyecto de origen |

> **El libro es append-only y el undo es por compensación.** Deshacer
> un movimiento no edita la historia: mueve la cantidad por `−delta` y
> **agrega** un movimiento `reversal` que apunta al original. Así
> "esto se corrigió dos veces y después se volvió atrás" se sigue
> leyendo un año después. Si la pila se achicó en el medio, la cantidad
> se clampea en 0 y el libro anota lo que pasó de verdad, no un
> `−delta` mecánico.

> **Todavía no hay pantalla del libro.** El libro se escribe siempre y
> es lo que hace que "Pedido recibido" no se pueda contar dos veces
> (§11), pero por ahora no hay una vista en la UI que lo liste. Tampoco
> hay un modal de "Ajustar" con motivo obligatorio: el `±` escribe el
> movimiento con el motivo que corresponde a la operación.

## 10. ¿Me alcanza? — el reporte de faltantes

![Reporte de faltantes contra el pedido del proyecto](img/10-faltantes.jpg)

El botón **¿Me alcanza?** del header abre un modal por encima del
sheet que compara **el pedido de materiales del proyecto abierto**
contra el inventario del taller. Es una foto del momento en que lo
abrís, no una vista viva.

La tabla tiene cinco columnas — **Item** · **Pide** · **Hay** ·
**Falta** · **Estado** — y las filas van tableros → herraje → cinta,
con las unidades de cada tipo (`tableros`, `u.`, `m`). El estado es
uno de tres: **Alcanza**, **Falta** o **No hay en stock**.

Cómo compara cada tipo:

- **Tableros** — por bucket exacto (material + espesor + formato). Los
  tableros del mismo material y espesor **en otra medida** no cuentan
  como cobertura: aparecen como *"{n} en otro formato"* al lado de lo
  que hay, con la aclaración *"Puede que sirvan: eso lo decide el
  nesteo, no el stock."*
- **Herraje** — por SKU.
- **Cinta** — el pedido pide **metros** y el stock guarda **rollos**,
  así que los metros que hay son `cantidad × largo del rollo`. Además
  el pedido no guarda el ancho, así que los rollos de distinto ancho se
  suman juntos: el reporte lo aclara al pie.

El pie resume: *"Alcanza todo: los {n} items del proyecto están
cubiertos."* o *"Falta comprar: {x} incompletos y {y} sin nada en
stock."*, más **lo que cuesta lo que falta**. Si algún faltante no
tiene precio cargado dice *"Lo que falta no se puede valorizar: al
menos un item no tiene precio cargado."* en lugar de mentir un
`$ 0,00`.

> **Pedir lo que falta.** Cuando hay algo faltando aparece el botón
> **Pedir lo que falta**, que exporta el **mismo CSV de Pedido
> materiales** de siempre (`pedido-<modelo>.csv`, UTF-8 con BOM y
> separador `;`) pero recortado a las cantidades que faltan y
> re-preciado, así la plata coincide con lo que realmente vas a
> comprar. El resultado se confirma en el pie del modal: *"Pedido
> exportado: {archivo}"*.
>
> El herraje que falta **suma al total** del pedido pero no lleva
> filas propias en ese CSV: el reporte lo dice explícitamente.

> **Requisitos.** Necesita un proyecto abierto y con piezas: si no,
> el modal explica cuál de las tres cosas falta (no hay proyecto, no
> hay piezas para exportar — mirá "Solo visibles" en Configuración —,
> o el proyecto todavía no pide materiales).

## 11. Marcar pedido recibido

![Modal de pedido recibido con líneas tildables](img/11-pedido-recibido.jpg)

El botón **Pedido recibido** lee el mismo pedido del proyecto y lo
ofrece para sumarlo al stock. La idea es no volver a tipear catorce
líneas de remito en un formulario: *"Destildá lo que no llegó y corregí
las cantidades antes de confirmar. Cada línea tildada suma al stock y
queda registrada como compra."*

Arriba dice `Pedido: <referencia>`. La tabla tiene cuatro columnas:

| Columna | Qué muestra |
|---------|-------------|
| **Llegó** | Checkbox. Viene tildado solo si la línea se pudo resolver |
| **Item** | El material o el SKU y, si no se resolvió, el motivo en palabras |
| **Cantidad** | Editable, con la unidad (`tableros` · `u.` · `rollos`) |
| **Destino** | *"suma a lo que ya hay"* o *"fila nueva"* |

Una línea que el módulo no puede resolver se muestra igual, sin
tildar, con el motivo: *"El pedido no trae cantidad para esta línea."*
o *"La línea no trae material ni SKU con el que identificarla."*. Al
pie se cuenta cuántas son: *"{n} líneas necesitan que completés algo
antes de poder tildarlas."*

> **La cinta necesita ancho y largo de rollo.** El pedido mide la
> cinta en **metros** y el stock la cuenta en **rollos**, así que una
> línea de canto no se puede tildar hasta que sepamos las dos cosas:
> *"La cinta se compra en metros y se guarda en rollos: cargá el ancho
> y el largo del rollo para poder tildarla."* La fila trae dos campos
> para eso, **Ancho (mm)** y **Largo del rollo (m)**; si ya tenés en el
> taller un rollo de ese material y espesor, el largo se toma de ahí
> solo. Con los dos datos, los rollos son `techo(metros / largo del
> rollo)` — siempre para arriba. Completar un campo re-planifica esa
> línea contra el stock actual.

Al confirmar, cada línea tildada entra por el camino de merge (suma a
la pila que ya existe en lugar de crear una fila gemela), se estampa
con la fecha de hoy y con la referencia del pedido en el campo `Lote`,
y anota un movimiento `purchase` con esa referencia. El resultado sale
en el footer del sheet: *"Pedido recibido: {líneas} líneas, {unidades}
unidades sumadas al stock."* Si tildaste algo que no se podía
resolver, lo aclara: *"{n} líneas tildadas quedaron afuera: les falta
el dato que las identifica."*

> **Idempotente por pedido: un remito no se puede contar dos veces.**
> La referencia del pedido es un digest de sus propias líneas
> (material, espesor, formato, cantidades) más el nombre del proyecto,
> y queda escrita en el libro de movimientos, que es lo único que
> sobrevive a un reload, a una segunda pestaña y a reabrir el proyecto.
> Si ya existe un movimiento `purchase` con esa referencia, el modal
> muestra la banner *"Este pedido ya se marcó como recibido el
> {fecha}. Para no contar doble, no se puede volver a confirmar."* y
> **Confirmar recepción** queda deshabilitado. El chequeo corre dos
> veces — al abrir y otra vez justo antes de escribir — así que si dos
> modales corren carrera, los dos pierden.
>
> Como las **cantidades** son parte del digest, una entrega parcial no
> se puede completar después por acá: el resto entra por el drawer o
> por el `±`, que es la forma honesta de decir "llegó la mitad".

> **Herraje.** Las líneas de herraje llegan al modal solo si el
> proyecto tiene herrajería cargada **y con precio**: sin total de
> catálogo, el herraje no aparece ni acá ni en el reporte de faltantes.

## 12. Configuración, integraciones y exports

![Configuración → Stock](img/12-settings-stock.jpg)

`Configuración → Stock` vive en el grupo **docs** del panel de
configuración (junto a Plano de taller, Presupuesto, Clientes, Corte,
Taller y Catálogo) y tiene cuatro campos:

| Campo | Default | Significado |
|-------|---------|-------------|
| `stockEnabled` | **`false`** | Master switch, **opt-in**. Apagado esconde el botón `[T]` y el atajo; los items guardados se preservan |
| `defaultMinQuantity` | `0` | `Mínimo` que arranca en los items nuevos (se clampea a 0–9999) |
| `alertsAsToast` | `true` | Además del badge, avisar cuando aparecen items nuevos bajo el mínimo |
| `autoAddOffcuts` | `true` | Los retazos que sumás al inventario del nest también entran a esta tabla |

### Retazos del nest → stock

Con el módulo prendido y `autoAddOffcuts` en `true`, el botón **Sumar
al stock** del nest banca los retazos en tres lugares, y uno de ellos
es este inventario. Cada retazo entra por el camino de merge (suma
cuando coinciden material, espesor y formato; otra medida es otra
fila), se estampa con la fecha de hoy y anota un movimiento `offcut`
con el proyecto de origen como referencia. La acción es idempotente por
resultado de nest: apretar el botón dos veces no duplica nada.

El `Mínimo` de un retazo queda a propósito en 0: un retazo es scrap que
el taller tiene, no una línea que repone, y un piso distinto de cero
levantaría una alerta imposible de bajar.

### Stock → nest, y por qué el modo Costo lo necesita

En el workbench de nesting, la columna **Setup** tiene una sección
**Stock del taller** que lista los tableros reales con `en stock: {n}`
y `$ {precio} c/u`, y un botón **Traer** que los suma a los formatos de
corte (la cantidad se clampea a lo que hay). *Traerlos no descuenta
stock*: un nest es un plan, no un consumo.

Ese camino es el **único** que le pone precio a un tablero del nest, y
por eso es el que hace funcionar `Optimizar: Costo (menor precio
total)`. Sin tableros con precio, el modo Costo no tiene con qué
comparar y se comporta como Material; el workbench lo dice — *"Falta
precio en {n} tablero(s)"* y *"Modo Costo activo pero ningún tablero
tiene precio — cayó a Material."*

### Exports

Los dos botones del header exportan **todo el inventario**, no la
pestaña activa. El nombre del archivo usa el nombre del taller (o
`taller`) y la fecha local: `<taller>-stock-<YYYY-MM-DD>`.

**CSV** — un archivo, los tres sub-stores discriminados por la columna
`kind`. UTF-8 con BOM, fin de línea CRLF, separador **`;`** y decimales
con **coma**, que es lo que espera Excel en es-AR. Los encabezados son
los nombres de campo (formato de intercambio, no reporte traducido) y
son **16, sin `id`**:

```
kind;materialKey;skuId;thicknessMm;widthMm;formatX;formatY;lengthM;quantity;minQuantity;unitCost;supplier;location;lotRef;receivedAt;notes
board;Melamina MDP · Blanco TX;;18;;1830;2750;;12;5;8200;Masisa;Dep.A-E3;L-2291;2026-08-12;
hardware;;BIS-BLUM-110;;;;;;24;10;1500;Häfele;Est.2;;2026-08-05;
edgeBanding;Melamina Blanco TX;;0,45;22;;;100;5;2;850;Rehau;Dep.B;;;
```

Cada fila deja vacías las columnas que no le corresponden: un herraje
no tiene medidas, un tablero no tiene `lengthM`.

**Excel** — un libro con tres hojas (`Tableros` / `Herraje` / `Cinta
de canto`), encabezado congelado y en negrita, formatos numéricos por
columna y una fila **TOTAL** con Σ cantidad y Σ `cantidad × $
unitario`. Acá los encabezados **sí** están traducidos.

### Atajos

| Atajo | Acción |
|-------|--------|
| `Shift+T` | Abre / cierra el sheet Stock |
| Click en `[T]` | Lo mismo |
| `Esc` | Cierra el drawer; si no hay drawer, cierra el sheet |
| `←` `→` `↑` `↓` `Home` `End` | Mueven entre las tres pestañas |
| `Enter` en un campo del drawer | Guarda |

> **Cómo se refresca el sheet.** El armazón se construye una sola vez
> al abrir; después cada cambio parchea solo la parte que cambió
> (header, banner, tabs, lista + footer, drawer). El re-render está
> condicionado a que haya cambiado `stockEpoch` o el estado de carga,
> así que un `setState` ajeno — un aviso que se apaga cuatro segundos
> después — ya no puede tocar el sheet. Y si el foco está adentro del
> sheet, el re-render se posterga hasta que el foco se va: nunca te
> borra un formulario a medio tipear. `F5` recarga y `bootstrapStock()`
> vuelve a hidratar desde IDB — los items sobreviven al refresh.

---

## Ver también

- [Tutorial · Optimización de corte](optimizacion-corte.html) — el
  nest: traer tableros reales del taller, banquear retazos y el modo
  Costo.
- [Tutorial · Herrajes](herrajes.html) — el catálogo de herrajería, de
  donde sale el código que va en el campo SKU.
- Spec
  [`2026-07-26-stock-inventario.md`](../superpowers/specs/2026-07-26-stock-inventario.md)
  — diseño del módulo, schema, integraciones.
- Plan
  [`2026-08-25-stock-audit-and-sota.md`](../superpowers/plans/2026-08-25-stock-audit-and-sota.md)
  — la auditoría que rehizo el módulo y las enmiendas a la spec.
