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.

Los datos viven en IDB 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

Topbar con el botón [T] y badge de alertas
01 Botón [T] en el topbar. Último del grupo de documentos, después de [K] Clientes. El badge rojo muestra el conteo de items bajo el mínimo; queda oculto cuando no hay alertas.
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.

Con el módulo prendido hay dos formas de abrir el sheet:

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.

02 Empty state y las tres pestañas

Sheet Stock vacío con las tres pestañas
02 Sheet vacío con tabs y acciones. El header expone el alta del tab activo más ¿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ónQué hace
+ Tablero / + Herraje / + CintaAbre el drawer de alta del tab activo
¿Me alcanza?Compara el pedido del proyecto abierto contra el stock (paso 10)
Pedido recibidoSuma un pedido al stock, una sola vez (paso 11)
CSVExporta el inventario completo a CSV (paso 12)
ExcelExporta 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:

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.

03 Cargar un tablero

Drawer de alta de tablero
03 Drawer de alta de tablero. Material, espesor, formato (largo × ancho), cantidad, mínimo, costo unitario, lote, proveedor, ubicación, fecha de recibido y notas.

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.

CampoDefaultReglas
MaterialvacíoObligatorio. <datalist> con el catálogo de materiales; también acepta texto libre
Espesor (mm)18Obligatorio, > 0 (rango 1–100, paso 0,5)
Largo (mm)1830Obligatorio, > 0 (rango 1–9999)
Ancho (mm)2750Obligatorio, > 0 (rango 1–9999)
Cantidad1Entero ≥ 0
MínimodefaultMinQuantityEntero ≥ 0. Umbral de la alerta
$ unitariovacío≥ 0, dos decimales. Por tablero
LotevacíoTrazabilidad de la entrega
Proveedorvacío"Masisa", "Faplac"…
Ubicaciónvacío"Dep.A-Estantería 3"
RecibidohoyFecha (YYYY-MM-DD, local)
NotasvacíoTexto 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.

04 La lista: columnas, alertas y footer

Lista de tableros con uno bajo mínimo
04 Lista con encabezados y alerta. La fila bajo el mínimo pinta 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 ·:

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.

05 Cargar herraje

Drawer de alta de herraje
05 Drawer de herraje. El campo principal es 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.

CampoDefaultReglas
SKUvacíoObligatorio, texto libre
Cantidad1Entero ≥ 0. Se muestra como 24 u.
MínimodefaultMinQuantityEntero ≥ 0
$ unitariovacío≥ 0. Por unidad
Proveedor / Ubicación / Recibido / Notasigual que tableroigual

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.

06 Cargar cinta de canto (rollos)

Drawer de alta de cinta
06 Drawer de cinta en rollos. Espesor (0,45 o 2), ancho, largo del rollo y cantidad en rollos — no en metros.
CampoDefaultReglas
MaterialvacíoObligatorio
Espesor (mm)2Obligatorio, > 0 (paso 0,01 — la cinta fina típica es 0,45)
Ancho (mm)22Obligatorio, > 0 (rango 1–999)
Largo (m)100Obligatorio, > 0. Largo de un rollo
Cantidad1Entero ≥ 0 — cantidad de rollos
MínimodefaultMinQuantityEntero ≥ 0, en rollos
$ unitariovacío≥ 0. Por rollo
Proveedor / Ubicación / Recibido / Notasigual que tableroigual
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 (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

Búsqueda en el sheet Stock
07 Búsqueda en vivo y encabezados ordenables. El input filtra por nombre, medidas, proveedor, ubicación y lote con normalización de acentos.

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 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.

08 Editar en el drawer

Drawer de edición con todos los campos
08 Drawer lateral de edición. Header con el título del item, cuerpo con los campos del schema, pie con Cancelar / Guardar y el mensaje de error inline.

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.

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.

09 Ajuste rápido ± y el libro de movimientos

Ajuste rápido con los botones ± de la fila
09 Ajuste rápido por fila. 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):

CampoSignificado
atCuándo (epoch ms)
kind + recordIdQué fila
deltaCon signo: + entró, salió
reasonmanual · count · purchase · offcut · consumption · reversal
refMotivo 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 (paso 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
10 ¿Me alcanza para este proyecto? Item · Pide · Hay · Falta · Estado, con el resumen y el costo de lo que falta al pie.

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:

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
11 Marcar pedido recibido. Llegó · Item · Cantidad · Destino, con la referencia del pedido arriba y la recepción idempotente.

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:

ColumnaQué muestra
LlegóCheckbox. Viene tildado solo si la línea se pudo resolver
ItemEl material o el SKU y, si no se resolvió, el motivo en palabras
CantidadEditable, 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
12 Configuración → Stock. Cuatro campos en el grupo docs: master switch opt-in, mínimo por defecto, alertas como aviso y auto-agregar retazos.

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:

CampoDefaultSignificado
stockEnabledfalseMaster switch, opt-in. Apagado esconde el botón [T] y el atajo; los items guardados se preservan
defaultMinQuantity0Mínimo que arranca en los items nuevos (se clampea a 0–9999)
alertsAsToasttrueAdemás del badge, avisar cuando aparecen items nuevos bajo el mínimo
autoAddOffcutstrueLos 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 están traducidos.

Atajos

AtajoAcción
Shift+TAbre / cierra el sheet Stock
Click en [T]Lo mismo
EscCierra el drawer; si no hay drawer, cierra el sheet
Home EndMueven entre las tres pestañas
Enter en un campo del drawerGuarda
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