Ponto de partida
Cinco decisões
Sigla local é a chave; UN/CEFACT é metadado. A sigla tem no máximo 6 caracteres porque é ela que vai para o campo uCom da NF-e e para o registro 0190 do SPED. O código UN/CEFACT Rec 20 fica ao lado, opcional e não único, para interoperabilidade. Escolher só um dos dois é o erro.
Numerador e denominador inteiros, nunca fator decimal. Um fator de conversão é uma razão, não uma medida. 1/3 gravado como 0,333333 destrói informação que era exata na origem, e o erro acumula. Isso coloca o Nexio junto da SAP e à frente de Oracle, NetSuite, D365 e Coupa, que usam decimal.
Item × fornecedor vence item, que vence global — e a falta de conversão é erro, não 1:1. O nível de fallback implícito é o bug que produz um pedido de 84 caixas quando o comprador queria 84 unidades.
Todo documento copia o fator para a linha. Se o fornecedor mudar a caixa de 12 para 10 amanhã, pedidos em aberto não podem mudar de significado retroativamente. É o mesmo princípio que a legislação do SPED já impõe.
A classe Serviço não converte, nem internamente. Não existe "quantas horas tem uma verba". Duas propostas sem denominador comum são exibidas lado a lado como não comparáveis, em vez de receberem um preço unitário sem significado.
Fundamentos
Seis princípios
1. Nenhum produto sério tem "a UoM do item"
Coupa carrega seis papéis de unidade; D365 e NetSuite, três; SAP separa unidade base, unidade de pedido e unidade de preço. "Unidade de medida padrão" como campo único é um erro de modelagem que nenhum produto real comete.
2. A unidade base é a única constante da cadeia
A requisição pode vir em uma unidade, a cotação em outra, o pedido em outra e o recebimento em outra. Tudo é comparado, somado e saldado na unidade base do item. Ela é imutável depois da primeira transação, porque trocá-la reescreve o significado de todo o histórico.
3. Seja rígido com quantidade, tolerante com valor
Arredondar quantidade muda o mundo físico e o erro é cumulativo — o saldo carrega para sempre. Arredondar valor é local e limitado a meio centavo por linha. Por isso a quantidade convertida nunca é arredondada, e o valor é arredondado exatamente uma vez, no total da linha.
4. Preço mora na unidade em que foi acordado
Não normalize preço; normalize quantidade. Derivar preço por divisão e depois multiplicar é como nasce a divergência de centavos contra a nota do fornecedor — e, no Brasil, a rejeição 629 ou 630 na autorização da nota.
5. Campos que mudam o significado de dado gravado são imutáveis
Código, classe e fator canônico de uma unidade não se editam depois do primeiro uso. Nome, rótulo e código de interoperabilidade sim. Toda a matriz de governança decorre desta frase.
6. O sistema nunca inventa um fator
Quando não há conversão cadastrada, a resposta é erro explícito e uma pergunta ao usuário — nunca um 1:1 silencioso, nunca uma estimativa por classe.
Modelo
Mapa de entidades
storage-uom da Coupa. Complexidade que só faz sentido com estoque.
Convenção de nomes. O documento é em português; os identificadores são em inglês — entidades, atributos, valores de enum, rotas, permissões e códigos de erro. A exceção são os dados de negócio brasileiros, que ficam como são no mundo real: identificadores fiscais (cnpj, cpf, primaryCnae), nomes de regime tributário (SimplesNacional, LucroReal), siglas de unidade (UN, CX, KG), tipos de certidão e os exemplos de taxonomia — esses são conteúdo, não código.
Domínio · UnitOfMeasure
Catálogo de unidades
| Campo | Tipo | Regra |
|---|---|---|
| code | varchar(6) | Máximo 6 caracteres — é o limite do campo uCom da NF-e e do registro 0190 do SPED. Caixa alta, único no escopo. Imutável após o primeiro uso. |
| name | string(60) | Editável. É o rótulo de tela. |
| uomClass | enum | Lista fechada. Imutável após o primeiro uso — mudar a classe reinterpreta toda conversão existente. |
| isClassBase | bool | Exatamente uma por classe. É o pivô das conversões padrão. |
| canonicalNum / canonicalDen / canonicalExp | bigint / bigint / smallint | Conversão para a unidade canônica da classe: valor × (num ÷ den) × 10^exp. Obrigatórios exceto na classe Serviço. Imutáveis após o primeiro uso. |
| qtyDecimals | smallint | Casas permitidas na entrada do usuário. UN → 0, KG → 3, MT → 2. A coluna física sempre guarda 6. Aumentar é livre; diminuir é bloqueado se houver quantidade gravada com mais casas. |
| isIntegerOnly | bool | Unidade indivisível. false → true é bloqueado se já houver quantidade fracionária gravada — a transição inversa é sempre segura. |
| unece20Code / unece21Code | varchar(3) | Metadado de interoperabilidade. Não único e podendo ser nulo — forçar um mapeamento errado é pior que não ter mapeamento. Editável. |
| scope / tenantId | enum / Guid? | Global (mantida pelo Nexio, read-only para o tenant) ou Tenant. Ver Governança. |
| isActive | bool | Desativação sujeita às regras de uso. |
Cuidado com as colisões entre a sigla brasileira e o código UN/CEFACT. No Brasil CT é cento; na Rec 20, carton. CS aqui é cápsula; lá é case. PT aqui é pacote; lá é pint. MI aqui é milheiro e na Rec 20 nem existe — o equivalente é MIL. Nunca trate a sigla local como se fosse o código Rec 20: são dois campos, com dois significados.
Estrutura
Classes
Lista fechada. O tenant escolhe a classe ao criar uma unidade própria, mas não cria classe nova — porque a classe é o que autoriza ou proíbe uma conversão.
| Classe | Base | Converte? | Observação |
|---|---|---|---|
| Count | UN | só por item | CX, PT, FD, DZ, CT, MI. Não existe conversão global de caixa — depende do item. |
| Weight | KG | global + item | G, MG, TN. Conversões padrão inequívocas. |
| Volume | LT | global + item | ML, M3, GL. |
| Length | MT | global + item | MM, CM, KM, metro linear. |
| Area | M2 | global + item | CM2, HA. |
| Time | HR | global parcial | MIN, DIA, SEM. MES e ANO não têm conversão global — "1 mês = 30 dias" é convenção contratual, não física. Ver Casos difíceis. |
| Service | — | nunca | VB (verba), SERV, OS, ATV, HH, HM. Sem unidade base, sem conversão, sem preço normalizado. |
Carga inicial
Catálogo-semente
Cerca de 40 unidades globais, mapeadas manualmente para códigos verificados como ativos na Rec 20 (revisão 17) e na Rec 21 para embalagens. Os 1.755 códigos ativos entram numa tabela de referência de leitura — não se expõem 1.755 opções ao comprador.
| Classe | Sigla | Nome | Rec 20/21 | Fator canônico |
|---|---|---|---|---|
| Count | UN | Unidade | EA | 1 |
| Count | PC | Peça | H87 | 1 |
| Count | PAR | Par | PR | 2 |
| Count | DZ | Dúzia | DZN | 12 |
| Count | CT | Cento | CEN | 100 |
| Count | MI | Milheiro | MIL | 1 000 |
| Count | RS | Resma | RM | 500 FL (convenção) |
| Count | FL | Folha | LEF | 1 |
| Count | KIT | Kit | KT | por item |
| Count | JG | Jogo / conjunto | SET | por item |
| Count | CX | Caixa | XBX (Rec 21) | por item |
| Count | PT | Pacote | XPK (Rec 21) | por item |
| Count | FD | Fardo | XBE (Rec 21) | por item |
| Count | SC | Saco | XSA (Rec 21) | por item |
| Count | RL | Rolo | XRO (Rec 21) | por item |
| Weight | KG | Quilograma | KGM | base |
| Weight | G | Grama | GRM | 10⁻³ KG |
| Weight | MG | Miligrama | MGM | 10⁻⁶ KG |
| Weight | TN | Tonelada | TNE | 10³ KG |
| Volume | LT | Litro | LTR | base |
| Volume | ML | Mililitro | MLT | 10⁻³ LT |
| Volume | M3 | Metro cúbico | MTQ | 10³ LT |
| Length | MT | Metro | MTR | base |
| Length | CM / MM / KM | Centímetro / milímetro / quilômetro | CMT / MMT / KMT | 10⁻² / 10⁻³ / 10³ MT |
| Area | M2 | Metro quadrado | MTK | base |
| Time | HR / MIN / DIA | Hora / minuto / dia | HUR / MIN / DAY | base / 1/60 / 24 HR |
| Time | MES / ANO | Mês / ano | MON / ANN | sem conversão global |
| Service | VB | Verba | LS (lump sum) | — |
| Service | SERV | Serviço | E48 | — |
| Service | OS | Ordem de serviço | E51 | — |
| Service | ATV | Atividade | ACT | — |
| Service | HH / HM | Homem-hora / homem-mês | LH / 3C | — |
Os mapeamentos menos óbvios e mais úteis: LS (lump sum) cobre exatamente "verba", E48 cobre "serviço", e ACT, E51, LH e 3C cobrem a família de serviço sem inventar código. Marcar unece20Code como nulo é legítimo e precisa ser suportado.
O coração
Conversão
Hierarquia com fallback explícito
1. ItemSupplier.purchaseNum / purchaseDen item × fornecedor ← vence 2. ItemUomConversion.num / den item 3. StandardConversion (só intraclasse) global 4. → erro explícito. Nunca fator implícito 1:1.
O nível 4 é o mais importante do desenho inteiro. Nenhum sistema deve assumir fator 1 quando não encontra conversão — é assim que nasce o pedido de 84 caixas para quem queria 84 unidades.
Por que inteiros e não decimal
Um fator de conversão é uma razão, não uma medida, e razões de embalagem quase sempre têm denominador pequeno: 6, 10, 12, 24, 100, 1000.
| Fator decimal (6 casas) | Num/den inteiros | |
|---|---|---|
| Armazenado | UN→CX = 0,333333 | num=1, den=3 |
| 300 UN → CX | 99,9999 CX | 100 CX exato |
| Volta 100 CX → UN | 300,000300 UN | 300 UN exato |
| Erro por ida e volta | +0,0003 UN | 0 |
| Após 1.200 recebimentos | +0,36 UN de estoque fantasma | 0 |
O caso que dói de verdade é o de fatores irracionais aproximados. A documentação da Oracle traz pound → gram = 454; o valor exato é 453,59237, um erro relativo de 0,09%.
20 toneladas compradas em libras = 44.092 lb convertido com fator 454 = 20.017.768 g = 20.017,77 kg erro = +17,77 kg a R$ 8,50/kg = R$ 151,05 de divergência pedido × nota com num/den = 45359237 / 100000 → erro zero
Note que esse par não caberia nos campos de conversão da SAP, limitados a 5 dígitos. Dimensione os inteiros como bigint, não int.
Regras de implementação
numedensãobigint, ambos maiores que zero, normalizados por MDC na gravação.- Aritmética em decimal exato, nunca em ponto flutuante.
- Ordem obrigatória:
qty × num ÷ den— multiplicar antes de dividir. Inverter paraqty × (num ÷ den)reintroduz exatamente o erro que se acabou de eliminar. - Conversão entre classes diferentes existe apenas por item e nunca é herdada da conversão padrão.
ItemUomConversioné append-only com vigência. Editar o fator de uma conversão em uso é proibido — é literalmente a regra do registro 0220 do SPED, que exige criar um novo código de unidade quando a embalagem muda de conteúdo, em vez de alterar o fator do código existente.
Aritmética
Precisão e arredondamento
Casas decimais
| Grandeza | Coluna | Referência |
|---|---|---|
| Quantidade | numeric(19,6) | NF-e usa 4 casas em qCom e qTrib; SPED 0220 usa 6 no fator de conversão. Seis cobre os dois. |
| Preço unitário | numeric(19,10) | NF-e permite até 10 casas em vUnCom e vUnTrib — e o campo existe justamente para absorver conversão de embalagem. |
| Valores monetários totalizados | numeric(19,2) | vProd tem 2 casas. |
| Fator de conversão | bigint / bigint | Sem casas decimais, por construção. |
Onde arredondar
Arredonde a quantidade uma vez, na entrada. Nunca arredonde a quantidade convertida. Arredonde o valor uma vez, no total da linha. Nunca arredonde o preço unitário.
| Ponto da cadeia | Arredonda? | Por quê |
|---|---|---|
| Quantidade digitada pelo usuário | sim | Para qtyDecimals da unidade escolhida. É a única quantidade que um humano afirmou. |
| Quantidade convertida para a base | não | Valor derivado. Arredondar aqui é onde nascem o estoque fantasma e o clássico 4,99998. |
| Preço unitário | não | Não derive por divisão. Armazene na unidade em que foi cotado. |
| Total da linha | sim | 2 casas, ROUND_HALF_UP. É o único ponto em que dinheiro vira dinheiro. |
| Total do documento | não | É a soma dos totais de linha já arredondados. Arredondar de novo cria a divergência "soma dos itens ≠ total". |
Meio para cima, não bancário
ROUND_HALF_UP é a convenção comercial brasileira. Se o Nexio usar arredondamento bancário e o fornecedor usar meio para cima, os dois divergem em metade dos empates — sem que nenhum dos dois esteja errado.
Unidade indivisível arredonda sempre para cima
A documentação da PeopleSoft traz o alerta que vale ouro: com arredondamento ao mais próximo, uma quantidade calculada de 0,25 vira zero, não 1. Uma linha de pedido com quantidade zero é um bug silencioso. Para isIntegerOnly, arredonde para cima, sempre.
Divisível para indivisível é caminho de mão única
Aumentar as casas decimais de uma unidade é sempre seguro; reduzi-las depois de haver quantidade fracionária gravada, não. É a mesma assimetria que a PeopleSoft documenta e que a matriz de governança formaliza.
Aritmética
Preço e a unidade dele
O erro que produz divergência contra a nota é derivar preço unitário por divisão e depois multiplicar.
| Estratégia | Cálculo | Total | Divergência |
|---|---|---|---|
| Preço guardado por CX, cálculo na unidade de compra | 7 × 37,00 | 259,00 | R$ 0,00 |
| Preço derivado por UN com 10 casas | 84 × 3,0833333333 | 259,00 | R$ 0,00 |
| Preço derivado por UN com 4 casas | 84 × 3,0833 | 258,99 | −R$ 0,01 |
| Preço derivado por UN com 2 casas | 84 × 3,08 | 258,72 | −R$ 0,28 |
Quatro casas quebram a partir de 200 unidades: o erro máximo de arredondamento é 5×10⁻⁵, e a divergência é qty × 5×10⁻⁵. Com 10 casas o mesmo limite só seria atingido acima de duzentos milhões de unidades — é exatamente por isso que a NF-e dá 10 casas ao vUnCom.
As três regras
- Guarde o preço na unidade em que foi acordado, com a unidade junto:
priceAmount+priceUomId+priceQty. Não normalize preço para a unidade base do item. - Calcule o total na unidade de precificação:
lineTotal = ROUND(qtyInPriceUom ÷ priceQty × priceAmount, 2). Converta a quantidade, nunca o preço. priceQtyresolve item barato. Uma lâmpada a R$ 0,36263 cadastra-se comopriceQty = 1000epriceAmount = 362,63. Um parafuso a R$ 0,0037 virapriceQty = 1000,priceAmount = 3,70. É o PEINH da SAP e o price quantity do D365, e elimina inteiramente a questão de casas decimais de preço.
No Brasil isso deixa de ser questão contábil e vira rejeição fiscal. A SEFAZ valida vProd = qCom × vUnCom (rejeição 629) e vProd = qTrib × vUnTrib (rejeição 630), com tolerância de exatamente R$ 0,01 para mais ou para menos. No exemplo acima, com uTrib = UN: vUnTrib = 259,00 ÷ 84 = 3,0833333333 e a diferença fica em R$ 0,0000000028 — passa. Truncando a 2 casas, 84 × 3,08 = 258,72, diferença de R$ 0,28 — nota rejeitada. Um erro de modelagem de unidade de medida aqui não gera divergência contábil: gera nota que não autoriza.
Vínculo
Unidade e item
| Campo no item | Regra |
|---|---|
| baseUomId | Obrigatório. A classe precisa constar em ItemType.allowedUomClasses. Imutável após a primeira transação — requisição, cotação, pedido ou recebimento congelam a base. Alterar responde 409 e sugere criar item sucessor. |
| defaultPurchaseUomId | Opcional; default é a base. Precisa ter conversão cadastrada para a base. |
| conversions[] | Uma linha por unidade de destino, mesma classe da base, exceto no caso de dual UoM. A unidade base não pode figurar como destino. Máximo 20. |
| qtyDecimalsOverride | Opcional. Um item específico pode ser mais restrito que a unidade — parafuso vendido só em caixa fechada. |
Item de tipo com classe Service aceita apenas unidades dessa classe e não admite nenhuma linha de conversão. É validação de domínio, não de tela.
Vínculo
Unidade, item e fornecedor
A unidade de compra é do fornecedor, não do item. O fornecedor A vende em caixa de 12; o B, em caixa de 24; o C, em milheiro. É o mesmo item.
| Campo no ItemSupplier | Regra |
|---|---|
| purchaseUomId | Obrigatório. É a unidade em que o pedido sai. |
| purchaseNum / purchaseDen | Obrigatórios. 1 purchaseUom = num ÷ den × baseUom. Prioridade máxima na hierarquia — sobrepõe a conversão do item. |
| priceUomId | Pode ser diferente da unidade de compra. Compra-se em CX e cobra-se por KG, em cabo e aço. Precisa ter conversão conhecida para a base. |
| priceQty | Default 1. Ver Preço. |
| minOrderQty / orderMultiple | Na unidade de compra. O múltiplo é o lote fechado — o comprador não consegue pedir 7 quando o fornecedor só vende de 5 em 5. |
| supplierUomLabel | Texto livre: como o fornecedor chama a embalagem. Serve só para conferência humana contra a proposta em PDF. Nunca participa de cálculo. |
| overTolerancePct / underTolerancePct | Tolerância de recebimento. Fica aqui porque é o fornecedor quem tem variabilidade de embalagem, com override na linha do pedido. |
| validFrom / validTo | Versionamento. É o que permite que a embalagem mude sem reescrever o passado. |
Quando o fornecedor muda a embalagem
Cenário: a caixa de luva da ACME passa de 12 para 10 sem aviso. Chega nota de "7 CX" que na verdade são 70 UN, não 84.
O que não funciona: editar o registro para num = 10. Isso reescreve retroativamente todos os pedidos históricos — um pedido de 7 CX do ano passado passa a valer 70 UN, o estoque histórico fica errado e o custo médio é recalculado sobre uma mentira.
| Mecanismo | O que resolve |
|---|---|
| 1. Snapshot na linha do documento | Documentos já emitidos continuam com num = 12. Imunes por construção. |
2. Nova versão do ItemSupplier | validFrom na data da mudança, num = 10. Documentos novos usam a nova versão, e fica auditável em que data a caixa mudou. |
| 3. Detecção no recebimento | É o que salva na prática: esperado 7 × 12 = 84 UN, contado 70 UN, desvio de −16,7%, além da tolerância. O sistema bloqueia e pergunta: quantidade divergente ou embalagem mudou? Se for embalagem, abre o fluxo de nova versão e sinaliza os pedidos em aberto daquele item e fornecedor. |
A alternativa mais conservadora — e a que a legislação do SPED já exige para efeito fiscal — é criar um novo código de unidade (CX10) em vez de mudar o fator de CX. Vale considerar como comportamento padrão, não só como opção.
Fluxo
A cadeia ponta a ponta
| Etapa | Unidade | Regra |
|---|---|---|
| Requisição | A que o requisitante escolher, entre as conversões do item | Sempre grava também a quantidade na base. |
| Cotação | A que o fornecedor escolher | Nenhum produto impede o fornecedor de cotar em outra unidade — e é a norma. Grava preço, quantidade, unidade cotada e fator declarado. |
| Pedido | Unidade de compra do fornecedor | O documento que sai da empresa precisa ser executável por quem vai recebê-lo. Um pedido de "84 unidades" para quem só vende caixa fechada de 12 vira 7 caixas por interpretação do vendedor — ou 84 caixas por erro de digitação dele. |
| Recebimento | A que chegar fisicamente | Precisa aceitar unidade diferente da do pedido: o fornecedor quebrou a caixa e mandou 80 UN soltas. Converte para a base e a linha fica com 4 UN em aberto — sem arredondar. |
| Nota | A que o fornecedor emitir | Registrar a unidade original. Se só guardar a convertida, perde-se a capacidade de auditar a divergência depois. |
O que toda linha de documento carrega
orderedQty numeric(19,6) na unidade escolhida → 7 orderedUomId → CX orderedQtyBase numeric(19,6) na unidade base do item → 84 baseUomId → UN convNum, convDen bigint SNAPSHOT do fator → 12, 1 priceAmount numeric(19,10) → 37,0000000000 priceUomId → CX priceQty numeric(19,6) → 1
O saldo em aberto da linha é calculado em unidade base, nunca na unidade do pedido — só assim é possível representar "sobraram 4 UN de uma linha de 7 CX".
Conferência a três (pedido × recebimento × nota)
Os três lados comparam na unidade base, usando o snapshot da linha e não o cadastro atual. A tolerância precisa ser percentual e absoluta ao mesmo tempo, passando se qualquer uma acomodar — o inverso do default de alguns ERPs, e é deliberado: divergências de arredondamento são absolutas e pequenas, divergências de preço são percentuais e grandes. Um piso absoluto de poucos centavos por linha elimina quase todo o ruído sem afrouxar controle real. E quando a nota vem em unidade que o ItemSupplier não conhece, não converta por classe genérica: levante exceção e peça o fator ao comprador — é exatamente o momento em que a embalagem mudou e alguém precisa saber.
Onde tudo isso paga
Comparativo de cotações
O erro clássico é comparar preço por unidade cotada.
| Supplier | Cota em | Preço | 1 unidade cotada = | Preço por UN |
|---|---|---|---|---|
| A | CX | R$ 37,00 | 12 UN | R$ 3,083333 |
| B | CX | R$ 68,00 | 24 UN | R$ 2,833333 |
| C | MI | R$ 2.900,00 | 1 000 UN | R$ 2,900000 |
| D | UN | R$ 3,05 | 1 UN | R$ 3,050000 |
Sem normalizar, A parece o mais barato — R$ 37 é menos que R$ 68. Normalizado, B ganha e C é o segundo. Nenhum dos dois seria escolhido por leitura ingênua.
Normalize para a unidade base, não para a da requisição
A base é a única constante da cadeia. normalizedPrice = quotedPrice × den ÷ (num × priceQty), calculado com 10 casas e arredondado só na exibição. Duas cotações que diferem em R$ 0,0001 por unidade diferem em R$ 100 num pedido de um milhão.
Exiba as duas colunas
Preço na unidade cotada, para o comprador conferir contra a proposta em PDF, e preço normalizado, para decidir. Um comparativo que só mostra o normalizado gera desconfiança e o comprador refaz a conta na calculadora.
Mostre também o preço efetivo pela quantidade requisitada
Com múltiplo indivisível, o ranking muda. Para 100 UN: o fornecedor B, em caixa de 24, exige 5 caixas — 120 UN por R$ 340,00, ou R$ 3,40 por unidade efetiva. O A, em caixa de 12, exige 9 caixas — 108 UN por R$ 333,00, ou R$ 3,33. O "mais barato por unidade" perde para o "mais barato nesta compra".
Sem conversão, sem número inventado
Se o item é de classe Peso e o fornecedor cotou em UN sem fator declarado, o comparativo exibe as propostas lado a lado marcadas como não comparáveis e pede o fator ao comprador. Estimar seria pior que não comparar. A classe Serviço nunca normaliza — a comparação ali é por escopo, com campo qualitativo.
Recebimento
Tolerância
Avaliar tolerância na unidade de compra ou na unidade base dá o mesmo resultado quando a conversão é linear — o que não é o caso quando há arredondamento no meio.
| Avaliada em | Limite | Entrega de 8 CX (96 UN) |
|---|---|---|
| Unidade de compra (CX) | 7,7 CX | rejeita |
| Unidade base (UN) | 92,4 UN | rejeita |
| CX arredondado para caixa inteira | 8 CX | aceita — a tolerância virou +14,3% sem ninguém decidir isso |
- Avalie sempre na unidade base, com quantidades não arredondadas. É o único ponto de comparação estável entre requisição, pedido e recebimento.
- Não arredonde o limite. Compare
receivedQtyBase ≤ orderedQtyBase × (1 + tol)diretamente em decimal. - Exponha o limite também na unidade de compra, marcando quando ele cai entre dois múltiplos indivisíveis: "+10% de 7 CX = 7,7 CX — a próxima caixa inteira excede a tolerância".
- Tolerância de entrega a menor e "recebimento concluído" são coisas diferentes: a primeira aceita fechar a linha com menos; a segunda encerra o saldo em aberto e ainda assim admite recebimento dentro da tolerância a maior.
Operação
Governança do catálogo
As ~40 unidades da semente, mantidas pelo Nexio. Read-only para o tenant, que não pode editar código, classe ou fator canônico. Pode ocultar o que não usa e pode renomear o rótulo de exibição — "PT" aparece como Pacote ou Sachê conforme o negócio, mas continua sendo PT no XML e na integração. Correções propagam automaticamente, o que só é seguro porque documentos guardam snapshot.
Unidades próprias — CARRETA, M3COMP, PTFUNC, TONKM. Não permitir isso é irreal: cada vertical tem a sua. Obrigatório escolher uma classe da lista fechada e informar o fator canônico, exceto na classe Serviço. Código de tenant não pode colidir com código global — se o global tem CT como cento e o tenant quer CT como cartão, isso é ambiguidade fatal em documento fiscal, e é bloqueado na criação com mensagem explícita.
Desativar unidade em uso
| Situação | Comportamento |
|---|---|
| Nunca usada | Exclusão permitida |
| Usada em documento histórico fechado | Exclusão bloqueada; desativação permitida. Documentos antigos continuam legíveis, com marca visual de unidade inativa |
| Usada em documento em aberto (RFQ, pedido não recebido) | Desativação bloqueada, com a lista dos documentos que impedem |
| É unidade base de item ativo | Desativação bloqueada, sempre |
A mensagem de erro precisa nomear os documentos, não dizer "unidade em uso". A diferença entre um sistema usável e um irritante costuma ser essa lista.
Matriz de imutabilidade
| Campo | Depois do primeiro uso |
|---|---|
| code | imutável É a chave que foi para o XML da NF-e, para o e-mail do fornecedor e para o PDF do pedido |
| uomClass | imutável Mudar a classe reinterpreta toda conversão existente |
| canonicalNum / Den / Exp | imutável Corrigir um fator errado exige criar nova unidade e migrar item por item. Doloroso de propósito |
| qtyDecimals | assimétrico Aumentar é livre; diminuir é bloqueado se houver quantidade gravada com mais casas |
| isIntegerOnly | assimétrico Ligar é bloqueado se houver quantidade fracionária gravada; desligar é livre |
| name, unece20Code, unece21Code | editável São metadados de exibição e integração |
O princípio geral, que vale registrar como decisão de arquitetura: campos que alteram o significado numérico de dados já gravados são imutáveis; campos que alteram apresentação são editáveis. Toda a matriz acima decorre disso.
Validação do modelo
Casos que quebram modelos ingênuos
| Caso | O que quebra | Como o modelo resolve |
|---|---|---|
| Compra por milheiro | Fator 1000 com preço por milheiro; dividir dá dízima | MI com num=1000, den=1 e preço guardado por MI. Nunca derivar preço por UN. |
| Preço por tonelada, compra por saco | Unidade de compra e unidade de preço são diferentes | purchaseUom = SC, priceUom = TN, com conversão SC→KG por item. É o motivo de priceUomId existir separado. |
| Service por hora vs. por verba | Não há denominador comum | Classe Serviço não converte. Comparativo marca como não comparável e compara por escopo. |
| Aço e cabo: vende por metro, cobra por peso | Conversão entre classes (Comprimento → Peso) que só vale para aquele item | Conversão interclasse por item, jamais herdada da conversão padrão. |
| Embalagem muda de conteúdo entre lotes | Editar o fator reescreve o histórico | Nova versão do ItemSupplier ou novo código de unidade (CX10), mais snapshot na linha. Ver § Unidade, item e fornecedor. |
| Resma, fardo, pacote | Siglas de embalagem sem equivalente na Rec 20 | Rec 21 (XBX, XPK, XBE) para embalagem; RM existe na Rec 20 para resma. |
| Locação por mês | "1 mês = 30 dias" é convenção contratual, não física | MES não tem conversão global para DIA. Se o contrato define, é conversão por item. |
| Compra por caixa, pagamento por peso real | Duas quantidades verdadeiras para a mesma linha | Dual UoM — fora do MVP. Enquanto isso, o recebimento aceita a unidade de peso e a divergência sai na tolerância. |
Rastreabilidade
Regras de negócio
Código tem no máximo 6 caracteres, caixa alta, único no escopo
Limite herdado da NF-e e do registro 0190 do SPED. Código de tenant não pode colidir com código global.
Toda unidade pertence a exatamente uma classe da lista fechada
O tenant escolhe a classe; não cria classe. A classe é o que autoriza ou proíbe uma conversão.
Fator canônico é obrigatório, exceto na classe Serviço
Unidade sem fator canônico fora de Serviço responde 400. Sem ele, a unidade não participa de nenhuma agregação.
Conversão usa numerador e denominador inteiros positivos
bigint, normalizados por MDC na gravação. Zero ou negativo responde 400.
Multiplicar antes de dividir, em decimal exato
qty × num ÷ den, nunca qty × (num ÷ den), nunca em ponto flutuante. É regra de implementação com teste automatizado dedicado.
A hierarquia de resolução é fixa e o fallback final é erro
Item × fornecedor, depois item, depois global intraclasse, depois 422 UOM_CONVERSION_NOT_FOUND. Nunca fator implícito.
Conversão entre classes só existe por item
Nunca global, nunca herdada. Comprimento para Peso vale para aquele cabo, não para "cabo".
Classe Serviço não admite conversão alguma
Nem entre unidades da própria classe. Tentativa responde 400 UOM_SERVICE_CLASS_NOT_CONVERTIBLE.
Conversão de item é append-only com vigência
Editar o fator de uma conversão em uso é proibido. Cria-se nova versão com validFrom.
Todo documento copia o fator para a linha
convNum e convDen são snapshot, não referência. Documento emitido nunca muda de significado.
Quantidade convertida nunca é arredondada
Arredonda-se a quantidade digitada, para as casas da unidade escolhida, e mais nada. O saldo em aberto vive em unidade base com 6 casas.
Unidade indivisível arredonda para cima, nunca ao mais próximo
Evita que uma quantidade calculada de 0,25 vire zero e produza linha de pedido vazia.
Valor arredonda uma vez, no total da linha
Duas casas, meio para cima. O total do documento é a soma dos totais de linha já arredondados — nunca um segundo arredondamento.
Preço é guardado na unidade acordada, com quantidade de precificação
priceAmount, priceUomId e priceQty gravam juntos. Preço nunca é derivado por divisão para armazenamento.
Unidade base do item é imutável após a primeira transação
Requisição, cotação, pedido ou recebimento congelam a base. Alteração responde 409 com a sugestão de criar item sucessor.
Tolerância é avaliada em unidade base, sem arredondar o limite
Arredondar o limite para o múltiplo indivisível seguinte altera silenciosamente a tolerância efetiva.
Comparação sem conversão conhecida é recusada, não estimada
O comparativo exibe as propostas sem preço normalizado, marcadas, e pede o fator ao comprador.
Campos que mudam o significado de dado gravado são imutáveis
Código, classe e fator canônico após o primeiro uso. Nome e códigos de interoperabilidade permanecem editáveis.
Camada API
Endpoints
| Rota | Permissão | Observação |
|---|---|---|
| GET /uoms | uom:read | Paged. Filtros: search, uomClass, scope, isActive. Consumido pelo autocomplete dos formulários. |
| GET /uoms/{id} | uom:read | Com as conversões padrão da classe. |
| POST /uoms | uom:manage | Cria unidade do tenant. Recusa colisão de código com a camada global. |
| PUT /uoms/{id} | uom:manage | Respeita a matriz de imutabilidade. Campos bloqueados respondem 409 nomeando o campo. |
| POST /uoms/{id}/deactivate | uom:manage | Devolve a lista de documentos e itens que impedem, quando impedem. |
| PUT /uoms/{id}/alias | uom:manage | Rótulo de exibição do tenant sobre unidade global. Não altera o código. |
| PUT /uoms/visibility | uom:manage | Oculta unidades globais que o tenant não usa. |
| GET /uom-classes | uom:read | Lista fechada, com a unidade base de cada classe. |
| GET /uoms/standard-conversions | uom:read | Conversões globais intraclasse. |
| POST /uoms/convert | uom:read | Utilitário: quantidade, unidade de origem, unidade de destino e contexto opcional (item, fornecedor). Devolve o resultado, o fator usado e de qual nível da hierarquia ele veio. Sem efeito colateral — é a ferramenta de diagnóstico quando o número na tela não bate. |
| GET /ref/unece-uoms | uom:read | Tabela de referência da Rec 20 e Rec 21, só leitura, para mapeamento e integração. |
As conversões por item vivem nos endpoints do catálogo (PUT /catalog/items/{id}/uom-conversions) e as do fornecedor em PUT /catalog/items/{id}/suppliers/{supplierItemId} — estão especificadas nos respectivos documentos.
Contrato
Códigos de erro
| Código | HTTP | Quando |
|---|---|---|
| UOM_NOT_FOUND | 404 | Unidade inexistente no escopo |
| UOM_CODE_TOO_LONG | 400 | Código com mais de 6 caracteres |
| UOM_CODE_DUPLICATED | 409 | Código já existe no tenant ou colide com a camada global |
| UOM_CANONICAL_FACTOR_REQUIRED | 400 | Fator canônico ausente fora da classe Serviço |
| UOM_CLASS_BASE_ALREADY_EXISTS | 409 | Segunda unidade marcada como base da mesma classe |
| UOM_FIELD_IMMUTABLE_CONFLICT | 409 | Alteração de código, classe ou fator canônico após o primeiro uso — mensagem nomeia o campo |
| UOM_DECIMALS_CANNOT_DECREASE | 409 | Redução de casas com quantidade gravada mais precisa |
| UOM_IN_USE_CONFLICT | 409 | Desativação ou exclusão de unidade em uso — mensagem lista os documentos e itens |
| UOM_CONVERSION_INVALID | 400 | Numerador ou denominador não positivo, ou destino igual à origem |
| UOM_CONVERSION_NOT_FOUND | 422 | Nenhum nível da hierarquia resolveu o fator — nunca cair em 1:1 |
| UOM_CONVERSION_CROSS_CLASS_NOT_ALLOWED | 400 | Conversão entre classes fora do escopo de item |
| UOM_SERVICE_CLASS_NOT_CONVERTIBLE | 400 | Qualquer conversão envolvendo a classe Serviço |
| UOM_CONVERSION_LOCKED_CONFLICT | 409 | Edição de conversão de item já usada em documento |
| UOM_QUANTITY_PRECISION_EXCEEDED | 400 | Quantidade com mais casas do que a unidade permite |
| UOM_QUANTITY_NOT_INTEGER | 400 | Quantidade fracionária em unidade indivisível |
Limites
Fora de escopo
- Dual UoM / catch weight — comprar por caixa e pagar por peso real, caso de proteína, aço e cabo. Exige duas quantidades verdadeiras na mesma linha e uma segunda unidade no item.
- Unidade de estocagem separada — o
storage-uomda Coupa. Só faz sentido quando houver estoque. - Unidade de consumo — distinta da de compra e da de estoque. Mesma condição.
- Conversão dependente de temperatura ou densidade — combustíveis e químicos. O D365 tem "quantidade adicional" para isso.
- Expor os 1.755 códigos da Rec 20 como opção de escolha. Ficam em tabela de referência, para mapeamento e integração.
- Criar classe de unidade pelo tenant. A lista é fechada porque a classe é o que autoriza uma conversão.
- Editar fator canônico para corrigir erro. A correção é criar unidade nova e migrar — doloroso de propósito.
- Normalizar preço de serviço. Não existe denominador comum entre hora e verba.
Pendência para decidir: quando a embalagem do fornecedor muda, o comportamento padrão é nova versão do ItemSupplier ou novo código de unidade (CX10)? A segunda opção é o que a legislação do SPED já exige para efeito fiscal e é mais à prova de erro humano; a primeira é mais limpa no cadastro. A escolha muda a UI do recebimento e vale fechar antes de implementar.