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
Preservar quantidade original e fatores evita mudar o fato declarado. A quantidade base armazenada tem seis casas e é uma projeção; valor monetário é calculado sem usar essa aproximação intermediária, com arredondamento no total da linha nas casas da moeda.
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. |
| isPackaging | bool | Novo em 09/09/2026. Marca a unidade como embalagem: caixa, pacote, fardo, saco, rolo, kit, jogo. O que a embalagem contém depende do item, e por isso ela não tem — nem pode ter — fator canônico. Imutável após o primeiro uso, pela mesma razão que a classe é: liga e desliga o nível 3 da hierarquia de conversão. |
| canonicalNum / canonicalDen / canonicalExp | bigint? / bigint? / smallint? | Conversão para a unidade canônica da classe: valor × (num ÷ den) × 10^exp. Anuláveis desde 09/09/2026 — obrigatórios exceto na classe Serviço e nas unidades de embalagem, onde são proibidos. 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 | — |
As dez unidades com "por item" na última coluna são as de embalagem — KIT, JG, CX, PT, FD, SC, RL — mais as que dependem de convenção (RS) ou de contrato (MES, ANO). As de embalagem nascem com isPackaging verdadeiro e sem fator canônico: a semente não inventa um, e o cadastro não aceita um (RN-UOM-19). 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. ItemSupplierOffer.numerator / denominator item × fornecedor ← vence 2. ItemUomConversion.numerator / denominator item 3. StandardConversion (só intraclasse, e só global entre unidades com fator canônico) 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.
A porta pela qual o 1:1 voltaria — fechada em 09/09/2026. As unidades de embalagem (CX, PT, FD, SC, RL, KIT, JG) são da classe Count, e enquanto o fator canônico era obrigatório fora da classe Serviço não havia valor correto para elas: ou a semente não rodava, ou alguém gravava 1/1 — e aí o nível 3 resolvia CX → UN como um para um, em silêncio, que é literalmente o bug que a Decisão 3 e a RN-UOM-06 existem para impedir. Agora a unidade de embalagem é marcada (isPackaging), não tem fator canônico, e a regra está escrita: unidade sem fator canônico nunca resolve no nível 3 — cai direto no 422. O nível 3 exige canônico nos dois lados da conversão. Quem sabe quantas unidades cabem numa caixa é o item, no nível 2, ou o fornecedor, no nível 1.
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
numeratoredenominatorsã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.
Contrato vigente · revisão consolidada
Precisão e arredondamento
| Grandeza | Persistência | Cálculo |
|---|---|---|
| Quantidade original e normalizada | numeric(19,6) | Preservar original, unidade e fatores inteiros. Normalizada é projeção com seis casas HALF_UP; declarar precisão e resíduo. Não é representação exata de toda fração. |
| Preço unitário acordado | numeric(19,10) | Preservar unidade e quantidade de precificação; não derivar e armazenar preço arredondado por unidade base. |
| Total monetário | numeric(21,4) | Arredondar HALF_UP nas casas da moeda (0–4). Limitar magnitude ao tipo; overflow é erro declarado. |
| Fatores | bigint / bigint | Positivos, reduzidos por MDC; multiplicar antes de dividir. Usar precisão decimal ampliada ou racional para evitar overflow intermediário. |
Validar a precisão da quantidade informada contra a unidade; qualquer ajuste deve ser apresentado antes da confirmação. Para planejamento de unidade indivisível, arredondar a sugestão para cima e mostrar o acréscimo, mínimo e múltiplo comercial. Nunca alterar silenciosamente quantidade recebida ou evidência da nota.
Dinheiro é calculado da quantidade original e fatores, sem usar a quantidade base já arredondada. Total do documento soma linhas; rateio fecha na moeda original e o total funcional soma fatias convertidas, conforme Moeda. Exportação fiscal pode exigir precisão distinta: o adaptador conserva valores originais e registra a transformação exigida pelo leiaute.
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,9972 | 259,00 | R$ 0,00 |
| Preço derivado por UN com 2 casas | 84 × 3,08 | 258,72 | −R$ 0,28 |
No exemplo, preço unitário reduzido a quatro casas gera R$ 259,00 após arredondar 84 × 3,0833 = 258,9972; com duas casas, gera R$ 258,72. A divergência depende da quantidade e do ponto de arredondamento. O contrato evita acumular aproximações preservando o preço acordado.
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(orderedQty × convNum × priceConvNum × priceAmount ÷ (convDen × priceConvDen × priceQty), Currency.minorUnits). 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. A unidade de destino pode ser de outra classe, e é assim que o aço comprado em SC e cobrado em TN, e o cabo comprado em MT e cobrado em KG, entram no cadastro — corrigido em 09/09/2026, quando ficou claro que a regra da coleção proibia os três casos que a própria especificação usa como prova. A equivalência interclasse é deste item: 1 MT daquele cabo pesa o que aquele cabo pesa, e nada disso é herdado nem vira conversão global. 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.
Contrato vigente · revisão consolidada
Oferta comercial e conversões
O nível mais específico de conversão é a versão de ItemSupplierOffer selecionada; depois a conversão do item e, por último, o fator global intraclasse. Embalagem sem fator canônico não usa conversão global implícita. ItemSupplier continua sendo a relação; preço referencia a oferta.
A oferta contém purchaseUomId, numerator/denominator, código comercial, supplierUomLabel, MOQ, múltiplo, prazo e tolerâncias. Reajustar preço não altera fator; mudar embalagem ou condição relevante cria revisão de oferta. A caixa de 12 e a caixa de 24 podem coexistir para o mesmo fornecedor e item, com identidades distintas.
Ao receber 70 UN contra 7 CX de 12, registrar a contagem real e divergência de −14 UN; não reescrever o pedido para 7 CX de 10. Confirmar mudança de embalagem permite cadastrar a apresentação CX10 e revisar pedidos ainda abertos pelo processo de alteração. O recebimento ocorrido permanece registrado com pendência.
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 priceConvNum, SNAPSHOT do fator → 1, 12 priceConvDen bigint base → unidade de preço
convNum/convDen converte pedido → base; priceConvNum/priceConvDen converte base → preço. No exemplo, 7 CX × 12/1 × 1/12 × R$ 37 = R$ 259. Se o preço for por UN, só o segundo fator será 1/1. Ambos os fatores são congelados na linha.
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 a versão de ItemSupplierOffer 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 |
| isPackaging | imutável Liga e desliga o nível 3 da hierarquia de conversão — mudá-lo reinterpreta toda resolução já feita |
| 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, declarada na coleção conversions[] daquele item e jamais herdada da conversão padrão (RN-UOM-07). |
| Embalagem muda de conteúdo entre lotes | Editar o fator reescreve o histórico | Novo código de unidade (CX10) — decidido em 08/09/2026, não é mais alternativa —, nova revisão de ItemSupplierOffer apontando para ela, mais o snapshot na linha do documento. Ver § Unidade, item e fornecedor e RN-ITM-25. |
| 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 em Serviço e em embalagem
Revista em 09/09/2026. Unidade sem fator canônico fora de Serviço e fora de embalagem responde 400 UOM_CANONICAL_FACTOR_REQUIRED; unidade com isPackaging e com fator canônico preenchido responde 400 UOM_CANONICAL_FACTOR_NOT_ALLOWED. As duas metades importam: sem a primeira, unidade física fica fora de toda agregação; sem a segunda, alguém grava 1/1 na caixa e o nível 3 devolve conversão silenciosa. Ver RN-UOM-19.
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.
Resolver pela revisão de ItemSupplierOffer selecionada, depois ItemUomConversion e depois conversão global intraclasse com fatores canônicos. Ausência resulta UOM_CONVERSION_NOT_CONFIGURED_UNPROCESSABLE, sem estimativa implícita.
Conversão entre classes só existe por item
Nunca global, nunca herdada. Comprimento para Peso vale para aquele cabo, não para "cabo". E é permitida: a coleção conversions[] do item aceita unidade de destino de outra classe quando o item declara a equivalência — corrigido em 09/09/2026, quando ficou claro que a regra da coleção dizia "mesma classe da base" e proibia os casos SC → TN e MT → KG que a especificação usa como prova. A classe Service continua sem conversão alguma (RN-UOM-08).
Classe Serviço não admite conversão alguma
Nem entre unidades da própria classe. Tentativa responde 400 UOM_SERVICE_CLASS_NOT_CONVERTIBLE.
Não editar fatores usados em documento. Publicar nova revisão com validade, preservando a anterior. Mudança física de embalagem ganha apresentação própria na oferta; nome CX10 é convenção do produto.
Todo documento copia o fator para a linha
convNum, convDen e — desde 09/09/2026 — priceConvNum e priceConvDen são snapshot, não referência. Documento emitido nunca muda de significado, nem na quantidade nem no total. Ver RN-ITM-29.
Preservar quantidade original, unidade e fatores. Quantidade base armazenada em numeric(19,6) é projeção arredondada HALF_UP; cálculo monetário usa os fatores originais sem depender desse arredondamento. Comparação e liquidação conservam também as parcelas originais e seus resíduos.
Planejamento arredonda para cima conforme unidade, MOQ e múltiplo, com confirmação. Fato recebido e quantidade fiscal original não são aumentados automaticamente.
Calcular em precisão ampliada, arredondar uma vez por linha em Currency.minorUnits e persistir numeric(21,4). Total do documento é soma das linhas. HALF_UP é decisão do produto; adaptador fiscal valida o leiaute aplicável.
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. isPackaging entrou na lista em 09/09/2026.
Unidade de embalagem não tem fator canônico, e sem canônico não há nível 3
Nova em 09/09/2026. isPackaging marca a unidade cujo conteúdo depende do item — CX, PT, FD, SC, RL, KIT, JG. Ela não tem fator canônico, e a hierarquia de resolução passa a dizer isto de frente: unidade sem fator canônico nunca resolve no nível 3, cai direto no 422 UOM_CONVERSION_NOT_CONFIGURED_UNPROCESSABLE. O que se fecha aqui é a porta de entrada do 1:1 implícito: com o canônico obrigatório e nenhum valor correto para a caixa, a saída de menor esforço era gravar 1/1 — e uma caixa que vale uma unidade é o pedido de 84 caixas para quem queria 84 unidades. Quem sabe o conteúdo da embalagem é o item (nível 2) ou o fornecedor (nível 1).
Camada API
Endpoints
| Rota | Permissão | Observação |
|---|---|---|
| GET /uoms | read:uoms | Paged. Filtros: search, uomClass, scope, isActive. Consumido pelo autocomplete dos formulários. |
| GET /uoms/{id} | read:uoms | Com as conversões padrão da classe. |
| POST /uoms | manage:uoms | Cria unidade do tenant. Recusa colisão de código com a camada global. |
| PUT /uoms/{id} | manage:uoms | Respeita a matriz de imutabilidade. Campos bloqueados respondem 409 nomeando o campo. |
| PATCH /uoms/{id}/deactivate | manage:uoms | Devolve a lista de documentos e itens que impedem, quando impedem. |
| PUT /uoms/{id}/alias | manage:uoms | Rótulo de exibição do tenant sobre unidade global. Não altera o código. |
| PUT /uoms/visibility | manage:uoms | Oculta unidades globais que o tenant não usa. |
| GET /uom-classes | read:uoms | Lista fechada, com a unidade base de cada classe. |
| GET /uoms/standard-conversions | read:uoms | Conversões globais intraclasse. |
| POST /uoms/convert | read:uoms | 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 | read:uoms | 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_ALREADY_EXISTS | 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 e fora de unidade de embalagem |
| UOM_CANONICAL_FACTOR_NOT_ALLOWED | 400 | Novo em 09/09/2026. Fator canônico preenchido em unidade com isPackaging — o conteúdo da embalagem é do item, não da unidade (RN-UOM-19) |
| 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_CONFLICT | 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_CONFIGURED_UNPROCESSABLE | 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.
Mudança de embalagem gera nova versão de ItemSupplierOffer; apresentações simultâneas têm ofertas distintas. Documentos preservam seus fatores e códigos originais. O adaptador fiscal mapeia unidade comercial e conversões exigidas pelo ERP/SPED conforme a operação; essa obrigação não determina criar códigos globais CX10 ou alterar o histórico do item.