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 código da unidade é outra conversa: o Nexio não emite a EFD, entrega ao ERP o código, o fator congelado e a linha de embalagem, e é o ERP que traduz para o registro 0220 quando o mesmo código teve outro fator (ver SPED em Conversão).
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
ItemSupplier, com fator próprio — N por relação, uma ativa por unidade. Preço e quantidade de precificação ficam na versão de preço.
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 | 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 — obrigatórios nas classes físicas, opcionais na classe Time, inexistentes em Serviço e proibidos nas unidades de embalagem (RN-UOM-03). 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 | global + item | PC, PAR, DZ, CT, MI, RS e FL convertem pela conversão global. Embalagem — KIT, JG, CX, PT, FD, SC, RL — só por item ou fornecedor: não existe conversão global de caixa. |
| Weight | KG | global + item | G, MG, TN. Conversões padrão inequívocas. |
| Volume | LT | global + item | ML, M3. |
| Length | MT | global + item | MM, CM, KM, POL. |
| Area | M2 | global + item | Só a própria unidade na semente. |
| Time | HR | global parcial | MIN converte pela global. DIA, MES e ANO não têm conversão global — "1 mês = 30 dias" é convenção contratual, e "1 dia = 24 h" é física que não vale para a diária de mão de obra (8 h) nem para a de locação: essas unidades convertem só por item. Ver Casos difíceis. |
| Service | — | nunca | VB (verba), SERV, OS, ATV, HH, HM. A classe não tem unidade de referência, conversão nem preço normalizado; o item de serviço escolhe uma delas como a sua base. |
Carga inicial
Catálogo-semente
39 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 |
| Length | POL | Polegada | INH | 254/10 000 MT (exata) |
| Area | M2 | Metro quadrado | MTK | base |
| Time | HR / MIN | Hora / minuto | HUR / MIN | base / 1/60 HR |
| Time | DIA / MES / ANO | Dia / mês / ano | DAY / 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 sete unidades com "por item" na última coluna são as de embalagem — KIT, JG, CX, PT, FD, SC, RL. As três de calendário sem conversão global — DIA, MES, ANO — dependem de contrato e também só convertem por item. A resma (RS) tem fator canônico de 500 folhas por convenção de mercado. 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. ItemSupplierUom.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. 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 44.092 lb pelo fator exato = 19.999,79 kg erro = +17,97 kg a R$ 8,50/kg = R$ 152,77 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.
- Fator usado nunca é reescrito (RN-UOM-09): o
ItemUomConversionversiona com vigência e aItemSupplierUomtroca de linha; registro nunca usado corrige-se no lugar. - SPED. O registro 0220 da EFD não aceita o mesmo código de unidade com dois fatores para o mesmo item: quem declara precisa de código distinto quando o fator muda. O Nexio não emite a EFD. A integração entrega ao ERP, por linha de documento, o código da unidade, o fator congelado e a linha de embalagem (
itemSupplierUomId), e sinaliza quando o mesmo código já teve outro fator para o item; o código declarado no 0190 e no 0220 é do ERP. Cadastrar código distinto no Nexio (CX10) é escolha do tenant, nunca exigência.
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(21,10) | O teto é o da NF-e: vUnCom e vUnTrib têm até 11 inteiros e 10 casas — e o campo existe justamente para absorver conversão de embalagem. |
| Valores monetários totalizados | numeric(19,4) | Guardados com 4 casas e arredondados às casas da moeda (minorUnits, de 0 a 4 — ver a página de Moeda): BRL e USD 2, JPY 0, BHD 3. Nunca se assume 2 casas; o vProd da NF-e, em reais, tem 2. |
| 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 | só ao gravar | Valor derivado. No cálculo não arredonda: saldo, tolerância e total partem da quantidade digitada e dos fatores congelados, nunca da coluna gravada. Ao gravar, 6 casas, ROUND_HALF_UP, para consulta e exibição. Arredondar no meio do cálculo é onde nascem o estoque fantasma e o clássico 4,99998 (RN-UOM-11). |
| Preço unitário | não | Não derive por divisão. Armazene na unidade em que foi cotado. |
| Total da linha | sim | Às casas da moeda da linha (minorUnits), 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,9972 | 259,00 | R$ 0,00 — passa, por sorte |
| Idem, pedido de 13 CX (156 UN) | 156 × 3,0833 = 480,9948 | 480,99 | −R$ 0,01 contra 13 × 37,00 = 481,00 |
| Preço derivado por UN com 2 casas | 84 × 3,08 | 258,72 | −R$ 0,28 |
Com 84 UN as quatro casas ainda acertam o total, mas é coincidência de arredondamento: com 13 caixas o total derivado já diverge em um centavo. No pior caso, quatro casas passam da tolerância da SEFAZ acima 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(orderedQty × convNum × priceConvNum × priceAmount ÷ (convDen × priceConvDen × priceQty), minorUnits)— todos os fatores multiplicados antes da única divisão, em decimal exato, e um só arredondamento no fim. No exemplo:7 × 12 × 1 × 37,00 ÷ (1 × 12 × 1) = 259,00. 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, e unidade de embalagem não é base. Imutável depois da primeira linha de documento — requisição, cotação, pedido, recebimento ou nota, em qualquer estado. Alterar responde 409 e sugere criar item sucessor (RN-UOM-15). |
| 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. 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. |
A regra lê a classe da unidade base do item, não o tipo: item cuja base é da classe Service aceita apenas unidades dessa classe e não admite nenhuma linha de conversão; item de serviço com base em HR ou M2 converte dentro da classe normalmente. É 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 e em fardo de 240; o C, em milheiro. É o mesmo item. E um mesmo fornecedor vende em mais de uma unidade — por isso a unidade de compra é uma filha da relação, ItemSupplierUom, com N linhas por relação.
| Campo | Regra |
|---|---|
| uomId | Na ItemSupplierUom. Obrigatório. Uma unidade em que este fornecedor vende o item; o pedido sai numa delas. Uma linha ativa por unidade na relação, e exatamente uma marcada isDefault. |
| numerator / denominator | Na ItemSupplierUom. Obrigatórios. 1 uom = numerator ÷ denominator × baseUom. Os nomes são os do Cadastro de Item. Linha usada é imutável: embalagem nova é linha nova, e a usada é desativada (RN-UOM-09, RN-ITM-25). Prioridade máxima na hierarquia — sobrepõe a conversão do item. |
| priceUomId | Vive na versão de preço (ItemSupplierPrice), não na relação — ver a caixa abaixo. 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, e o fator dessa conversão vai congelado para a linha do documento (RN-UOM-10). |
| priceQty | Na versão de preço. Default 1. Ver Preço. |
| moq / orderIncrement | Na ItemSupplierUom. Os nomes são os do Cadastro de Item, que é a spec dona do campo. Na unidade daquela linha. O múltiplo é o lote fechado — o comprador não consegue pedir 7 quando o fornecedor só vende de 5 em 5. |
| supplierUomLabel | Na ItemSupplierUom. Texto livre: como o fornecedor chama a embalagem. Serve só para conferência humana contra a proposta em PDF. Nunca participa de cálculo. |
| receiptTolerance | Na relação. Tolerância de recebimento — quantidade e data, no objeto definido em Tolerância. Fica no fornecimento porque é o fornecedor quem tem variabilidade de embalagem, com override na linha do pedido e herança do estabelecimento. |
| validFrom / validTo | Vive na versão de preço. Versionamento: é o que permite que o preço mude sem reescrever o passado. |
A relação, as unidades de compra e a versão de preço. ItemSupplier é a relação de fornecimento e leva as tolerâncias. As unidades em que o fornecedor vende são a filha ItemSupplierUom — uomId, numerator/denominator, isDefault, supplierUomLabel e múltiplo —, e é dela que a unidade de medida se serve no nível 1. ItemSupplierPrice é a versão de preço, append-only, e leva priceUomId, priceQty, valor, moeda, faixas, modalidade de frete e a vigência. O fator de embalagem é do fornecimento, não da tabela de preço — e é por isso que reajustar preço não cria fator de conversão novo, e trocar de caixa não cria preço novo. Ver Cadastro de Item.
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. Linha nova na ItemSupplierUom | CX com numerator = 10 numa linha nova; a linha de 12, já usada, é desativada na mesma transação e fica legível para os documentos que a congelaram. Documentos novos usam a linha nova, e fica auditável quando 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, leva à troca da unidade de compra na relação e sinaliza os pedidos em aberto que congelaram a linha antiga. |
Embalagem que muda de conteúdo vira linha nova, nunca fator editado. Quando a caixa passa a ter 10 onde tinha 12, grava-se uma linha nova de ItemSupplierUom — pode ser o mesmo código CX — e a linha usada é desativada na mesma transação. A linha não tem vigência: a relação tem, a cada momento, uma linha ativa por unidade, e cada documento congela a linha e o fator que usou (itemSupplierUomId, convNum/convDen, RN-UOM-10). É isso que mantém o cadastro legível — com o fator editado no lugar, CX valeria 12 até ontem e 10 hoje, e nenhuma consulta saberia distinguir — sem exigir código de unidade por conteúdo de caixa. O fluxo de divergência descrito acima continua igual: o recebimento pergunta se foi quantidade ou embalagem, e "embalagem" leva à troca da linha, não à edição de um fator. E o preço acertado pela caixa antiga sai da sugestão até existir versão de preço nova (RN-ITM-38): as mesmas 7 caixas a R$ 37,00, que eram 84 UN a R$ 3,0833, passariam a ser 70 UN a R$ 3,70 — 20% mais caro sem nenhum número mudar na tela. Ver RN-UOM-09 e RN-ITM-25.
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 itemSupplierUomId SNAPSHOT da linha → (id da CX de 12) convNum, convDen bigint SNAPSHOT do fator → 12, 1 priceAmount numeric(21,10) → 37,0000000000 priceUomId → CX priceQty numeric(19,6) → 1 priceConvNum, SNAPSHOT do fator → 1, 12 priceConvDen bigint base → unidade de preço
Por que dois pares. convNum/convDen é o fator da unidade pedida para a base; ele não serve quando o preço é cobrado em outra unidade — ItemSupplierUom.uomId = SC com priceUom = TN, que esta especificação trata como caso de primeira classe. Sem o segundo par, calcular qtyInPriceUom obrigava a resolver a conversão no cadastro, no momento do cálculo, e aí uma conversão nova, ou uma linha nova de unidade de compra, mudava o total de uma linha antiga — a divergência de centavos que produz as rejeições 629 e 630. O par vem congelado da versão de preço (RN-ITM-38). Quando a unidade de preço é a base, ele é 1/1; quando é a própria unidade pedida, é o inverso de convNum/convDen — e grava assim mesmo: snapshot não tem exceção. Ver RN-ITM-29.
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 de preço e o piso absoluto do three-way match são da spec da Nota; esta página garante a base comum da comparação — unidade base e snapshot da linha — e a resolução da unidade da nota. A unidade da nota é texto do emitente (uCom, e uTrib no grupo tributário) e resolve em dois passos, nunca por semelhança nem por classe genérica: código exato de unidade do Nexio, depois apelido da relação (ItemSupplierUomAlias, RN-ITM-35). O apelido diz qual unidade é; o fator vem do snapshot da linha do pedido quando a unidade é a pedida, e da hierarquia de conversão (RN-UOM-06) quando não é — nunca do apelido. Texto que não resolve, ou unidade resolvida sem fator, levanta exceção de conferência e pede ao comprador o apelido ou a linha de unidade de compra — é 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, D parece o mais barato — R$ 3,05 é o menor número da coluna — e, entre as caixas, A parece mais barata que B, porque R$ 37 é menos que R$ 68. Normalizado, B ganha (R$ 2,83), C é o segundo (R$ 2,90), D o terceiro (R$ 3,05) e A o último (R$ 3,08). Os dois primeiros seriam os dois descartados 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 C exige um milheiro — R$ 2.900,00, ou R$ 29,00 por unidade requisitada. O D vende solto: 100 UN por R$ 305,00, ou R$ 3,05. Nesta compra D ganha, A é o segundo e B — o mais barato por unidade — só o terceiro. 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; propostas na mesma unidade de serviço, porém, comparam direto, porque não há conversão a fazer.
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.
O objeto receiptTolerance
Um objeto só, com o mesmo formato nos três níveis em que pode aparecer. Esta página é a dona: o Cadastro de Fornecedor e o Cadastro de Item citam daqui, não repetem.
| Campo | Tipo | O que faz |
|---|---|---|
| overTolerancePct | decimal? | Quanto a mais que o pedido o recebimento aceita, em percentual da quantidade da linha. Acima disso o recebimento da linha é recusado. |
| underTolerancePct | decimal? | Quanto a menos fecha a linha. Dentro da faixa, o saldo em aberto encerra; fora dela, o saldo continua aberto. Nunca recusa o que chegou. |
| earlyDays | int? | Dias de antecipação aceitos sobre a data prometida da linha. |
| lateDays | int? | Dias de atraso aceitos. Nunca recusa entrega — classifica a linha para o OTIF do fornecedor. |
| earlyReceiptPolicy | enum | Warn | Block: o que fazer com a entrega antecipada além de earlyDays. Warn recebe, registra e marca a linha fora de janela; Block recusa na doca — para quem paga armazenagem ou tem contrato de janela. Default Warn. |
Data adiantada é parâmetro; data atrasada é medida. Atraso nunca impede receber — recusar mercadoria que chegou tarde não desfaz o atraso, só cria um problema novo —, então lateDays existe para dizer o que conta como on time no OTIF, e nada mais. Antecipação é o caso em que recusar faz sentido para parte dos tenants e não faz para a maioria, e por isso é o único que tem política: o padrão recebe, e quem quiser travar liga o Block no nível que quiser.
Três níveis, resolvidos campo a campo
A precedência é estabelecimento do fornecedor no documento → ItemSupplier (a relação) → linha do pedido, e a mais específica vence. A resolução é campo a campo, não objeto a objeto: cada campo é lido no nível mais específico em que estiver preenchido, e campo nulo herda do nível acima. 0 é valor, não ausência — overTolerancePct = 0 na linha significa "esta linha é exata" e não é sobrescrito por nada.
| Campo | Estabelecimento | Relação | Linha | Vale |
|---|---|---|---|---|
| overTolerancePct | 5% | 10% | — | 10% |
| underTolerancePct | 5% | — | — | 5% |
| earlyDays | 2 | — | 0 | 0 |
| lateDays | 0 | — | — | 0 |
| earlyReceiptPolicy | Warn | — | Block | Block |
Com objeto a objeto, o 10% da relação apagaria o underTolerancePct do estabelecimento e uma entrega de 80 das 84 unidades seria recusada por um campo que ninguém decidiu. É a mesma base de dados dando dois resultados opostos, e é por isso que a regra está escrita (RN-UOM-20).
Quando nenhum nível preenche o campo, o sistema não inventa faixa: overTolerancePct = 0, underTolerancePct = 0, earlyDays = 0, lateDays = 0 e earlyReceiptPolicy = Warn. Ou seja, quantidade exata, e qualquer dia fora da data prometida conta no OTIF — que é a definição do indicador, não uma punição.
Esta página define a tolerância; quem recusa é o documento. O ato de recebimento, a tela, o código de erro da recusa por quantidade ou por antecipação e o registro da divergência são da spec do Recebimento. Aqui ficam o objeto, a precedência e a matemática — a regra que ela aplica. A tolerância de preço e o piso absoluto do three-way match são da spec da Nota.
Avaliação em unidade base
| 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 | Unidade de compra SC na ItemSupplierUom, priceUom = TN, com o fator do saco para a base declarado na linha do fornecedor ou no 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 | Linha nova na ItemSupplierUom da relação, com a usada desativada na mesma transação, mais o snapshot da linha e do fator no documento. Ver § Unidade, item e fornecedor e RN-UOM-09. |
| 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 nas classes físicas, opcional em tempo, proibido em embalagem
Unidade sem fator canônico em Count, Weight, Volume, Length ou Area — fora de embalagem — responde 400 UOM_CANONICAL_FACTOR_REQUIRED. Na classe Time ele é opcional, porque dia, mês e ano são contrato e não física; na classe Service não existe. Unidade sem canônico nunca resolve no nível 3 (RN-UOM-19); 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.
A hierarquia de resolução é fixa e o fallback final é erro
Item × fornecedor (a linha ativa da ItemSupplierUom da relação para aquela unidade), depois item, depois global intraclasse, depois 422 UOM_CONVERSION_NOT_CONFIGURED_UNPROCESSABLE. Nunca fator implícito. O nível 3 só resolve entre unidades que tenham fator canônico — faltando de um dos lados, a hierarquia cai direto no 422, sem passar pelo global (RN-UOM-19).
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. 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.
Fator já usado nunca é reescrito: muda por registro novo, e o documento congela o que usou
Uma regra só para toda conversão já usada em cotação, pedido, recebimento ou nota: o fator não é editado no registro. Fator diferente para unidade já usada grava registro novo na mesma transação, na forma da entidade: no ItemUomConversion, que tem vigência, a versão vigente fecha em validTo e a nova abre em validFrom; na ItemSupplierUom, que não tem, a linha usada é desativada e a nova é criada. Nada é apagado, e as duas rotas se comportam igual — o PUT do conjunto (…/uom-conversions e …/suppliers/{supplierItemId}/uoms) recebe o fator novo e decide sozinho entre corrigir no lugar o registro nunca usado e gravar registro novo sobre o usado; nenhuma das duas responde 409 por fator em uso. Mudança de embalagem não exige código de unidade novo. O documento congela o registro e o fator (RN-UOM-10), e por isso nenhum documento emitido ou em aberto muda de significado. O código declarado ao SPED (0190/0220) é do ERP, que recebe código e fator por linha.
Todo documento copia o fator para a linha
convNum, convDen, priceConvNum e priceConvDen são snapshot, não referência; quando a linha usa unidade de compra do fornecedor, itemSupplierUomId vai junto. Documento emitido nunca muda de significado, nem na quantidade nem no total. Ver RN-ITM-29.
Quantidade convertida não se arredonda no cálculo; grava com 6 casas
Arredonda-se a quantidade digitada, para as casas da unidade escolhida. A quantidade convertida é calculada em decimal exato, multiplicando antes de dividir (RN-UOM-05), com pelo menos 18 casas nos passos intermediários, e gravada com 6 casas, ROUND_HALF_UP, para consulta. Saldo em aberto, tolerância e total de linha são calculados a partir das quantidades digitadas e dos fatores congelados de cada linha, nunca somando colunas já arredondadas; um saldo cujo valor a 6 casas é zero é zero, e a linha fecha.
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
Às casas da moeda da linha (minorUnits), meio para cima, sobre a fórmula que multiplica todos os fatores antes da única divisão. O total do documento é a soma dos totais de linha já arredondados — nunca um segundo arredondamento. Valor gravado em coluna de 4 casas, preço em coluna de 21 dígitos com 10 casas.
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 depois da primeira linha de documento, e não é embalagem
O gatilho é o mesmo do Cadastro de Item (RN-ITM-10): existindo qualquer linha de requisição, cotação, pedido, recebimento ou nota que referencie o item, em qualquer estado, a base não muda — 409, com a sugestão de criar item sucessor. E unidade com isPackaging nunca é base.
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.
A tolerância de recebimento resolve campo a campo, da mais específica para a mais geral
receiptTolerance é lido em três níveis — estabelecimento do fornecedor no documento, relação ItemSupplier, linha do pedido —, um campo de cada vez: vale o nível mais específico que preencheu aquele campo, e campo nulo herda do nível acima. 0 é valor. Sem nenhum nível preenchido, tudo é 0 e earlyReceiptPolicy é Warn. Atraso nunca recusa entrega — só classifica o OTIF; antecipação além de earlyDays recusa apenas quando a política resolvida for Block.
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. Propostas na mesma unidade — inclusive de serviço — comparam direto, sem conversão.
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 também.
Unidade de embalagem não tem fator canônico, e sem canônico não há nível 3
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, que exige lookup:uoms: read e lookup não se implicam. |
| GET /uoms/{id} | read:uoms | Com as conversões padrão da classe. |
| POST /uoms | create:uoms | Cria unidade do tenant. Recusa colisão de código com a camada global. |
| PUT /uoms/{id} | update:uoms | Respeita a matriz de imutabilidade. Campos bloqueados respondem 409 nomeando o campo. |
| PATCH /uoms/{id}/deactivate | delete:uoms | Devolve a lista de documentos e itens que impedem, quando impedem. |
| PUT /uoms/{id}/alias | update:uoms | Rótulo de exibição do tenant sobre unidade global. Não altera o código. |
| PUT /uoms/visibility | update: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}/uoms — 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 em classe física (Count, Weight, Volume, Length, Area) fora de unidade de embalagem (RN-UOM-03) |
| UOM_CANONICAL_FACTOR_NOT_ALLOWED | 400 | 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_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.
Registro
Histórico de revisões
| Versão | Data | O que mudou |
|---|---|---|
| v1.8 | 30/10/2026 | Permissões padronizadas em read/lookup/create/update/delete (decisão de 30/10/2026). manage:uoms vira, por ação: POST /uoms → create:uoms; PUT /uoms/{id}, PUT /uoms/{id}/alias e PUT /uoms/visibility → update:uoms; PATCH /uoms/{id}/deactivate → delete:uoms. O autocomplete dos formulários passa a exigir lookup:uoms, separada de read:uoms. |
| v1.7 | 20/09/2026 | Correção da validação de 18/09 (achado 18a, completado na base) e decisões do Roger. Tolerância: o objeto receiptTolerance passa a ser definido aqui — overTolerancePct, underTolerancePct, earlyDays, lateDays e earlyReceiptPolicy (Warn | Block, default Warn) —, com a precedência estabelecimento → relação → linha resolvida campo a campo e 0 valendo como valor (RN-UOM-20). Atraso nunca recusa entrega: classifica o OTIF. O código de erro da recusa é da spec do Recebimento. |
| v1.6 | 20/09/2026 | Recuo da validação de 18/09 (achado 18b). A tolerância de preço do three-way match — percentual, absoluta e o piso por linha — sai desta página e passa a ser da spec da Nota; fica aqui a base comum da comparação e a resolução da unidade da nota. |
| v1.5 | 11/09/2026 | Correção da validação R3: a decisão do fator na linha deixa de atribuir ao Nexio a regra do registro 0220 (um código, um fator) e remete ao ERP, que traduz o código — ET6-1. |
| v1.4 | 11/09/2026 | Validação de 10/09 e Ata B15. Nível 1 da hierarquia passa a ser a linha ativa de ItemSupplierUom. Uma regra para fator usado (RN-UOM-09): registro novo, nunca edição; sai o CX10 obrigatório, o erro UOM_CONVERSION_LOCKED_CONFLICT e a pendência "nova versão × código novo"; o 0220 da EFD fica com o ERP. Unidade da nota resolve por código exato ou apelido da relação. Preço numeric(21,10), valores numeric(19,4) arredondados por minorUnits; quantidade convertida grava com 6 casas e não arredonda no cálculo; fórmula do total com uma divisão só. Fator canônico opcional em Time; DIA sem conversão global; POL na semente (39 unidades); base nunca embalagem e gatilho único da imutabilidade; unidade idêntica compara direto. Corrigidos os exemplos das quatro casas, da libra e do comparativo. Marcas de histórico retiradas do corpo. |
| v1.2 – v1.3 | 09/09/2026 | isPackaging e fator canônico anulável (sem canônico, sem nível 3); priceConvNum/priceConvDen na linha; conversão interclasse declarada pelo item; relação e versão de preço separadas. |
| até v1.1 | 29/08 – 08/09/2026 | Primeira especificação (29/08): sigla local como chave, par inteiro, hierarquia com erro no fim, snapshot na linha, preço na unidade acordada; nomes de campo alinhados ao Cadastro de Item (08/09). |