Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Relatório de Custo e Margem — Pedidos de Venda Omie Aplicativo local, somente leitura, que consulta a API da Omie e calcula informações que a Omie não fornece diretamente sobre um pedido de venda: custo unitário e total por item, margem em reais, margem sobre a venda, markup sobre o custo e o resumo operacional do pedido. O aplicativo nunca cria, altera ou exclui dados na Omie. Todas as chamadas usam apenas métodos de consulta: ListarPedidos, ConsultarPedido, ConsultarProduto, ListarPosEstoque, ListarEtapasFaturamento, ListarVendedores, ConsultarCliente e ListarContasReceber.README 전문 보기 ↗
Aplicativo local, somente leitura, que consulta a API da Omie e calcula informações que a Omie não fornece diretamente sobre um pedido de venda: custo unitário e total por item, margem em reais, margem sobre a venda, markup sobre o custo e o resumo operacional do pedido.
O aplicativo nunca cria, altera ou exclui dados na Omie. Todas as
chamadas usam apenas métodos de consulta: ListarPedidos, ConsultarPedido,
ConsultarProduto, ListarPosEstoque, ListarEtapasFaturamento,
ListarVendedores, ConsultarCliente e ListarContasReceber.
Além da consulta individual de um pedido/orçamento, há uma área de Relatórios com três visões agregadas — Vendas, Orçamentos e Comissionamento — ver seção dedicada abaixo.
.env).npm install
.envcp .env.example .env
OMIE_APP_KEY e OMIE_APP_SECRET com as credenciais do seu
aplicativo Omie (veja a seção Segurança sobre como criar um
aplicativo dedicado, somente leitura).PORTA e OMIE_INTERVALO_MIN_MS.O arquivo .env nunca é commitado (está no .gitignore) e seu conteúdo
nunca é impresso em log, resposta de API ou arquivo gerado pelo aplicativo.
npm start
Isso compila o TypeScript (backend e frontend) e inicia o servidor. Acesse
http://localhost:3000 no navegador (ou a porta configurada em PORTA).
Para desenvolvimento com recompilação automática do backend:
npm run dev
(nesse modo, reinicie node dist/server.js manualmente após cada mudança,
ou rode em outro terminal).
npm test
Os testes do núcleo (cálculo de custo, margem e montagem do relatório) usam um cliente Omie falso, com respostas fixas — nenhum teste toca a rede ou depende da Omie real.
O custo unitário de cada produto vem do custo de entrada mais recente
disponível na posição de estoque (ObterEstoqueProduto), não do cadastro
do produto.
O módulo src/calculo/custo.ts define, no topo do
arquivo, a constante CAMPOS_CUSTO_CANDIDATOS:
export const CAMPOS_CUSTO_CANDIDATOS: readonly string[] = [
'nCMC',
'nCustoMedio',
'nPrecoUnitario',
'nCustoUnitario',
];
Para alterar quais campos são usados, ou a ordem de prioridade entre eles, edite essa lista. Nenhuma outra parte do código precisa mudar.
listaEstoque da resposta, na ordem em que
aparecem, procurando o primeiro campo candidato (na ordem da lista
acima) que exista, seja convertível para número e seja maior que zero.listaEstoque, repete a busca nos campos do
nível raiz da resposta.0 e a origem é
"sem custo".0 e a origem é
"indisponivel". Um produto com problema nunca derruba o relatório
inteiro — os demais itens continuam sendo calculados normalmente.A origem do custo (estoque.nCMC, raiz.nPrecoUnitario, sem custo,
indisponivel, etc.) fica disponível tanto na API (origemCusto de cada
item) quanto na exportação CSV, para rastreabilidade total.
Itens sem custo permanecem no relatório e entram nos totais — nunca são escondidos ou excluídos — e recebem um alerta próprio, pois um custo zero infla artificialmente a margem calculada.
Por item:
receita = valor_mercadoria
(ou quantidade × valor_unitario − valor_desconto, se valor_mercadoria ausente)
custo_total = custo_unitario × quantidade
margem_valor = receita − custo_total
margem_venda% = margem_valor / receita (quanto sobra de cada real vendido)
markup_custo% = margem_valor / custo_total (quanto a venda representa de ganho sobre o custo)
Margem e markup não são a mesma coisa. Um markup de 100% sobre o custo corresponde a uma margem de 50% sobre a venda — não a 100%. Exemplo: um produto que custa R$ 50 e é vendido por R$ 100 tem markup de 100% (50/50 = 100%) e margem de 50% sobre a venda (50/100 = 50%). A interface e o CSV sempre exibem as duas métricas lado a lado, identificadas separadamente.
margem_venda_percentual = null (nunca zero
artificial, Infinity ou NaN).markup_custo_percentual = null, pelo mesmo motivo.receita_total = soma das receitas dos itens
custo_total = soma dos custos totais dos itens
margem_total = receita_total − custo_total
margem_venda_total% = margem_total / receita_total
Isso é calculado exatamente assim porque pondera cada item pelo seu peso financeiro real no pedido.
A margem percentual total nunca é a média das margens percentuais dos
itens. Essa abordagem estaria errada: um item de R$ 10.000 com 5% de
margem e um item de R$ 10 com 90% de margem não têm o mesmo peso no
resultado final do pedido, e uma média simples (47,5%) distorceria
completamente a leitura real da rentabilidade. O cálculo correto
(margem_total / receita_total) reflete o resultado financeiro agregado de
verdade. O teste tests/calculo/margem.test.ts
("Caso 4") verifica isso explicitamente e falha se alguém substituir o
cálculo por uma média de percentuais.
O total do pedido informado pela Omie (total_pedido.valor_total_pedido)
pode incluir frete, seguro e outras despesas acessórias que não entram
no custo de produto e, portanto, não fazem parte da margem calculada por
este aplicativo.
Por isso, o relatório compara receita_total calculada com o total
informado pela Omie. Se a diferença for maior que R$ 0,05, um alerta é
exibido, explicando essa possível causa e mostrando os dois valores e a
diferença — nunca escondendo a divergência.
OMIE_INTERVALO_MIN_MS (padrão
300 ms). Implementado como uma fila que serializa todas as chamadas,
segura mesmo com requisições concorrentes.425 (limite de
requisições excedido), falha de rede e resposta que não seja JSON válido
(até 3 tentativas, com backoff de 500ms/1s/2s). Qualquer outro erro da
Omie (regra de negócio, dado inválido etc.) sobe imediatamente, sem
retry — para não mascarar problemas reais.faultstring/faultcode no corpo da
resposta, independentemente do status HTTP.POST /api/cache/limpar.ListarPedidos idênticas em sequência. Como
/api/pedidos e /api/orcamentos fazem a mesma chamada subjacente para a
mesma página (a Omie pagina os dois tipos juntos — a separação é
filtrada localmente), o resultado bruto de cada combinação de filtros é
cacheado em memória: trocar de aba ou repetir a mesma consulta reaproveita
a página já buscada, em vez de disparar o bloqueio da Omie. Use
POST /api/cache/limpar para forçar uma releitura.ListarPosEstoque com todos os
produtos na data informada. Por isso a posição inteira é buscada (com
paginação de 500 registros por página) e cacheada em memória por data de
referência na primeira vez que um item daquele pedido precisa de custo; as
buscas seguintes (mesmo pedido ou outros pedidos com a mesma data) usam o
resultado já cacheado.OMIE_APP_KEY, OMIE_APP_SECRET). Elas nunca são impressas em log,
retornadas por qualquer rota da API, incluídas em mensagens de erro ou
gravadas em arquivo (CSV incluído).GET /api/saude informa apenas credenciaisCarregadas: true/false,
nunca o conteúdo das credenciais.por_codigo) é validada no backend antes de qualquer uso —
por_pagina é sempre limitado a no máximo 200.textContent/criação de nós DOM, nunca
innerHTML, para renderizar qualquer valor vindo da API.Na tela de um Orçamento, a tabela de itens ganha quatro colunas adicionais para simular um preço de venda diferente do original, sem alterar nenhum dado da Omie:
| Coluna | Fórmula |
|---|---|
| Markup editável | Editável. Valor inicial = preço unitário original ÷ custo unitário |
| Preço de venda | custo unitário × markup editável |
| Preço total | quantidade × preço de venda |
| Subtotal | igual ao Preço total |
A soma dos subtotais de todos os itens é exibida em destaque como Valor total do orçamento (simulação de venda), logo acima da tabela, e é recalculada instantaneamente a cada edição de markup — sem recarregar a página nem consultar a Omie novamente.
Regras de cálculo (ver public-src/precificacao.ts):
null, nunca Infinity/NaN), com aviso "Sem custo" no
campo. Se o usuário digitar um markup mesmo assim, o preço de venda
resultante é 0 (custo 0 × qualquer markup = 0) — o sistema nunca
inventa um preço.A Omie modela Orçamento e Pedido de Venda como o mesmo tipo de documento
(pedido_venda_produto), diferenciados apenas pelo campo cabecalho.etapa.
Os códigos de etapa — e principalmente os rótulos exibidos para cada
um — são configuráveis por conta, em Configurações de Venda de Produtos >
Kanban de Vendas. Não existe um código de etapa fixo e universal que
signifique "isto é um orçamento" em todas as contas Omie: a documentação
genérica da Omie sugere etapa "00", mas isso não é garantido.
Por isso a classificação nunca usa um código de etapa fixo no código-fonte.
Em vez disso, consulta a configuração real da conta via
ListarEtapasFaturamento (/produtos/etapafat/, operação "Venda de
Produto" — cCodOperacao "11") e verifica se a descrição configurada
para aquela etapa contém a palavra "orçamento" (ver
src/omie/classificacaoDocumento.ts).
Verificado em 2026-09-04 contra a conta configurada no .env deste
projeto: a etapa "00" (inativa) e a etapa "10" (ativa, em uso
corrente) estão ambas rotuladas "Orçamento" nesta conta — prova de que
um código fixo não seria confiável.
Quando a etapa de um documento não existe na configuração retornada pela
Omie, a classificação fica ambígua: o documento nunca é tratado como
PEDIDO nem como ORÇAMENTO por padrão. Ele é excluído das listagens
(/api/pedidos e /api/orcamentos) e, se consultado diretamente por
identificador, o relatório é retornado com tipoDocumento: null e um
motivoClassificacaoAmbigua explicando o motivo — nunca classificado por
adivinhação.
As rotas de detalhe/CSV são específicas por tipo
(/api/pedido/:id vs. /api/orcamento/:id) e rejeitam com HTTP 409
quando o identificador informado corresponde ao tipo errado — nunca
misturando as duas categorias, mesmo em uma consulta direta por número.
Além da consulta individual, há uma área de Relatórios (aba no topo da interface) com três visões agregadas, cada uma completamente separada das outras — nunca uma tabela ou total misturando categorias diferentes:
Cada relatório tem filtro de período (data_de/data_ate — filtram pela
data de previsão do pedido informada na Omie, a única data disponível em
ListarPedidos), filtro por vendedor (carregado diretamente de
ListarVendedores, nunca uma lista fixa no código) e busca local por
número/cliente/vendedor.
A Omie não expõe nenhum dos campos abaixo prontos — por isso, em vez de
adivinhar, as seguintes definições foram fixadas no código
(src/comissionamento/calcularMargemComissionamento.ts e
calcularComissao.ts) e confirmadas com o usuário:
margemVendaTotal) para determinar a
faixa — essa métrica de custo continua disponível no relatório apenas
para análise comercial, mas nunca influencia a comissão.margem_comissionamento % = (valor_da_venda − despesas) ÷ valor_da_venda × 100.
total_pedido.valor_total_pedido (bruto, inclui
impostos destacados na nota) — é também a base da comissão
(base × percentual da faixa), conforme o exemplo do usuário.valor_da_venda − soma(valor_mercadoria dos itens). Optou-se
por essa diferença em vez de somar campos individuais de imposto
(valor_IPI, valor_icms, valor_st, valor_pis, valor_cofins,
valor_ibs, valor_cbs...) porque a Reforma Tributária introduziu
campos novos (IBS/CBS) que coexistem com os antigos (ICMS/PIS/COFINS)
nesta conta, arriscando contagem duplicada ao somar linha por linha.frete.valor_frete +
frete.valor_seguro + frete.outras_despesas do pedido (sempre 0
nesta conta até o momento, mas somados por completude).Arquitetura/Comissão por Tabela de Preços.md.ListarContasReceber com id_origem: "VENR" (gerados a partir de um
Pedido de Venda) trazem nCodPedido/numero_parcela — títulos lançados
manualmente (id_origem: "MANR") não têm vínculo com nenhum pedido e são
ignorados. Pedidos ainda não faturados legitimamente não têm título
algum — aparecem marcados como "Nenhum título financeiro localizado".O funil comercial (quantidade de orçamentos convertidos em pedido, taxa de conversão) não foi implementado. A Omie modela Orçamento e Pedido como o mesmo registro, apenas com a etapa alterada ao longo do tempo — não há, na API, um histórico de mudança de etapa. Calcular uma taxa de conversão exigiria que esta aplicação guardasse um snapshot dos documentos ao longo do tempo, o que contradiz o design atual (somente leitura, sem banco de dados, cache apenas em memória durante a execução do processo). Implementar isso exigiria adicionar persistência — uma mudança de arquitetura, não uma funcionalidade pontual.
A Omie rejeita chamadas concorrentes do mesmo método ("Já existe uma
requisição desse método sendo executada"), então a busca de títulos por
vendedor é sequencial, um vendedor por vez. Num período com muitos
vendedores distintos, a primeira consulta pode levar de dezenas de segundos
a poucos minutos; consultas seguintes com os mesmos vendedores reaproveitam
o cache em memória e são quase instantâneas. Use POST /api/cache/limpar
para forçar uma releitura.
| Método | Rota | Descrição |
|---|---|---|
GET | / | Serve a interface web (Consulta individual + Relatórios). |
GET | /api/pedidos | Lista apenas documentos classificados como PEDIDO. Parâmetros: pagina, por_pagina (máx. 200), etapa, data_de, data_ate (dd/mm/aaaa). |
GET | /api/orcamentos | Lista apenas documentos classificados como ORÇAMENTO. Mesmos parâmetros de /api/pedidos. |
GET | /api/pedido/:identificador | Retorna o pedido completo, com custos, margens, totais, alertas e resumo operacional. Responde 409 se o identificador for de um orçamento. Parâmetro por_codigo=true para tratar o identificador como código interno em vez de número do pedido. |
GET | /api/pedido/:identificador/csv | Exporta o mesmo relatório em CSV (;, UTF-8 com BOM). Aceita por_codigo. |
GET | /api/orcamento/:identificador | Equivalente a /api/pedido/:identificador, mas para orçamentos — responde 409 se o identificador for de um pedido. Inclui avisoOrcamento explicando que os valores são projeção, não receita concretizada. |
GET | /api/orcamento/:identificador/csv | Exporta o mesmo relatório em CSV. |
GET | /api/vendedores | Lista os vendedores cadastrados na Omie (codigo, nome, inativo). |
GET | /api/relatorios/vendas | Relatório agregado de Vendas (só PEDIDO). Parâmetros: data_de, data_ate, vendedor (código), busca. |
GET | /api/relatorios/orcamentos | Relatório agregado de Orçamentos (só ORÇAMENTO). Mesmos parâmetros. |
GET | /api/relatorios/comissionamento |
src/omie/ → cliente HTTP, detecção de erro, rate limiting/retry, cache, classificação Pedido/Orçamento
src/calculo/ → custo, margem/markup, arredondamento, tipos
src/relatorio/ → orquestração da consulta individual + relatórios agregados de Vendas/Orçamentos
src/comissionamento/ → faixas de comissão, distribuição por parcela, orquestração do relatório de comissionamento
src/rotas/ → rotas Express (documentos.ts, relatorios.ts, comissionamento.ts, vendedores.ts) e tratamento de erros HTTP
src/csv/ → exportação CSV
src/validacao.ts → validação de toda entrada externa
src/server.ts → bootstrap do Express
public/ → interface web (HTML/CSS/JS compilado)
public-src/ → código-fonte TypeScript do frontend
tests/ → testes automatizados (sem rede), com cliente Omie falso
status_titulo === "RECEBIDO"ConsultarContaReceberrecebimento: null| Relatório de comissionamento (só Vendas) — faixa, percentual, valor líquido, comissão total/liberada/pendente e parcelas por pedido. Mesmos parâmetros de filtro. |
POST | /api/cache/limpar | Limpa o cache em memória de produtos, estoque, etapas de faturamento, vendedores, clientes e contas a receber, forçando nova leitura. |
GET | /api/saude | Informa apenas se as credenciais foram carregadas (true/false). |