Saltar para o conteúdo
PMPaulo Mota
Voltar à demo

Como foi feito

Food cost, por dentro

Uma função que custeia cada ficha ao preço de hoje, e um cenário que corre no browser porque não é um registo.

01

O problema

O preço do prato foi decidido há dois anos com o frango a cinco euros. O frango está a seis e vinte e ninguém recalculou, porque recalcular é abrir a folha de cálculo, procurar a factura mais recente, copiar o preço, e fazer isto para quarenta pratos. Não se faz. O food cost real só aparece no fim do mês, como uma surpresa.

02

Modelo de dados

Não há tabelas novas. As receitas e as linhas já existiam do stock; os preços com data já existiam das compras. O food cost é o que se vê quando se juntam as duas. É por isso que os quatro sistemas foram construídos sobre os mesmos ingredientes: uma subida do frango no fornecedor chega ao prato sem ninguém copiar nada.

TabelaPara que serve
demo_ingredientsPartilhada com o stock e as compras: nome, unidade, categoria, validade.
demo_supplier_pricesPartilhada com as compras: cada preço de cada fornecedor, com data. Nunca se apaga; junta-se o novo.
demo_recipesA ficha: doses, preço de venda, food cost alvo, vendas de demonstração.
demo_recipe_linesPartilhada com o stock: quanto de cada ingrediente entra num lote.
03

Que preço é o preço

O custo de cada linha usa o preço de referência do ingrediente: o mais baixo entre os preços actuais de todos os fornecedores. É o que um comprador atento pagaria hoje. Podia ser o último preço pago, ou o do fornecedor habitual: são decisões de negócio, e a função troca-se numa linha. O que não muda é que o preço vem de uma tabela com data, nunca de um número escrito na ficha.

sql
-- O preço de referência de um ingrediente: o mais baixo entre os
-- preços actuais de cada fornecedor. Vem de uma tabela com data,
-- nunca de um número escrito na ficha.
create or replace function demo_ingredient_price(p_ingredient integer)
returns integer language sql stable as $$
  select min(price_cents) from (
    select distinct on (supplier_id) price_cents
      from demo_supplier_prices
     where ingredient_id = p_ingredient
     order by supplier_id, since desc, id desc
  ) latest;
$$;
04

As contas

Custo do lote é a soma das linhas, quantidade vezes preço. Custo por dose é o lote a dividir pelas doses. Food cost é o custo por dose sobre o preço de venda. Margem bruta é o preço menos o custo por dose: antes de pessoal, renda e IVA. Apresentar isto como lucro seria mentir, e o ecrã diz isso ao lado do número.

sql
-- Uma função custeia todas as fichas ao preço de hoje.
-- Lote = soma das linhas; dose = lote / doses; food cost = dose / preço.
create or replace function demo_recipe_costs()
returns table (recipe_id int, batch_cents int, portion_cents int,
               food_cost_pct numeric, margin_cents int)
language sql stable as $$
  with batch as (
    select r.id, r.portions, r.price_cents,
           sum(round(l.qty * demo_ingredient_price(l.ingredient_id)))::int as batch_cents
      from demo_recipes r join demo_recipe_lines l on l.recipe_id = r.id
     group by r.id
  )
  select id, batch_cents,
         round(batch_cents / portions)::int,
         round(100.0 * (batch_cents / portions) / price_cents, 1),
         price_cents - round(batch_cents / portions)::int
    from batch;
$$;
05

O cenário corre no browser

Mexer no preço do frango e ver o prato mudar não é um registo: é uma pergunta. Por isso corre no browser, sobre os mesmos números, sem escrever nada. O preço de venda sugerido para manter a margem é uma conta, custo a dividir por um menos o food cost alvo, e o ecrã chama-lhe cálculo, não decisão. Decidir o preço é do dono.

typescript
// O cenário corre no browser: os mesmos números, nada gravado.
const unit = override[line.ingredient_id] ?? referencePrice(line.ingredient_id);
const lineCents = Math.round(line.qty * unit);
const portion = Math.round(batch / recipe.portions);
const foodCost = (portion / recipe.price_cents) * 100;

// Preço para manter o food cost alvo: uma conta, não uma decisão.
const suggested = portion / (1 - recipe.target_pct / 100);
06

O que faria diferente num cliente real

Perdas de preparação por ingrediente, porque um quilo de cebola não são mil gramas de cebola picada. Versões da ficha, para saber o que mudou e quando. Sub-receitas, o molho que entra em cinco pratos. Vendas reais a vir da caixa em vez de números de demonstração. E um aviso quando um prato passa do food cost alvo, em vez de esperar que alguém abra o painel.