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 módulo vive en un panel sheet full-screen que se
abre con Shift+T o el botón [T]
del topbar, el último del grupo de documentos.
despiece-stock v3 con
cuatro object stores (boards · hardware
· edgeBanding · movements).
Workshop-global, no per-project (igual que el catálogo de
herrajería y la agenda de clientes). Los IDs internos llevan
prefijo stkb: / stkh: /
stke: para evitar colisión con proyectos. El módulo
es opt-in: stockEnabled arranca en
false.
01 Activar y abrir el sheet [T] Stock
[K] Clientes. El badge rojo muestra el conteo de items bajo el mínimo; queda oculto cuando no hay alertas.
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.
Con el módulo prendido 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.
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.
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.
02 Empty state y las tres pestañas
¿Me alcanza?, Pedido recibido, CSV y Excel.
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 (paso 10) |
Pedido recibido | Suma un pedido al stock, una sola vez (paso 11) |
CSV | Exporta el inventario completo a CSV (paso 12) |
Excel | Exporta el inventario completo a XLSX (paso 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.
03 Cargar un tablero
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 | 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 |
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.
04 La lista: columnas, alertas y footer
Cantidad y Mínimo en rojo y suma faltan n. El footer resume filas, cantidad y valorización.
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— o3 de 12 filascuando 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 diceValorizado $ … (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.
[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.
05 Cargar herraje
SKU — el código del catálogo de herrajería — y no hay medidas.
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.
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.
06 Cargar cinta de canto (rollos)
| 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 |
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 (pasos 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.
07 Buscar y ordenar
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
18o1830encuentran 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.
08 Editar en el drawer
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
stockEpochy — si cambió la cantidad — anota el movimiento en el libro (paso 9).
× 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.
09 Ajuste rápido ± y el libro de movimientos
− y + mueven la cantidad de a uno y anotan el movimiento; − queda deshabilitado en 0.
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 |
−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.
± escribe el movimiento con el motivo que corresponde a la operación.
10 ¿Me alcanza? — el reporte de faltantes
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.
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.
11 Marcar pedido recibido
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."
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."
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".
12 Configuración, integraciones y exports
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 |
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 — el nest: traer tableros reales del taller, banquear retazos y el modo Costo.
- Tutorial · Herrajes — 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— diseño del módulo, schema, integraciones. - Plan
2026-08-25-stock-audit-and-sota.md— la auditoría que rehizo el módulo y las enmiendas a la spec.