Saltar para o conteúdo
PMPaulo Mota
Voltar à demo

Como foi feito

Stock, por dentro

Dois conceitos que fazem a diferença: a quantidade nunca se escreve à mão, e o consumo escolhe o lote, não o utilizador.

01

O problema

O stock de um restaurante vive num quadro branco ou na cabeça do chefe. Sabe-se quanto frango há, mais ou menos. Não se sabe qual dos três lotes expira amanhã, e quando alguém abre a câmara tira do que está mais à mão, que é o que chegou ontem. O de anteontem estraga-se ao fundo. O desperdício não se mede porque nunca foi registado, por isso parece que não existe.

02

Modelo de dados

Ingredientes, fornecedores, preços e receitas são dados de referência, partilhados com as compras e o food cost. Os lotes pertencem a uma sala: cada um sabe quanto entrou, quanto resta, a que custo, a validade e onde está. E cada alteração a um lote é uma linha na tabela de movimentos. A quantidade do lote é um número derivado; o histórico é a verdade.

TabelaPara que serve
demo_ingredientsO que a casa compra: nome, unidade e categoria. Partilhado com as compras e o food cost.
demo_suppliersQuem fornece, com prazo de entrega e encomenda mínima. Partilhado com as compras.
demo_recipesAs fichas técnicas: doses, preço de venda e as linhas de ingredientes. Partilhado com o food cost.
demo_stock_batchesUm lote por entrada: quanto entrou, quanto resta, custo, validade e local. A quantidade é derivada dos movimentos.
demo_stock_movementsCada alteração ao stock, com tipo, quantidade, motivo e referência. Nunca se apaga.
sql
-- A quantidade do lote nunca se escreve à mão.
-- Cada alteração é uma linha aqui; o lote é a soma.
create table demo_stock_movements (
  batch_id   uuid references demo_stock_batches(id),
  kind       demo_movement_kind not null,   -- entrada, consumo, desperdicio, ajuste
  qty        numeric(9,3) not null check (qty > 0),
  reason     demo_waste_reason,
  ref        text,                          -- a receita, a guia, o motivo
  created_at timestamptz default now(),
  -- Desperdício sem motivo não é desperdício, é um buraco no stock.
  constraint demo_waste_reason_needed
    check (kind <> 'desperdicio' or reason is not null)
);
03

O consumo escolhe o lote

Quando a cozinha pede seis quilos de frango, o browser não diz de onde tirar. A função percorre os lotes desse ingrediente por validade, do que expira primeiro para o que expira depois, e tira de cada um até perfazer. Se o primeiro lote só tem quatro, tira quatro dele e dois do seguinte, e escreve um movimento por cada. Um lock por sala e ingrediente impede dois pedidos simultâneos de tirar o mesmo quilo. Lotes sem validade vão para o fim: é o FIFO dentro do FEFO.

sql
-- O browser não diz de onde tirar. A função percorre os lotes
-- por validade, do que expira primeiro para o que expira depois.
perform pg_advisory_xact_lock(hashtext(p_room || '|stock|' || p_ingredient));

for v_batch in
  select * from demo_stock_batches
   where room_code = p_room and ingredient_id = p_ingredient and qty > 0
   order by expiry asc nulls last, received_at asc
   for update
loop
  exit when v_left <= 0;
  v_take := least(v_batch.qty, v_left);

  update demo_stock_batches set qty = qty - v_take where id = v_batch.id;
  insert into demo_stock_movements (batch_id, kind, qty, ref)
  values (v_batch.id, 'consumo', v_take, p_ref);

  v_alloc := v_alloc || jsonb_build_object(
    'batch', v_batch.code, 'qty', v_take, 'left', v_batch.qty - v_take);
  v_left := v_left - v_take;
end loop;

return v_alloc;   -- exactamente de que lote saiu cada quantidade
04

Cozinhar é consumir por receita

Cozinhar vinte doses de frango assado converte-se nos ingredientes da receita, escalados pelas doses, e cada um passa pela função de consumo. É tudo ou nada: se faltar azeite, não se tira o frango. A função devolve exactamente de que lote saiu cada quantidade, e é isso que o ecrã mostra, para ninguém ter de acreditar na palavra do sistema.

sql
-- Cozinhar vinte doses converte-se nos ingredientes da receita,
-- escalados pelas doses, cada um pela função de consumo.
for v_line in select * from demo_recipe_lines where recipe_id = p_recipe
loop
  v_need := round(v_line.qty * p_portions / v_recipe.portions, 3);
  v_out := v_out || jsonb_build_object(
    'ingredient', v_line.name_pt, 'needed', v_need,
    'allocation', demo_stock_consume(p_room, v_line.ingredient_id, v_need, v_recipe.name_pt));
end loop;

-- Tudo ou nada: a função corre numa transacção. Se o azeite faltar
-- na terceira linha, o frango da primeira volta atrás.
05

Desperdício com motivo

Registar desperdício sem motivo é recusado por uma restrição na tabela, não por um aviso no ecrã. Sem motivo, o número não serve para nada: não se sabe se o problema é a validade, a preparação ou a compra a mais. Com motivo, o painel mostra onde está a perder dinheiro, em euros, ao custo real do lote.

06

O que faria diferente num cliente real

Leitor de código de barras na recepção, para o lote e a validade entrarem sem escrever. Ligação ao KDS, para o consumo acontecer quando o prato sai e não quando alguém se lembra. Inventário físico periódico com ajustes assinados, porque a realidade nunca bate certo com o sistema. E autenticação: aqui qualquer visitante recebe e deita fora, porque é uma demonstração.