Pular para o conteúdo principal

Rateio de Materiais de Consumo Compartilhado

Aprovado2026-07-28

Problema

Toda peça ou insumo solicitado em uma ordem de serviço (OS) é apropriado diretamente ao custo da frota daquela OS. Isso funciona bem para peças específicas — um filtro trocado no PN-0007 é custo do PN-0007.

Mas existem materiais de uso compartilhado que não servem a um único ativo:

MaterialComo é consumidoPor que não é de um ativo só
GraxaAplicada em vários veículos ao longo do mêsUm balde lubrifica a frota inteira
Disco de corteGasto em múltiplos reparosServe à oficina, não a uma OS
Arame MIG (solda)Consumido em qualquer soldaNão há como medir por veículo
Estopa, lixa, produtos de limpezaConsumo difusoUso geral da oficina

Hoje esse consumo cai 100% na frota da OS onde o material foi solicitado. Um balde de graxa pedido numa OS do Veículo A é debitado integralmente no Veículo A, mesmo tendo lubrificado outros dez veículos.

Resultado: o custo por ativo e o CPK (custo por km) ficam distorcidos. Um veículo aparece artificialmente caro, outros aparecem baratos demais, e decisões gerenciais — renovação de frota, priorização de manutenção, negociação com transportadora — partem de um número errado.


Fluxo Atual

Como o custo de peça deveria fluir

O modelo de dados já rastreia a cadeia completa peça -> OS -> frota/veículo:

Part.costPrice
└─ PartRequest (quantity / approvedQuantity) ← baixa de estoque na aprovação
└─ WorkOrder.fleetId ← OS amarrada a uma frota
└─ WorkOrderAsset.vehicleId ← e a um veículo específico

Os dashboards de custo por frota, custo por veículo, CPK e centro de custo leem os campos denormalizados da OS: partsCost, labourCost, tireCost, externalCost, totalCost.

Como o consumível é tratado hoje

Consumíveis já têm um comportamento distinto no sistema, controlado pela flag isConsumable na categoria da peça:

  • A PartRequest de um consumível fica sem serviceExecutionId — ou seja, sem localização de veículo/eixo. Ela é auto-atribuída a um serviço de sistema chamado "Consumíveis Diversos".
  • Peças não-consumíveis, ao contrário, exigem um serviceExecutionId e herdam dele o veículo/eixo.

Ou seja, o consumível já não aponta para um veículo específico. Mas ele continua preso a uma única OS, que por sua vez está amarrada a uma única frota. O custo, portanto, ainda cai integralmente naquela frota.

:::warning Descoberta importante durante a análise A materialização do custo de peça na OS ainda não existe em produção. Hoje os campos partsCost / totalCost da WorkOrder são preenchidos por um seed com valores aleatórios (prisma/seed-work-order-costs.ts) — nenhum use case recalcula partsCost a partir das PartRequest reais. O custo real das peças (approvedQuantity × costPrice) só é computado em relatórios de consumo/curva ABC, que não são segmentados por frota nem por veículo.

Consequência para esta proposta: o rateio não deve ser construído sobre um mecanismo que ainda não existe. A decisão de rateio precisa ser tomada agora e embutida no momento em que a apropriação real de custo (PartRequest -> WorkOrder.partsCost) for implementada — que é um passo natural e próximo. São duas faces do mesmo trabalho. :::


Proposta

Conceito em uma frase

Classificar cada material como Direto ou Compartilhado. Material direto continua sendo apropriado ao ativo/frota da OS. Material compartilhado não vai para uma frota única: seu custo entra num pool de manutenção e é rateado entre os ativos por um critério justo.

As três peças da solução

PeçaO que éDecisão envolvida
1. ClassificaçãoMarcar o material como Direto ou CompartilhadoReusar isConsumable ou criar campo próprio?
2. AcúmuloOnde o custo compartilhado se junta antes de distribuirPool por período vs. rateio dentro da própria OS
3. DistribuiçãoPor qual critério o pool é dividido entre os ativosBase de rateio (custo direto, nº de OS, km, igualitário)

Fluxo proposto

Consumível compartilhado solicitado


Custo NÃO debita a frota da OS


Entra no POOL de material compartilhado (por período + centro de custo de oficina)


No fechamento do período / geração de relatório


RATEIO entre os ativos ativos no período (base configurável)


Custo por ativo = custo DIRETO + parcela RATEADA (as duas parcelas visíveis)

O ponto inegociável: o custo por ativo passa a ter duas parcelas explícitas e auditáveis — "direto" e "rateio de compartilhados". Nunca um número opaco que o gestor não consegue rastrear.


Protótipo Interativo

O cenário abaixo simula uma oficina que consumiu R$ 1.100 em materiais compartilhados no mês (graxa, disco de corte, arame MIG). Troque as abas para comparar como o mesmo pool muda o custo por ativo — e veja a distorção do modelo atual.

Materiais compartilhados no mes:Graxa (balde 5kg) R$ 600Disco de corte R$ 300Arame MIG R$ 200Pool total R$ 1.100
Hoje — 100% numa unica frota
Todo o custo dos materiais compartilhados (R$ 1.100) e apropriado ao ativo da OS onde a graxa foi solicitada. O Veiculo A absorve sozinho o consumo que serviu a oficina inteira.
AtivoCusto diretoParcela compartilhadaTotal apropriadoCPK
PN-0007
Frota 01
R$ 800R$ 1.100
R$ 1.900
0,63
PN-0012
Frota 01
R$ 1.600R$ 0
R$ 1.600
0,20
PN-0021
Frota 02
R$ 400R$ 0
R$ 400
0,27
PN-0033
Frota 02
R$ 1.200R$ 0
R$ 1.200
0,20
TotalR$ 4.000R$ 1.100R$ 5.100
Distorcao: o Veiculo A aparece 86% mais caro do que apareceria com rateio por custo direto (R$ 1.900 contra R$ 1.020). O CPK dele fica inflado e qualquer decisao de renovacao/manutencao baseada nesse numero parte de um dado errado.
Troque a estrategia nas abas para comparar como o mesmo pool de R$ 1.100 muda o custo por ativo.

Decisões

1. O que define "compartilhado"? — Campo próprio na categoria (Opção B) · APROVADO

Duas opções:

OpçãoComo funcionaTrade-off
A. Reusar isConsumableTodo consumível é tratado como compartilhado (rateio); o resto é diretoZero campo novo. Mas acopla dois conceitos: isConsumable hoje governa "pula seleção de serviço", não "como o custo é apropriado"
B. Campo próprio costAllocation (DIRETO | RATEIO) na categoriaClassificação explícita de custo, independente do isConsumableUm campo novo, mas desacopla as responsabilidades e permite exceções (um consumível direto, um material caro compartilhado)

Recomendação: Opção B, com a migração definindo o default de forma inteligente — categorias com isConsumable = true nascem como RATEIO, as demais como DIRETO. Assim ninguém precisa reclassificar o catálogo na mão, e os casos de exceção ficam possíveis.

Um interruptor, não dois campos para o usuário

As duas flags parecem redundantes, mas respondem perguntas diferentes:

FlagPergunta que respondeMomento
isConsumableO mecânico precisa dizer em qual serviço/veículo ao PEDIR?Solicitação
costAllocationO custo vai para um ativo ou para o pool ao CUSTEAR?Cálculo de custo

Hoje elas coincidem (todo material compartilhado é consumível), mas são conceitos distintos — por isso ficam separadas no banco. Se o isConsumable fosse removido, pedir graxa passaria a exigir a escolha de um serviço/veículo específico, que é justamente o atrito que não queremos.

Na tela, o gestor vê um único interruptor — "Material compartilhado" — que aciona as duas flags por baixo: pede sem exigir veículo (isConsumable = true) e manda o custo para o pool (costAllocation = RATEIO). A separação interna existe apenas para permitir a exceção rara (um material que precisa de serviço, mas cujo custo se rateia).

A configuração é por categoria, não por peça

O interruptor vive na PartCategory (uma dúzia de categorias, no máximo), não na peça. Cada Part apenas aponta para uma categoria e herda o comportamento.

NívelCampos novos para o usuário
Cada peça (graxa, filtro, parafuso)Zero — só escolhe a categoria, como hoje
Categoria (Consumíveis, Filtros, Lonas...)Um interruptor "Material compartilhado"

Marca-se "Consumíveis" como compartilhada uma vez, e toda graxa/disco/arame daquela categoria já entra no rateio — sem tocar em peça nenhuma. O mecânico que cadastra ou solicita peça nunca vê essas flags.

Nota de implementação: hoje nenhum formulário grava isConsumable — ele só existe no backend/seed. A tela de categoria (create-category-form, update-category-form) passa a expor o interruptor único, que grava as duas flags de uma vez.

2. Onde acumula e quando distribui — Pool por período · APROVADO (configurável)

OpçãoComo funcionaTrade-off
A. Rateio dentro da OSSe a OS toca vários ativos, divide o consumível só entre elesSimples e imediato. Mas não resolve o caso real: um balde de graxa pedido numa OS de um único veículo continua 100% naquele veículo
B. Pool por períodoTodo consumo compartilhado do mês vai a um pool e é rateado entre os ativos ativos no períodoRemove a distorção de verdade. Exige um "fechamento" de período (mensal)

Recomendação: Opção B. O problema descrito é cross-OS por natureza — a graxa serve à frota inteira ao longo do mês, não a uma OS. Rateio intra-OS só mascara o sintoma.

Decisão (produto, 26/07/2026): adotado o pool por período como padrão, porém configurável por empresa/centro de custo — quem preferir pode acionar também o rateio intra-OS quando fizer sentido. O modelo nasce flexível para os dois modos.

3. Base de rateio — Custo direto de manutenção · APROVADO (configurável)

O pool precisa de um critério para ser dividido. Opções, da mais justa à mais simples:

BaseLógicaDado disponível hoje?
Custo direto (peças + MO)Quem gerou mais manutenção consome mais insumo. Melhor proxyDepende da materialização de custo (ver aviso acima)
Nº de OS / serviçosQuantas OS tocaram o ativoSim — rastreável direto por WorkOrderAsset
Km rodadoProporcional a Vehicle.kmSim — mas km é proxy fraco para consumível de oficina
IgualitárioPartes iguais entre ativos ativosSempre disponível, porém ignora o esforço real

Recomendação: custo direto como default, com a base selecionável por empresa/centro de custo. Enquanto a materialização de custo não estiver pronta, nº de OS é o fallback mais honesto (dado já existe e não depende de custo). A base escolhida deve ficar registrada no relatório — rateio nunca pode ser uma caixa-preta.

4. Visibilidade do rateio — Sempre desdobrado

O custo por ativo nas telas de analytics e centro de custo passa a mostrar as duas parcelas:

Veículo PN-0007
Custo direto ............. R$ 800
Rateio compartilhado ..... R$ 220 (base: custo direto)
─────────────────────────────────
Total .................... R$ 1.020

O gestor sempre consegue responder "de onde veio esse número?". Sem isso, o rateio vira desconfiança.

5. Fechamento de período — como o mês "fecha"? (Aprovado · Opção C)

O rateio divide o pool do mês (ex: R$ 1.100 de graxa, disco e arame) entre os veículos, olhando os dados daquele mês. A pergunta:

Depois que o mês passa, o rateio vira uma foto fixa (oficial) ou é sempre recalculado com o dado mais recente?

Isso importa porque hoje o conceito de "fechar o mês" não existe — "período" é só filtro de data.

As opções

OpçãoComo funcionaTrade-off
A. Só congelaO mês vira foto oficial e nunca mais mudaRígido — o mês corrente também ficaria travado
B. Só on-the-flyRecalcula sempre com o dado atualFlexível, mas sem foto oficial; número de mês passado pode mudar
C. Híbrido — RECOMENDADOMês aberto recalcula ao vivo; mês fechado vira snapshot oficial; reprocessar gera nova foto com auditoriaO melhor dos dois. Padrão de ERP (competência aberta vs. fechada)

Como o híbrido (C) funciona

  • Mês aberto (o corrente) → rateio calculado ao vivo, valores provisórios, refletem o dado mais recente. Flexível.
  • Fechar mês (ação do admin) → o sistema tira uma foto dos valores e congela. A partir daí o mês mostra a foto oficial, com carimbo de quem e quando fechou. Auditável.
  • Reprocessar → se entrar lançamento atrasado num mês já fechado, o admin refecha, gerando nova foto com registro de auditoria. Rastro preservado.

Ou seja: flexível no presente, oficial no passado.

Como mostrar ao usuário

┌─────────────────────────────────────────────┐
│ Rateio · Julho 2026 ● Fechado │
│ Fechado em 02/08 por Tamires · base: custo │
│ Veículo A ....... R$ 220 (oficial) │
│ Veículo B ....... R$ 180 │
│ ⚠ Há 1 lançamento posterior ao fechamento │ ← fase 2
│ [ Reprocessar mês ] │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Rateio · Agosto 2026 ○ Aberto │
│ Valores provisórios · recalculados ao vivo │
│ Veículo A ....... R$ 190 (provisório) │
│ [ Fechar mês ] (admin) │
└─────────────────────────────────────────────┘

Três sinais garantem a confiança do gestor:

  1. Selo de status — Aberto/provisório (○) vs. Fechado/oficial (●). Nunca se confunde um número provisório com um oficial.
  2. Carimbo de fechamento — "Fechado em DD/MM por [usuário], base: X".
  3. Alerta de divergência (fase 2) — se entrou lançamento depois do fechamento, aparece "há lançamentos posteriores → Reprocessar?". O sistema é honesto sobre a foto estar desatualizada.

Esforço

C reaproveita todo o motor de cálculo do rateio (que é construído de qualquer forma — é a base). O incremental é moderado: 1 tabela de período/snapshot + os use cases de fechar/reprocessar + a UI de status. O detector de divergência (sinal 3) é opcional e fica para fase 2 — o admin pode reprocessar manualmente sem ele.

Decisão (produto, 28/07/2026): adotado o modelo híbrido (Opção C). Escopo distribuído nas tasks #17 (motor), #21 (fechamento de período + snapshot + reprocessar), #18 (exibição com selo de status) e #22 (detector de divergência — fase 2).


Cenários e Garantias

CenárioComportamento
Peça direta (filtro, lona)Apropriada ao ativo da OS, como hoje. Zero mudança
Consumível compartilhado (graxa)Vai ao pool, rateado no fechamento. Não cai numa frota só
Ativo sem nenhuma OS no períodoNão entra no rateio (base zero)
Período sem consumo compartilhadoPool vazio, rateio não roda, custo = só direto
Empresa quer manter tudo diretoBase "nenhuma" / classificar tudo como DIRETO — comportamento atual preservado
Material caro usado em vários ativos (ex: fluido especial)Pode ser marcado RATEIO mesmo não sendo consumível (exceção da Opção B)
Reprocessar um mês já fechadoRateio é determinístico a partir dos dados do período — recalcula igual

Migração

  • Classificação: categorias com isConsumable = true -> costAllocation = RATEIO; demais -> DIRETO. Sem reclassificação manual.
  • Comportamento preservado: enquanto ninguém marcar materiais como RATEIO (ou enquanto a base for "nenhuma"), o custo continua sendo apropriado como hoje. Adoção opcional e incremental.
  • Histórico: o rateio é calculado a partir dos dados do período. Meses passados podem ser recalculados sob demanda, mas não há reescrita retroativa automática.

Impacto Técnico

Dependência crítica (sequenciamento)

Esta proposta depende da materialização do custo real de peça na OS (o "elo faltante" identificado na análise). Recomendação: tratar como um único trabalho em duas camadas:

  1. Camada base — listener de PART_APPROVED / PART_DELIVERED / PART_RETURNED que recalcula e persiste WorkOrder.partsCost a partir de PartRequest.approvedQuantity × Part.costPrice. A infraestrutura de eventos já existe.
  2. Camada de rateio — no mesmo listener, decidir se o custo vai para o ativo/frota (direto) ou para o pool (compartilhado), e materializar a distribuição.

Construir o rateio antes da camada base seria construir sobre o seed aleatório.

Resumo das mudanças

ÁreaMudanças
DatabaseCampo costAllocation em PartCategory; estrutura de pool/rateio por período + centro de custo; (pré-requisito) materialização de partsCost
BackendListener de custo (base + rateio); use case de fechamento/cálculo de rateio; queries de analytics desdobrando direto vs. rateado
FrontendForm de categoria expondo classificação (+ isConsumable); analytics e centro de custo mostrando as duas parcelas; seleção da base de rateio
MigraçãoDefault de costAllocation derivado de isConsumable, zero reclassificação manual

O que NÃO muda

  • Fluxo de solicitação, aprovação, entrega e devolução de peças
  • Baixa de estoque (continua na aprovação)
  • Conversão de unidade de medida (proposta Unidade de Medida)
  • Peças diretas — apropriação idêntica à de hoje

Relação com outras propostas

PropostaRelação
Unidade de Medida e Conversão (aprovada)Define quanto de um consumível foi realmente consumido (stockImpact). O rateio distribui o custo desse consumo. Complementares
Devolução de PeçasDevolução de compartilhado retorna ao pool, não a um ativo. A ser tratado quando o rateio for detalhado

ADRs (Architecture Decision Records)

ADR-1: Classificação de custo em campo próprio, não reuso de isConsumable

Contexto: precisamos distinguir material direto de compartilhado. A flag isConsumable na categoria já separa consumíveis (que pulam a seleção de serviço) do resto, e na prática os materiais compartilhados são justamente consumíveis.

Decisão: criar costAllocation (DIRETO | RATEIO) na PartCategory, com default derivado de isConsumable na migração.

Justificativa:

  • isConsumable governa fluxo de solicitação ("precisa de serviço/localização?"), não apropriação de custo. São conceitos distintos que hoje coincidem por acaso.
  • Reusar acoplaria os dois — mudar um quebraria o outro no futuro.
  • O campo próprio permite exceções reais: um consumível barato apropriado direto, ou um material caro compartilhado que não é consumível.
  • Custo de implementar: um campo + migração com default inteligente. Baixo.

Consequências:

  • A tela de categoria passa a expor classificação de custo (e, junto, o isConsumable que hoje não tem UI).
  • Toda query de custo por ativo precisa ramificar direto vs. rateio.

ADR-2: Pool por período, não rateio intra-OS

Contexto: o custo compartilhado precisa se acumular em algum lugar antes de ser distribuído. Pode ser dentro da própria OS (entre os ativos que ela toca) ou num pool por período.

Decisão: pool por período (mensal), por centro de custo de oficina.

Justificativa:

  • O consumo compartilhado é cross-OS: um balde de graxa serve várias OS e vários veículos ao longo do mês.
  • Rateio intra-OS não resolve o caso mais comum (consumível pedido numa OS de um único veículo).
  • O pool por período espelha a prática contábil de rateio de custos indiretos.

Consequências:

  • Introduz o conceito de "fechamento de período" para consolidar o rateio.
  • O rateio é recalculável de forma determinística a partir dos dados do período.

ADR-3: Base de rateio configurável, default por custo direto

Contexto: o pool precisa de um critério de divisão. Nenhum critério único é perfeito para toda empresa.

Decisão: base selecionável (custo direto, nº de OS, km, igualitário), default = custo direto, fallback = nº de OS enquanto a materialização de custo não existir.

Justificativa:

  • Custo direto é o melhor proxy de "quanto de manutenção cada ativo gerou".
  • Mas ele depende da materialização de custo (ainda inexistente), então nº de OS é um fallback honesto e sempre disponível.
  • A base escolhida fica registrada no relatório — rateio auditável, nunca caixa-preta.

Consequências:

  • A configuração vive por empresa/centro de custo.
  • Trocar a base muda números históricos se o período for reprocessado — precisa ser explícito na UI.

ADR-4: Rateio depende da materialização de custo real

Contexto: hoje WorkOrder.partsCost vem de seed aleatório; nenhum código de produção calcula custo de peça a partir das requisições reais.

Decisão: implementar rateio como segunda camada do mesmo trabalho que materializa o custo real via listener de eventos de peça.

Justificativa:

  • Construir rateio sobre custo de seed produziria números sem sentido.
  • A infraestrutura de eventos (PART_APPROVED / PART_DELIVERED / PART_RETURNED) já existe e é o ponto natural para recalcular custo.
  • Unir os dois evita retrabalho e garante que a distinção direto/rateio nasça junto com o custo real.

Consequências:

  • O escopo desta proposta engloba a materialização de custo, que hoje é uma lacuna independente.
  • Benefício colateral: os dashboards de custo por frota deixam de depender de seed e passam a refletir dados reais.

Decisão

ItemDecisãoStatus
Classificação de materialCampo próprio costAllocation na categoria (Opção B, ADR-1)Aprovado (26/07/2026)
AcúmuloPool por período, configurável para rateio intra-OS (ADR-2)Aprovado (26/07/2026)
Base de rateioCusto direto de manutenção, configurável (ADR-3)Aprovado (26/07/2026)
VisibilidadeSempre desdobrado (direto + rateio)Aprovado (28/07/2026)
SequenciamentoMaterializar custo real + rateio no mesmo trabalho (ADR-4)Aprovado (28/07/2026)
Período/fechamentoModelo híbrido (Opção C): mês aberto recalcula ao vivo, mês fechado vira snapshot oficial, com reprocessar. Detector de divergência = fase 2. Ver Seção 5 (Decisões)Aprovado (28/07/2026)

Proposta aprovada (28/07/2026). Implementação sequenciada: Custo real (#8 → #9/#10 → #15), depois Rateio (#16 → #17 → #21 → #18/#19 → #22).