# GransoftWeb — documentação completa da API para desenvolvedores > Versão 1.0 (2026-11-11). Índice: https://www.gransoftweb.com/developers/llms.txt > Swagger: http://www.server1.gepersistemas.com.br:8082/swagger/ui/index --- # Início rápido: API do GransoftWeb Este guia mostra o mínimo para fazer a primeira chamada à API do GransoftWeb e as regras que valem para todos os métodos. ## 1. O que você precisa - **Um acesso ao GransoftWeb.** Toda chamada é feita em nome de um usuário. Para testar, peça ao administrador da sua empresa que crie um **usuário só para a integração**, com as permissões necessárias e, se possível, sobre dados de teste. - **O `userHash` desse usuário.** É a chave que identifica o usuário nas chamadas. Há duas formas de obtê-lo: - entrar no GransoftWeb com o usuário da integração, abrir `https://www.server1.gransoftweb.com/API/` e copiar a **chave de teste** exibida na página; ou - chamar o método de autenticação com e-mail e senha (veja [Autenticação](autenticacao.md)). - **O endereço do servidor da empresa** (`Servidor`), por exemplo `https://www.server1.gransoftweb.com`. Ele vem na resposta da autenticação e aparece como "URL base" na página do catálogo de APIs. - **Node.js 18 ou superior**, se for usar o kit pronto. > Trate o `userHash` como uma senha. Quem tem o `userHash` acessa o sistema com as permissões daquele usuário. Guarde-o em variável de ambiente, nunca no código, no navegador ou em logs. ## 2. Primeira chamada Consulta de chapas em estoque do material "branco": ```bash curl -X POST "https://www.server1.gransoftweb.com/API/ListaEstoqueChapas" \ -H "Content-Type: application/json" \ -d '{ "userHash": "SEU_USERHASH", "Search": { "Keyword": "branco", "Status": 0, "Reserva": 0, "MaxResult": 50 } }' ``` Resposta (resumida, valores fictícios): ```json { "Erro": "", "Chapas": [ { "Codigo": 3512, "Material": { "Descricao": "GRANITO BRANCO ITAÚNAS POLIDO" }, "Dimensoes": { "ComprLiq": 3.10, "AltLiq": 1.85, "QtdLiqM2": 5.735 }, "txtStatusChapa": "Em estoque", "Reservado": false } ] } ``` A mesma consulta com o kit em Node.js: ```js import { GransoftClient } from "./gransoft-client.js"; const gs = new GransoftClient({ userHash: process.env.GRANSOFT_USERHASH, servidor: process.env.GRANSOFT_SERVIDOR, // ex.: https://www.server1.gransoftweb.com }); const r = await gs.chamar("ListaEstoqueChapas", { Search: { Keyword: "branco", Status: 0, Reserva: 0, MaxResult: 50 }, }); console.log(r.Chapas.length, "chapas"); ``` ## 3. Regras que valem para todos os métodos | Regra | Detalhe | |---|---| | Método HTTP | `POST {Servidor}/API/{Metodo}` com corpo JSON (`Content-Type: application/json`). | | `userHash` | **No corpo JSON**, no campo `userHash`. Exceção: `ObterLista` lê o `userHash` da query string. Não use o header `Authorization`: os métodos de vendas o ignoram. | | Erros | A API responde **HTTP 200 mesmo quando falha**. Sempre verifique o campo `Erro` antes de usar os dados. | | Limite | **50 requisições por minuto** por usuário. Acima disso, `Erro` volta com "Limite de requisições excedido". | | Datas | Vêm como texto `"/Date(1731283200000)/"` (milissegundos desde 1970). Converta antes de usar. | | Enums | Vêm e vão como **número** (ex.: `"Acao": 1`, `"Status": 0`). | | Servidor | Leia o `Servidor` da autenticação. Não fixe o endereço no código: ele muda de uma empresa para outra. | Detalhes em [Convenções](convencoes.md). ## 4. Próximos passos - [Consultar estoque de chapas](guias/consultar-estoque-chapas.md) - [Consultar clientes](guias/consultar-clientes.md) - [Criar orçamento](guias/criar-orcamento.md) - [Kit em Node.js](kit/README.md) - [Referência: Swagger e catálogo de métodos](referencia.md) --- # Autenticação: obter o userHash e o Servidor Toda chamada da API do GransoftWeb é feita em nome de um usuário. A integração precisa de dois dados: - **`userHash`**: a chave que identifica o usuário autenticado. Vai em todas as chamadas. - **`Servidor`**: o endereço do servidor que atende a empresa desse usuário. É o início de todos os endereços da API. Os exemplos usam valores fictícios. ## 1. Duas formas de obter o userHash | Forma | Quando usar | |---|---| | **Chave de teste do catálogo de APIs** | Desenvolvimento e integrações internas de uma única empresa. Entre no GransoftWeb com o usuário da integração, abra `https://www.server1.gransoftweb.com/API/` e copie a chave exibida em "Sua chave de teste". A "URL base" da mesma página indica o `Servidor`. | | **Método `ObterAutenticacao`** | Aplicações em que cada pessoa entra com o próprio e-mail e senha do GransoftWeb. | Recomendação: crie no GransoftWeb um **usuário exclusivo para a integração**, com apenas as permissões de que ela precisa. Assim, o acesso pode ser revisto ou bloqueado sem afetar as pessoas da empresa. ## 2. ObterAutenticacao ### 2.1 Requisição ``` POST https://www.gransoftweb.com/API/ObterAutenticacao Content-Type: application/json ``` ```json { "Email": "joao@empresa.com.br", "Senha": "senha-do-usuario" } ``` | Campo | Obrigatório | Descrição | |---|---|---| | `Email` | Sim | E-mail do usuário no GransoftWeb. | | `Senha` | Sim | Senha do usuário. | | `Login` | Só quando o e-mail pertence a mais de um usuário | Nome de login do usuário (veja 4.3). | O endereço da autenticação é sempre `https://www.gransoftweb.com`. As demais chamadas usam o `Servidor` devolvido. ### 2.2 Resposta quando dá certo ```json { "Erro": "", "Hash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Servidor": "https://www.server1.gransoftweb.com", "Codigo": 1001, "CodigoUsuario": 7, "EmpresaID": 123456, "Nome": "João da Silva", "NomeEmpresa": "Marmoraria Exemplo", "Situacao": 1, "Bloqueado": false } ``` | Campo | Descrição | |---|---| | `Erro` | Vazio quando deu certo. | | `Hash` | O `userHash`. | | `Servidor` | Endereço base do servidor da empresa. | | `EmpresaID` | Código da empresa do usuário. | | `Situacao` | `0` usuário em teste, `1` ativo, `2` bloqueado, `3` mais de um usuário com o mesmo e-mail. | | `Bloqueado` | `true` quando o usuário está bloqueado. | A resposta traz outros campos, que a integração não precisa usar. **Guarde apenas `Hash`, `Servidor` e os dados de exibição.** ### 2.3 Como conferir a resposta A API responde HTTP 200 mesmo quando a autenticação falha. Confira nesta ordem: 1. **Status 200 e corpo JSON?** Se não, o serviço está indisponível (4.5). 2. **`Erro` preenchido?** A autenticação falhou (4.1 ou 4.5). 3. **`Situacao` igual a `3`?** Repita enviando o `Login` (4.3). 4. **`Hash` vazio?** A autenticação não aconteceu (4.2). 5. **`Bloqueado` é `true` ou `Situacao` é `2`?** Usuário bloqueado (4.4). Passou pelos cinco pontos: use `Hash` e `Servidor`. `Servidor` pode vir preenchido mesmo com erro. Só use-o se houver `Hash`. ## 3. Como enviar o userHash nas chamadas seguintes ``` POST {Servidor}/API/{Metodo} Content-Type: application/json { "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", ...demais campos do método } ``` - **Envie o `userHash` no corpo JSON.** Os métodos de vendas (`AcoesDocumentoVendaMarmoraria`) só leem o `userHash` do corpo e ignoram o header `Authorization`. Para não ter dois padrões, use o corpo em todos. - **Exceção: `ObterLista`** lê o `userHash` da query string: `POST {Servidor}/API/ObterLista?userHash=...`. - O `userHash` não tem prazo de validade documentado. Se uma chamada passar a recusar um `userHash` que funcionava (senha trocada, usuário bloqueado), autentique de novo. ## 4. Erros e como resolver ### 4.1 Dados recusados ```json { "Erro": "Email Não Cadastrado Na Base de Dados", "Hash": "", "Situacao": 0 } ``` E-mail, login ou senha não conferem. Mostre o texto de `Erro` ao usuário e **não repita automaticamente**: o resultado será o mesmo. O texto pode mudar; não o compare com textos fixos no código. ### 4.2 Sem erro e sem userHash ```json { "Erro": "", "Hash": "", "Situacao": 0 } ``` Trate como login ou senha inválidos (4.1). ### 4.3 Mais de um usuário com o mesmo e-mail Resposta com `Situacao` igual a `3`. **Não use o `Hash` dessa resposta**, mesmo que venha preenchido. Peça ao usuário o nome de login e repita: ```json { "Email": "joao@empresa.com.br", "Login": "joao.silva", "Senha": "senha-do-usuario" } ``` ### 4.4 Usuário bloqueado `Bloqueado` igual a `true` ou `Situacao` igual a `2`. Não use o `Hash`. O desbloqueio é feito no cadastro do usuário no GransoftWeb. ### 4.5 Serviço indisponível - Status diferente de 200 ou corpo que não é JSON (normalmente uma página HTML de erro); - `Erro` com mensagem técnica em inglês, como `The remote server returned an error: (500) Internal Server Error.`; - `Erro` começando com `Não foi possível obter os dados do Servidor` ou `Erro ao Deserializar Objeto`. Repita depois de alguns segundos, aumentando o intervalo a cada tentativa. Mostre ao usuário uma mensagem genérica. Qualquer outra mensagem em `Erro` é recusa dos dados (4.1). ## 5. Cuidados - Use sempre **POST com HTTPS e corpo JSON**. Não envie e-mail e senha na URL. - **Chame a API a partir do seu servidor**, nunca de código que roda no navegador: o `userHash` ficaria exposto. - Guarde o `userHash` em variável de ambiente ou cofre de segredos, **nunca no código-fonte nem no repositório**. - **Não grave a resposta inteira da autenticação em log.** Ela contém dados do usuário. - Mascare o `userHash` em qualquer log (ex.: `aEJ6…5U`). ## 6. Com o kit em Node.js ```js import { GransoftClient } from "./gransoft-client.js"; // Opção 1: userHash já obtido (chave de teste) const gs = new GransoftClient({ userHash: process.env.GRANSOFT_USERHASH, servidor: process.env.GRANSOFT_SERVIDOR, }); // Opção 2: autenticar com e-mail e senha const gs2 = await GransoftClient.autenticar({ email: process.env.GRANSOFT_EMAIL, senha: process.env.GRANSOFT_SENHA, login: process.env.GRANSOFT_LOGIN, // só se o e-mail tiver mais de um usuário }); ``` `GransoftClient.autenticar` já faz as conferências do item 2.3 e lança `GransoftErro` com `tipo` igual a `"autenticacao"`, `"login_necessario"`, `"bloqueado"` ou `"indisponivel"`. --- # Convenções da API do GransoftWeb Regras que valem para todos os métodos. Código que ignora estes pontos costuma funcionar no primeiro teste e falhar depois. ## 1. Requisição | Item | Regra | |---|---| | Endereço | `{Servidor}/API/{Metodo}`. O `Servidor` vem da autenticação. | | Método HTTP | `POST`. | | Corpo | JSON, com `Content-Type: application/json`. | | `userHash` | No corpo, campo `userHash`. Em `ObterLista`, na query string. | | Campos omitidos | Podem ser omitidos. O servidor usa o valor vazio do tipo (`0`, `""`, `false`). | | Nomes de campos | Exatamente como na documentação, com maiúsculas e minúsculas (`Search`, `Keyword`, `MaxResult`). | Alguns métodos antigos também aceitam formulário (`application/x-www-form-urlencoded`) ou query string. O catálogo de APIs indica o formato de cada um. Para métodos novos na sua integração, prefira JSON. ## 2. Erros: HTTP 200 com o campo Erro **A API responde HTTP 200 mesmo quando a operação falha.** Confira cada resposta nesta ordem: 1. **Status diferente de 200 ou corpo que não é JSON:** serviço indisponível. Repita depois, com intervalo crescente. 2. **`Erro` preenchido:** a operação foi recusada. Mostre o texto ao usuário. Não use os demais dados, mesmo que venham preenchidos. 3. **`Erro` vazio:** sucesso. ```js const resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body }); const texto = await resp.text(); let dados; try { dados = JSON.parse(texto); } catch { throw new Error("Serviço indisponível"); } const bloco = Array.isArray(dados) ? dados[0] : dados; // ObterLista devolve uma lista if (bloco && bloco.Erro) throw new Error(bloco.Erro); ``` O texto de `Erro` pode mudar. Use-o para exibir e registrar, **não o compare com textos fixos no código**. Exceções documentadas: "Limite de requisições" (item 4) e o aviso de medidas na criação de item de produção ([Criar orçamento](guias/criar-orcamento.md)). Alguns métodos antigos devolvem a mensagem de erro em outro campo. Por exemplo, `VisualizarAgendas` usa o campo `NomeCliente` do primeiro item. O Swagger indica esses casos. ## 3. Datas Datas nas respostas vêm como texto no formato: ``` "/Date(1731283200000)/" ``` O número é a quantidade de milissegundos desde 01/01/1970 (UTC). Conversão em JavaScript: ```js function lerData(v) { const m = /^\/Date\((-?\d+)([+-]\d{4})?\)\/$/.exec(v ?? ""); return m ? new Date(Number(m[1])) : null; } ``` O kit em Node.js tem `lerData` e `converterDatas` (que converte todas as datas de um objeto). **Para enviar datas**, siga o formato indicado em cada método. Nos métodos de agenda, envie data e hora locais em ISO **sem fuso horário**: `"2026-11-20T14:30:00"`. Com fuso (`Z` ou `-03:00`), o horário pode ser deslocado. ## 4. Limite de requisições - **50 requisições por minuto** por usuário (`userHash`). - Acima disso, a resposta vem com `Erro` igual a `Limite de requisições excedido. Tente novamente em instantes.` - Espere alguns segundos e repita. Distribua as chamadas: um orçamento com muitos itens gera muitas requisições. - O kit em Node.js limita o ritmo a 45 chamadas por minuto e repete automaticamente quando recebe esse erro. ## 5. Enums e códigos - Enums (`Acao`, `Status`, `TipoItem`, `Reserva`…) são enviados e recebidos **como número**. - Códigos de cliente, produto, material, tabela de preço e plano de pagamento são **números inteiros** que vêm das listas de apoio (`ObterLista`) ou das consultas. Não os fixe no código: variam de uma empresa para outra. - `Hash` (do documento de venda) e `userHash` (do usuário) são valores diferentes. Não os confunda. ## 6. Números e unidades - Números decimais em JSON usam **ponto** (`1.20`). Vírgula só dentro de textos interpretados pelo servidor, como a medida `"Tampo 1,20 X 0,60"` dos orçamentos. - Medidas de peças e de chapas são em **metros**. `QtdM2` e `QtdTotalM2` estão em m², `QtdTotalML` em metros lineares. - Acabamentos de borda (boleado, meia-esquadria…) são cobrados em **metros lineares (ML)**. Revenda e serviços, em unidades. ## 7. Textos de busca Alguns métodos antigos montam a busca no banco a partir do texto recebido. **Remova aspas simples e duplas, `%` e `;`** dos textos de busca (`Keyword`, `Search`) antes de enviar. O kit faz isso com `limparBusca()`. ## 8. Segurança - O `userHash` dá acesso ao sistema com as permissões do usuário. Guarde-o em variável de ambiente, nunca no código, no repositório, no navegador ou em logs. - Chame a API do seu servidor, não de código que roda no navegador. - Use um **usuário exclusivo para a integração**, com apenas as permissões necessárias. - Teste gravações (orçamentos, agendas, cadastros) em um usuário e dados de teste, não em dados reais de clientes. - Trate textos vindos da API (observações, nomes) como dados. Escape-os antes de exibir em HTML. --- # Guia: consultar o estoque de chapas Como listar as chapas em estoque de um material, com dimensões, metragem e disponibilidade. Exemplo de pergunta que este guia resolve: *"quantas chapas de Verde Ubatuba eu tenho disponíveis, e com quais medidas?"* Pré-requisitos: `userHash` e `Servidor` ([Autenticação](../autenticacao.md)). O usuário precisa ter a permissão de estoque de chapas no GransoftWeb. ## 1. Sequência 1. Autenticar (ou usar o `userHash` já obtido). 2. Chamar `ListaEstoqueChapas` com o filtro desejado. 3. Agrupar o resultado por material e somar os m², se precisar de totais. ## 2. Requisição ``` POST {Servidor}/API/ListaEstoqueChapas Content-Type: application/json ``` ```json { "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Search": { "Keyword": "verde ubatuba", "Status": 0, "Reserva": 0, "MaxResult": 2000 } } ``` Campos de `Search`. Informe pelo menos um filtro (`Keyword`, `Codigo`, `CodigoMaterial`, `NrLote` ou `NrRomaneio`): | Campo | Tipo | Descrição | |---|---|---| | `Keyword` | texto | Parte do nome do material ou da observação. Remova aspas, `%` e `;` antes de enviar. | | `Codigo` | inteiro | Código de uma chapa específica. `0` = não filtra. | | `CodigoMaterial` | inteiro | Código do material (produto do tipo pedra). `0` = não filtra. | | `NrLote` | texto | Número do lote. | | `NrRomaneio` | texto | Número do romaneio de entrada. | | `Status` | inteiro | `0` em estoque, `1` vendidas. | | `Reserva` | inteiro | `0` sem reserva (**disponíveis**), `1` só reservadas, `2` todas. | | `Filial` | inteiro | Código da filial. `0` = filial do usuário. | | `MaxResult` | inteiro | Máximo de chapas devolvidas. Para totais exatos, use um valor alto (ex.: `2000`). | "Disponível" = `Status: 0` e `Reserva: 0`: em estoque e sem reserva para algum cliente. ## 3. Resposta Valores fictícios: ```json { "Erro": "", "Chapas": [ { "Codigo": 3512, "Material": { "Descricao": "GRANITO VERDE UBATUBA POLIDO" }, "txtMedidas": "3,100 X 1,850", "Dimensoes": { "ComprLiq": 3.10, "AltLiq": 1.85, "QtdLiqM2": 5.735, "CompBruto": 3.15, "AltBruto": 1.90, "QtdBrutM2": 5.985, "xMedidas": "3,100 X 1,850" }, "txtStatusChapa": "Em estoque", "Reservado": false, "ReservadoM2": 0, "Localizacao": { "txtNroLote": "L-0425", "Local1": "Pátio A", "Local2": "Cavalete 3", "Local3": "" }, "InfoFilial": { "Descricao": "MATRIZ" }, "Observacao": "" } ] } ``` | Campo | Descrição | |---|---| | `Codigo` | Código da chapa. | | `Material.Descricao` | Nome do material. | | `Dimensoes.ComprLiq`, `Dimensoes.AltLiq` | Comprimento e altura líquidos (aproveitáveis), em **metros**. | | `Dimensoes.QtdLiqM2` | Área líquida em m². Use este valor para somar a metragem disponível. | | `Dimensoes.CompBruto`, `AltBruto`, `QtdBrutM2` | Medidas brutas (antes dos descontos de borda). | | `txtMedidas` / `Dimensoes.xMedidas` | Medidas já formatadas para exibição. | | `txtStatusChapa` | Situação da chapa, em texto. | | `Reservado`, `ReservadoM2` | Se há reserva e quantos m² estão reservados. | | `Localizacao` | Lote (`txtNroLote`) e locais no pátio (`Local1` a `Local3`). | | `InfoFilial.Descricao` | Filial onde a chapa está. | O objeto da chapa tem mais campos (compra, venda, custo). O custo só deve ser exibido a quem tem permissão para vê-lo no sistema. ## 4. Totais por material Uma busca por texto ("branco") pode trazer vários materiais (polido, escovado, levigado). Agrupe antes de responder: ```js const r = await gs.chamar("ListaEstoqueChapas", { Search: { Keyword: limparBusca("verde ubatuba"), Status: 0, Reserva: 0, MaxResult: 2000 }, }); const porMaterial = {}; for (const c of r.Chapas ?? []) { const nome = c.Material?.Descricao ?? "(sem material)"; porMaterial[nome] ??= { chapas: 0, m2: 0 }; porMaterial[nome].chapas++; porMaterial[nome].m2 += c.Dimensoes?.QtdLiqM2 ?? 0; } console.table(porMaterial); ``` Exemplo completo: [kit/exemplo-estoque.js](../kit/exemplo-estoque.js). ## 5. Falhas comuns | Sintoma | Causa | Solução | |---|---|---| | `Erro` de permissão | O usuário não tem acesso ao estoque de chapas. | Ajuste as permissões do usuário no GransoftWeb. | | Lista vazia | Texto muito específico ou material sem chapas disponíveis. | Use só parte do nome ("ubatuba") ou `Reserva: 2`. | | Totais menores que na tela | `MaxResult` baixo cortou a lista. | Aumente `MaxResult` ou filtre por `CodigoMaterial`. | | Erro inesperado com aspas no texto | O texto é usado na busca sem tratamento. | Remova `'`, `"`, `%` e `;` do texto. | ## 6. Métodos relacionados - `ListaItensAlmoxarifado`: saldo de insumos e ferramentas. - `ConsultarPreco`: preço de venda por m² do material na tabela de preços. - Baixa, reserva e alteração de status de chapas existem na API (`BaixarEstoqueChapa`, `AlterarStatusChapas`), mas alteram o estoque real. Consulte o Swagger e teste só com dados de teste. --- # Guia: consultar clientes Como localizar um cliente pelo nome e obter os dados completos dele. É também o primeiro passo para criar um orçamento. Pré-requisitos: `userHash` e `Servidor` ([Autenticação](../autenticacao.md)). ## 1. Sequência 1. Buscar o cliente pelo nome com `ObterLista`, tipo `16`. A resposta traz código e nome. 2. Se vier mais de um cliente parecido, **pergunte ao usuário qual é**. Não escolha o primeiro. 3. Com o código, obter os dados completos (endereço, documento, contatos) com `AcoesDocumentoVendaMarmoraria`, ação `44`. ## 2. Buscar pelo nome: ObterLista, tipo 16 Neste método o `userHash` vai na **query string** e o filtro vai no corpo: ``` POST {Servidor}/API/ObterLista?userHash={userHash} Content-Type: application/json ``` ```json { "Tipo": [16], "Search": "maria souza" } ``` `Search` aceita parte do nome, nome fantasia, razão social, telefone ou CNPJ. Use pelo menos 3 caracteres e remova aspas do texto. Resposta (valores fictícios). É **sempre uma lista**, com um bloco por tipo pedido: ```json [ { "Erro": "", "Tipo": [16], "Dados": [ { "Codigo": 1523, "Descricao": "MARIA DE SOUZA" }, { "Codigo": 2077, "Descricao": "MARIA DE SOUZA LIMA" } ] } ] ``` Verifique `Erro` no primeiro bloco e use `Dados`. `Codigo` é o código do cliente no GransoftWeb. ### Outras listas de apoio do mesmo método | `Tipo` | Lista | Precisa de `Search` | |---|---|---| | `1` | Condições (planos) de pagamento | Não | | `3` | Produtos de revenda | Não | | `5` | Produtos de produção (tipos de peça: pia, bancada…) | Não | | `6` | Serviços | Não | | `7` | Acabamentos (beneficiamento) | Não | | `8` | Tabelas de preço | Não | | `9` | Vendedores | Não | | `11` | Prazos de entrega | Não | | `14` | Filiais | Não | | `16` | Clientes | Sim: texto procurado | | `18` | Materiais (pedras) | Sim: `"1"` | Todas devolvem `Dados` com `Codigo` e `Descricao`. ## 3. Dados completos: ação 44 ``` POST {Servidor}/API/AcoesDocumentoVendaMarmoraria Content-Type: application/json ``` ```json { "Acao": 44, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "DadosCliente": { "Codigo": 1523 } } ``` Resposta (resumida, valores fictícios): ```json { "Erro": "", "DadosCliente": { "Codigo": 1523, "Nome": "MARIA DE SOUZA", "Rua": "RUA DAS ACÁCIAS", "Numero": "120", "EndCompl": "", "Bairro": "CENTRO", "Cidade": "BELO HORIZONTE", "Estado": "MG", "CEP": "30110-000", "CpfCnpj": "12345678909", "InscRG": "", "Celular": "31999990000", "Telefone": "", "Email": "maria@exemplo.com.br" } } ``` ## 4. Com o kit em Node.js ```js const clientes = await gs.obterLista(16, limparBusca("maria souza")); if (clientes.length === 0) console.log("Nenhum cliente encontrado."); const { DadosCliente } = await gs.venda(44, { DadosCliente: { Codigo: clientes[0].Codigo } }); console.log(DadosCliente.Nome, DadosCliente.Cidade); ``` Exemplo completo: [kit/exemplo-clientes.js](../kit/exemplo-clientes.js). ## 5. Falhas comuns | Sintoma | Causa | Solução | |---|---|---| | `Erro` em todas as chamadas de `ObterLista` | `userHash` enviado no corpo. | Neste método, envie-o na query string. | | Lista vazia | Texto não encontrado. | Use só parte do nome, ou o telefone ou CNPJ. | | Cliente errado | Homônimos. | Mostre a lista ao usuário e peça para escolher. | | Resposta tratada como objeto | `ObterLista` devolve uma lista de blocos. | Leia `resposta[0].Dados`. | ## 6. Gravar dados de cliente - Para incluir ou alterar o cliente **dentro de um orçamento**, veja [Criar orçamento](criar-orcamento.md), item 4. - Para alterar o **cadastro** de clientes, fornecedores e colaboradores, use `EditarPessoa` (catálogo de APIs, grupo Cadastro: `https://www.server1.gransoftweb.com/API/Cadastro`). Teste sempre em dados de teste. --- # Guia: criar um orçamento Como um sistema externo cria um orçamento no GransoftWeb: abrir o documento, informar cliente e obra, condições da venda, incluir peças sob medida, produtos de revenda e serviços, aplicar desconto e gerar o PDF. Pré-requisitos: `userHash` e `Servidor` ([Autenticação](../autenticacao.md)), usuário com permissão de criar orçamentos. **Teste com um usuário e clientes de teste**: cada chamada grava no banco na hora. Não existe um "salvar" final. Os exemplos usam valores fictícios. ## 1. Visão geral Todas as operações usam o mesmo endereço. O campo `Acao` diz o que fazer: ``` POST {Servidor}/API/AcoesDocumentoVendaMarmoraria Content-Type: application/json { "Acao": 1, "userHash": "...", "Hash": "..." } ``` | Campo | Descrição | |---|---| | `Acao` | Número da operação (tabela no item 11). | | `userHash` | Usuário autenticado. **Sempre no corpo**: este método ignora o header `Authorization`. | | `Hash` | Identificador do **orçamento**. Vem na criação e vai em todas as chamadas seguintes. É diferente do `userHash`. | Respostas: HTTP 200 sempre. Confira `Erro` antes de usar os dados ([Convenções](../convencoes.md)). ### Sequência | Passo | O que fazer | Ação | Item | |---|---|---|---| | 1 | Localizar o cliente | `ObterLista` tipo 16 | 2 | | 2 | Criar o orçamento e guardar o `Hash` | `2` | 3 | | 3 | Completar cliente e obra (opcional) | `42`, `43` | 4 | | 4 | Tabela de preço, pagamento, prazo, observação (opcional) | `3` | 5 | | 5 | Incluir os itens | `19`, `39`, `40`, `20`, `21` | 6 e 7 | | 6 | Desconto, frete e outros valores (opcional) | `50`, `51`, `3` | 8 | | 7 | Conferir e gerar o PDF | `1`, `25`, `7` | 9 | | 8 | Encerrar a edição | `37` | 10 | O mínimo é: criar o orçamento (passo 2) e incluir os itens (passo 5). **Ritmo:** cada item, medida e serviço é uma chamada, e o limite é de 50 por minuto. Use as medidas em lote (item 6.2) para economizar chamadas. ## 2. Localizar o cliente e os códigos Use `ObterLista` ([Consultar clientes](consultar-clientes.md)): | Para | `Tipo` | |---|---| | Cliente (com `Search` = texto) | `16` | | Produto de produção: tipo de peça (pia, bancada, soleira…) | `5` | | Material (pedra), com `Search` = `"1"` | `18` | | Produto de revenda (cuba, torneira…) | `3` | | Serviço | `6` | | Acabamento (boleado, meia-esquadria…) | `7` | | Tabela de preço / condição de pagamento / prazo de entrega | `8` / `1` / `11` | Se houver mais de um registro compatível, pergunte ao usuário qual usar. ## 3. Criar o orçamento: ação 2 Cliente já cadastrado (código vindo do item 2): ```json { "Acao": 2, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "DadosCliente": { "Codigo": 1523, "Nome": "MARIA DE SOUZA" } } ``` Cliente novo: envie `"Codigo": 0` e o nome. Os demais dados vão no item 4. Resposta: ```json { "Acao": 2, "Erro": "", "Hash": "bVc5a1RqUnhWMGx3...", "Tipo": 0 } ``` **Guarde o `Hash`.** Para ver número e totais, use a ação `1` (item 9). `Tipo` do documento: `0` orçamento (pode ser alterado), `1` venda finalizada/pedido (não pode ser alterado pela API). ## 4. Cliente e obra: ações 42 e 43 > **Atenção: estas ações substituem todos os campos de endereço.** Campo não enviado fica vazio. Para mudar só o telefone, consulte o documento (ação `1`), altere o campo no objeto `DadosCliente` recebido e envie o objeto inteiro. ```json { "Acao": 42, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Hash": "bVc5a1RqUnhWMGx3...", "DadosCliente": { "Codigo": 0, "Nome": "Carlos Pereira", "Rua": "Av. Brasil", "Numero": "850", "EndCompl": "Apto 302", "Bairro": "Funcionários", "Cidade": "Belo Horizonte", "Estado": "MG", "CEP": "30140-001", "CpfCnpj": "98765432100", "InscRG": "", "Celular": "31988887777", "Telefone": "", "Email": "carlos@exemplo.com.br" } } ``` Obra: ação `43` com `DadosObra` (`Nome`, `Rua`, `Numero`, `EndCompl`, `Bairro`, `Cidade`, `Estado`, `CEP`, `Contato`). Sem endereço de obra, o sistema usa o do cliente. Estas ações gravam **só no orçamento**. Para gravar o cliente novo no cadastro, use a ação `56` (`SalvarCadastroCliente`) com o mesmo `DadosCliente`, apenas quando `CpfCnpj` estiver preenchido. `Estado` é a sigla da UF. `CpfCnpj` só números. ## 5. Condições da venda: ação 3 ```json { "Acao": 3, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Hash": "bVc5a1RqUnhWMGx3...", "FieldsToUpdate": "Tabela,CondPagamento,PrazoEntrega,txtReferencia,Observacao", "Tabela": { "Codigo": 1 }, "CondPagamento": { "Codigo": 3 }, "PrazoEntrega": { "Codigo": 2 }, "txtReferencia": "Cozinha e banheiros", "Observacao": "Medição final a confirmar na obra." } ``` > **Sempre envie `FieldsToUpdate`**, com os nomes separados por vírgula. Se ele vier vazio, o servidor grava **todos** os campos do documento, incluindo os que você não enviou. O vendedor é o do usuário autenticado e volta na consulta (ação `1`). ## 6. Peça sob medida (item de produção) Uma peça sob medida (pia, bancada, lavatório, soleira) é um item com: - um **produto de produção** (lista `5`): o tipo da peça; - um **material** (lista `18`): a pedra; - **medidas**: as partes da peça (tampo, frontão, saia…), com comprimento e largura em metros; - **serviços e acabamentos** opcionais (item 7). ### 6.1 Criar o item: ação 19 ```json { "Acao": 19, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Hash": "bVc5a1RqUnhWMGx3...", "Itens": [ { "CodProduto": 55, "TipoItem": 0, "Qtd": 1, "Medidas": [ { "Medida": "", "Material": { "CodMaterial": 120 } } ] } ] } ``` - `TipoItem: 0` faz o servidor deduzir o tipo pelo cadastro do produto. É o recomendado. - Deixe `Medida` vazia aqui: as medidas entram no passo seguinte. - A resposta traz o item em `Itens[0]`, com o `Codigo`. **Guarde-o.** - Se `Erro` vier pedindo as medidas **e** `Itens[0].Codigo` estiver preenchido, o item foi criado: siga para 6.2. Qualquer outro erro interrompe. **Peças-modelo vazias.** Ao escolher a pedra, o sistema pode criar sozinho partes-modelo do produto (ex.: Tampo, Saia, Frontão) com área zero. Elas vêm em `Itens[0].Medidas` na resposta. **Anote os códigos delas** e remova as que continuarem com `QtdTotalM2` igual a `0` depois de lançar as medidas reais (6.3). Peças vazias impedem a geração do pedido. ### 6.2 Incluir as medidas: ação 39 **Opção A, campos separados (recomendada).** Uma chamada por parte. O servidor não interpreta texto, então não há risco de leitura errada: ```json { "Acao": 39, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Hash": "bVc5a1RqUnhWMGx3...", "Itens": [ { "Codigo": 9002, "Medidas": [ { "Medida": "", "Descricao": "Tampo", "Qtd": 1, "Comp": 1.20, "Larg": 0.60, "CompCompl": 0, "LargCompl": 0, "Material": { "CodMaterial": 120 } } ] } ] } ``` **Opção B, texto com várias partes de uma vez.** Economiza chamadas. Medidas em metros com vírgula decimal, partes separadas por `;`, quantidade opcional na frente: ```json { "Acao": 39, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Hash": "bVc5a1RqUnhWMGx3...", "Itens": [ { "Codigo": 9002, "Medidas": [ { "Medida": "Tampo 1,20 x 0,60; Frontão 1,20 x 0,10; Saia 1,20 x 0,05; 2 x 0,90 x 0,40 Lateral", "Material": { "CodMaterial": 120 } } ] } ] } ``` Resposta: o item com as medidas calculadas. ```json { "Erro": "", "Itens": [ { "Codigo": 9002, "QtdM2": 0.912, "vrTotal": 820.80, "Medidas": [ { "Codigo": 7001, "Descricao": "Tampo", "Qtd": 1, "Comp": 1.2, "Larg": 0.6, "QtdTotalM2": 0.72 }, { "Codigo": 7002, "Descricao": "Frontão", "Qtd": 1, "Comp": 1.2, "Larg": 0.1, "QtdTotalM2": 0.12 } ] } ] } ``` **Confira `Comp`, `Larg` e `Qtd` de cada medida na resposta.** Medida errada: corrija com a ação `41` (`EditarMedidas`, com o `Codigo` da medida) ou remova com a `40`. Frontão e saia são partes separadas do tampo. Não invente medidas que o usuário não informou. ### 6.3 Remover as peças-modelo vazias: ação 40 Para cada peça-modelo anotada em 6.1 que ainda tenha `QtdTotalM2` igual a `0`: ```json { "Acao": 40, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Hash": "bVc5a1RqUnhWMGx3...", "Itens": [ { "Codigo": 9002, "Medidas": [ { "Codigo": 7000 } ] } ] } ``` ## 7. Acabamentos, serviços e revenda ### 7.1 Na peça: ação 20 Acabamento, serviço ou produto de revenda **ligado a uma peça** (ex.: boleado na pia, cuba na bancada): ```json { "Acao": 20, "userHash": "aEJ6cG1oUjllOExOMXB2ay91TE5U...", "Hash": "bVc5a1RqUnhWMGx3...", "Itens": [ { "Codigo": 9002, "CodProduto": 77, "Qtd": 2.4 } ] } ``` - `Codigo` é o item de produção. `CodProduto` é o acabamento, serviço ou produto. - **Acabamentos de borda são em metros lineares** e o sistema não calcula a quantidade: some você as bordas que recebem o acabamento (ex.: frente de 1,20 m + duas laterais de 0,60 m = 2,40 ML). - A resposta traz `Itens[0].Servicos`, cada um com `Codigo`. Para mudar a quantidade depois: ação `22` com `Servicos: [{ "Codigo": 501, "Qtd": 3, "FieldsToUpdate": "Qtd" }]`. Para remover: ação `24`. ### 7.2 Item avulso de revenda ou serviço: ações 19 e 21 ```json { "Acao": 19, "userHash": "...", "Hash": "...", "Itens": [ { "CodProduto": 310, "TipoItem": 0 } ] } ``` Depois, quantidade e preço com a ação `21`. **`FieldsToUpdate` fica dentro do item:** ```json { "Acao": 21, "userHash": "...", "Hash": "...", "Itens": [ { "Codigo": 9001, "Qtd": 2, "FieldsToUpdate": "Qtd" } ] } ``` Se `IsQtdDecimal` vier `false` na criação, envie quantidade inteira. O preço unitário vem da tabela de preço. Só envie `vrUnit` (e inclua em `FieldsToUpdate`) se a regra da empresa permitir alterar preço. ## 8. Desconto, frete e outros valores Informe **depois de incluir todos os itens**: o desconto depende do total. Desconto em percentual, ação `50`: ```json { "Acao": 50, "userHash": "...", "Hash": "...", "Detalhe": { "PercDesconto": 5 } } ``` Desconto em reais, ação `51`: ```json { "Acao": 51, "userHash": "...", "Hash": "...", "Detalhe": { "ValorDesconto": 74.40 } } ``` O desconto passa pelas regras de limite do usuário. Confira em `Detalhe` da resposta o valor realmente aplicado. Frete e outros valores, ação `3`: ```json { "Acao": 3, "userHash": "...", "Hash": "...", "FieldsToUpdate": "ValorFrete,ValorOutros", "Detalhe": { "ValorFrete": 150.00, "ValorOutros": 0 } } ``` Total do documento: `ValorTotalItens + ValorOutros + ValorFrete − ValorDesconto`. ## 9. Conferir e gerar o PDF - **Ação `1`** (`ConsultarDocumento`): número (`NroDocumento`), cliente, obra, vendedor, tabela, condição de pagamento e `Detalhe` com os totais. - **Ação `25`** (`ListarItens`): lista dos itens em `Itens`. - **Ação `7`** (`VisualizarRelatorio`): gera o PDF. O link vem em `Anexos[0].Link`. Um segundo anexo, quando existe, é o relatório com os desenhos. Confira pelo menos: cliente, quantidade de itens, `Detalhe.ValorTotalItens` e `Detalhe.ValorTotalDocumento`. ## 10. Encerrar a edição: ação 37 ```json { "Acao": 37, "userHash": "...", "Hash": "..." } ``` Libera o documento no servidor. Chame ao terminar ou antes de abrir outro orçamento. ## 11. Referência das ações Todas: `POST {Servidor}/API/AcoesDocumentoVendaMarmoraria`, com `Acao` e `userHash`. Exceto `2`, `31` a `33`, `44`, `45` e `46`, todas exigem o `Hash` do documento. | `Acao` | Nome | Envia além de `Acao`/`userHash`/`Hash` | |---|---|---| | 1 | ConsultarDocumento | — | | 2 | CriarDocumento | `DadosCliente` (sem `Hash`) | | 3 | SalvarDocumento | `FieldsToUpdate` e os campos listados | | 4 | ExcluirDocumento | — (só orçamento; exige permissão de exclusão) | | 5 | RevisaoDocumento | — | | 6 | CopiarDocumento | — (novo orçamento a partir de outro documento) | | 7 | VisualizarRelatorio | — (PDF em `Anexos`) | | 10 | CarregarImagem | `Anexos: [{ Nome, Array }]` (bytes do arquivo); com `Itens[0].Codigo` + `ArquivoProjeto` = desenho do item | | 11 | ExcluirImagem | `Anexos: [{ Nome }]` | | 13 | ObterAnexos | — | | 19 | CriarItem | `Itens[0]`: `CodProduto`, `TipoItem`, `Medidas[0].Material.CodMaterial` | | 20 | CriarServico | `Itens[0]`: `Codigo` (item), `CodProduto`, `Qtd` | | 21 | SalvarItem | `Itens[0]`: `Codigo`, campos e `FieldsToUpdate` | | 22 | SalvarServico | `Itens[0].Codigo`, `Servicos[0]`: `Codigo`, `Qtd`, `FieldsToUpdate` | | 23 | ExcluirItem | `Itens[0].Codigo` | | 24 | ExcluirServico | `Itens[0].Codigo`, `Servicos[0].Codigo` | | 25 | ListarItens | — | | 31 / 32 / 33 | LocalizarOrcamento / LocalizarPedido / LocalizarOrcamentoPedido | `Localiza` (número ou nome) — sem `Hash` | | 37 | RemoveCache | — | | 38 | DetalheItemVenda | `Itens[0].Codigo` | | 39 | AdicionarMedidas | `Itens[0].Codigo`, `Medidas[0]` | | 40 | ExcluirMedidas | `Itens[0].Codigo`, `Medidas[0].Codigo` | | 41 | EditarMedidas | `Itens[0].Codigo`, `Medidas[0]` com `Codigo` | | 42 | AlteraDadosCliente | `DadosCliente` completo | | 43 | AlteraDadosObra | `DadosObra` completo | | 44 | ObterDadosCliente | `DadosCliente.Codigo` — sem `Hash` | | 45 / 46 | UltimosOrcamentos / UltimosPedidos | — sem `Hash` | | 50 / 51 | AlterarPercDesconto / AlterarValorDesconto | `Detalhe.PercDesconto` / `Detalhe.ValorDesconto` | | 53 | AlterarMaterial | `Itens[0].Codigo`, `Medidas[0].Material.CodMaterial` | | 54 | CopiarItem | `Itens[0].Codigo` | | 56 | SalvarCadastroCliente | `DadosCliente` | | 57 | ExportarXml | — (XML do documento, como o "Exportar para arquivo") | | 58 | AnaliseRentabilidade | — (exige permissão de ver custo) | As listagens (`31` a `33`, `45`, `46`) devolvem `Documentos`, cada um com `Hash`, `Tipo`, `NroDocumento`, `ClienteNome` e `ValorTotalDocumento`. Use o `Hash` para abrir o documento com a ação `1`. ## 12. Erros comuns | Sintoma | Causa | Solução | |---|---|---| | `Erro` em todas as chamadas | `userHash` no header, ou `Hash` do orçamento trocado pelo `userHash`. | `userHash` no corpo; `Hash` = o devolvido na criação. | | Erro ao alterar documento com `Tipo` igual a `1` | Pedido não pode ser alterado. | Gere um orçamento a partir dele (ação `6`). | | Endereço do cliente apagado | Ação `42`/`43` com objeto incompleto. | Envie o objeto inteiro (item 4). | | Campos do documento alterados sem querer | Ação `3` sem `FieldsToUpdate`. | Sempre envie `FieldsToUpdate`. | | Pedido não é gerado na tela | Peças-modelo com área zero. | Remova-as (item 6.3). | | Medida com dimensões erradas | Texto interpretado de outra forma. | Use a opção A (campos separados) e confira a resposta. | | "Limite de requisições excedido" | Mais de 50 chamadas por minuto. | Espere e repita; use medidas em lote. | Exemplo completo: [kit/exemplo-orcamento.js](../kit/exemplo-orcamento.js). --- # Referência: onde achar cada método A API do GransoftWeb tem três fontes de referência, que se complementam. | Fonte | Endereço | Para quê | |---|---|---| | **Guias** (este portal) | https://www.gransoftweb.com/developers | Tarefas completas passo a passo: autenticação, estoque, clientes, orçamento. Comece por aqui. | | **Swagger** | http://www.server1.gepersistemas.com.br:8082/swagger/ui/index | Contrato dos métodos (Swagger 2.0): rotas, parâmetros e descrição. O JSON fica em `/swagger/docs/v1`. | | **Catálogo de APIs** | https://www.server1.gransoftweb.com/API/ | Métodos por grupo (Cadastro, Vendas, Produção, Estoque, Financeiro, Fiscal, CRM, Agenda…), com testador e a sua chave de teste. | Quando o Swagger e um guia divergirem, siga o guia e avise o suporte da Gransoft. Os guias foram conferidos no código do sistema. ## Métodos mais usados | Tarefa | Método | Guia | |---|---|---| | Autenticar com e-mail e senha | `ObterAutenticacao` (em `https://www.gransoftweb.com`) | [Autenticação](autenticacao.md) | | Listas de apoio (clientes, produtos, tabelas, pagamento…) | `ObterLista` | [Consultar clientes](guias/consultar-clientes.md) | | Estoque de chapas | `ListaEstoqueChapas` | [Consultar estoque de chapas](guias/consultar-estoque-chapas.md) | | Saldo de almoxarifado | `ListaItensAlmoxarifado` | Swagger | | Preço de venda na tabela | `ConsultarPreco` | Swagger | | Orçamentos e pedidos (consultar, criar, alterar) | `AcoesDocumentoVendaMarmoraria` | [Criar orçamento](guias/criar-orcamento.md) | | Agenda de visitas (consultar, criar, alterar) | `AcoesAgendaUsuario` | Swagger | | Ordens de serviço | `ListaOrdensServicosMarmoraria`, `DetalheOrdemServicoMarmoraria` | Swagger | | Indicadores do painel | `GetDashboardData` | Swagger | | Consulta de CEP | `ConsultaCEP` | Swagger | | Cadastrar ou atualizar produtos (até 30 por chamada) | `CadastrarProdutos`, `AtualizarCadastroProdutos` | Catálogo de APIs, grupo Cadastro | ## Fora do escopo das integrações externas Métodos dos grupos **Database**, **Importação** e **Fornecedor externo**, e os métodos marcados como "Administrador" no catálogo, são de uso interno da Gransoft e não são suportados para integrações de terceiros. Emissão de documentos fiscais (NF-e, NFC-e, MDF-e, NFS-e) gera documentos com validade jurídica. Só integre com acompanhamento do suporte da Gransoft. --- # Usar uma IA para criar a sua integração Você não precisa conhecer a API em detalhes para começar. Uma IA (ChatGPT, Claude, Gemini, Copilot, Cursor…) consegue escrever a integração se receber a documentação certa. Este guia mostra como entregar essa documentação a ela. ## 1. O caminho mais simples: o link do índice Comece a conversa com a IA colando este pedido e trocando a última linha pela sua necessidade: ```text Vou integrar meu sistema com a API do GransoftWeb. Antes de escrever código, leia a documentação oficial: https://www.gransoftweb.com/developers/llms.txt Siga os guias e use o kit em Node.js (gransoft-client.js). Não invente métodos nem campos: se algo não estiver na documentação, me pergunte. Quero: um script que liste as chapas disponíveis de um material, com medidas e total em m². ``` Se a IA não conseguir abrir links, use o arquivo com toda a documentação, que você pode baixar e anexar à conversa: https://www.gransoftweb.com/developers/llms-full.txt ## 2. Em um editor com IA (Cursor, VS Code com Copilot, Claude Code…) 1. Baixe o kit ([instruções](kit/README.md)) para a pasta do projeto. 2. Deixe o `AGENTS.md` na raiz do projeto. Ele traz as regras que a IA deve seguir: segredos fora do código, tratamento de erros, limite de requisições. No Claude Code, copie-o também como `CLAUDE.md`. 3. Conecte o MCP de documentação (item 3) para a IA consultar os guias sozinha. 4. Peça a tarefa (exemplos no item 4). ## 3. MCP de documentação O MCP de documentação deixa a IA buscar só o trecho de que precisa, sempre na versão atual. - Endereço: `https://www.gransoftweb.com/developers/mcp` - Transporte: HTTP ("Streamable HTTP"), sem login. - Somente leitura: consulta documentação. **Não acessa dados de nenhuma empresa.** Ferramentas: | Ferramenta | O que faz | |---|---| | `buscar_documentacao` | Procura nos guias e na referência; devolve os trechos mais relevantes com o `id` de cada documento. | | `obter_documento` | Devolve um documento inteiro (ou uma seção), em partes quando é grande. | | `obter_endpoint` | Devolve a definição de um método no Swagger (rota, parâmetros, descrição). | | `listar_endpoints` | Lista os métodos do Swagger, com filtro por texto. | Configuração nos clientes mais comuns: **Claude Code** ```bash claude mcp add --transport http gransoft-docs https://www.gransoftweb.com/developers/mcp ``` **Cursor** (`.cursor/mcp.json` no projeto) ```json { "mcpServers": { "gransoft-docs": { "url": "https://www.gransoftweb.com/developers/mcp" } } } ``` **VS Code** (`.vscode/mcp.json` no projeto) ```json { "servers": { "gransoft-docs": { "type": "http", "url": "https://www.gransoftweb.com/developers/mcp" } } } ``` **Claude (claude.ai) e ChatGPT**: adicione um conector personalizado com o endereço acima, nas configurações de conectores. Não é preciso autenticação. > Este MCP é diferente do **MCP de operações** (`https://www.gransoftweb.com/API/mcp`), que acessa os dados da sua empresa para consultar estoque, criar orçamentos etc. diretamente pelo chat. O de documentação serve para **criar aplicações**; o de operações, para **usar o sistema** pela IA. ## 4. Exemplos de pedidos Pedidos que funcionam bem são específicos sobre **o que** você quer e **onde** o resultado vai aparecer: - *"Crie um script que, todo dia às 7h, gere uma planilha CSV com as chapas disponíveis por material e total em m²."* - *"Crie uma página web simples (servidor em Node.js com Express) onde o vendedor digita o nome do cliente e vê os orçamentos dele. O userHash fica só no servidor."* - *"Crie uma função que receba uma lista de peças (descrição, comprimento e largura em cm) e crie um orçamento para o cliente informado, convertendo as medidas para metros."* - *"Leia minha planilha produtos.xlsx e cadastre os produtos no GransoftWeb em lotes de 30."* ## 5. Boas práticas ao trabalhar com IA - **Use um usuário de teste** no GransoftWeb enquanto a integração estiver sendo construída. Gravações acontecem na hora. - **Nunca cole o seu `userHash` na conversa** com a IA. Diga que ele está na variável `GRANSOFT_USERHASH`. - **Peça para a IA explicar** o que o código faz antes de rodar algo que grava dados. - **Confira o resultado no GransoftWeb** (tela de orçamentos, estoque) depois do primeiro teste. - Se a IA disser que um método ou campo existe e você não o encontrar na documentação, peça a ela a fonte. Na dúvida, fale com o suporte da Gransoft. --- # Kit GransoftWeb para Node.js Cliente pronto e exemplos para consumir a API do GransoftWeb. Requer **Node.js 18 ou superior**. Não tem dependências. A IA pode usar este código como base também em outras linguagens: as regras estão comentadas em `gransoft-client.js`. ## Arquivos | Arquivo | O que é | |---|---| | [gransoft-client.js](gransoft-client.js) | Cliente: `userHash`, verificação do `Erro`, limite de requisições, datas. | | [exemplo-estoque.js](exemplo-estoque.js) | Chapas disponíveis de um material, com totais por material. | | [exemplo-clientes.js](exemplo-clientes.js) | Busca de clientes pelo nome e dados completos. | | [exemplo-orcamento.js](exemplo-orcamento.js) | Cria um orçamento com uma peça sob medida (grava; use dados de teste). | | [AGENTS.md](AGENTS.md) | Regras para o agente de IA que vai trabalhar no projeto. | | [package.json](package.json) | Configuração mínima (ES modules). | | [env.example](env.example) | Modelo das variáveis de ambiente. | ## Instalação 1. Crie uma pasta e baixe os arquivos: ```bash mkdir minha-integracao && cd minha-integracao for f in gransoft-client.js exemplo-estoque.js exemplo-clientes.js exemplo-orcamento.js AGENTS.md package.json env.example; do curl -fsSO "https://www.gransoftweb.com/developers/kit/$f" done ``` No Windows (PowerShell): ```powershell mkdir minha-integracao; cd minha-integracao "gransoft-client.js","exemplo-estoque.js","exemplo-clientes.js","exemplo-orcamento.js","AGENTS.md","package.json","env.example" | ForEach-Object { Invoke-WebRequest "https://www.gransoftweb.com/developers/kit/$_" -OutFile $_ } ``` 2. Copie `env.example` para `.env` e preencha: - `GRANSOFT_USERHASH`: a chave de teste do catálogo de APIs (`https://www.server1.gransoftweb.com/API/`, entrando com o usuário da integração); - `GRANSOFT_SERVIDOR`: a "URL base" da mesma página, sem o `/API/` (ex.: `https://www.server1.gransoftweb.com`). 3. Rode um exemplo: ```bash node --env-file=.env exemplo-estoque.js "branco" node --env-file=.env exemplo-clientes.js "maria" node --env-file=.env exemplo-orcamento.js --cliente 1523 --produto 55 --material 120 --pecas "Tampo 1,20 x 0,60" ``` `--env-file` existe a partir do Node.js 20.6. No Node.js 18, defina as variáveis no terminal antes de rodar. O exemplo de orçamento só grava com `--gravar`. **Não versione o `.env`.** Inclua-o no `.gitignore`. ## Uso do cliente ```js import { GransoftClient, GransoftErro, limparBusca, converterDatas } from "./gransoft-client.js"; const gs = new GransoftClient({ userHash: process.env.GRANSOFT_USERHASH, servidor: process.env.GRANSOFT_SERVIDOR, }); try { // Qualquer método: POST {servidor}/API/{metodo}, userHash incluído automaticamente. const r = await gs.chamar("ListaEstoqueChapas", { Search: { Keyword: limparBusca("branco"), MaxResult: 100 } }); // Vendas: atalho para AcoesDocumentoVendaMarmoraria. const ultimos = await gs.venda(45); // Listas de apoio: devolve [{ Codigo, Descricao }]. const tabelas = await gs.obterLista(8); console.log(converterDatas(ultimos.Documentos)); } catch (e) { if (e instanceof GransoftErro) console.error(e.tipo, e.message); // "api", "limite", "indisponivel"… else throw e; } ``` ## Pedir à IA para criar a integração Abra o projeto no seu editor com IA e peça, por exemplo: > Leia o AGENTS.md e https://www.gransoftweb.com/developers/llms.txt. Usando o gransoft-client.js, crie um script que liste os orçamentos dos últimos 7 dias com cliente e valor total e salve em um CSV. Mais exemplos de pedidos em https://www.gransoftweb.com/developers/usar-com-ia.md --- ## gransoft-client.js (cliente Node.js) ```js // gransoft-client.js — cliente mínimo da API do GransoftWeb para Node.js 18+ (sem dependências). // // Cuida das regras que todo código de integração precisa repetir: // - userHash no corpo JSON (ObterLista: na query string); // - HTTP 200 com o campo `Erro` (lança GransoftErro); // - limite de 50 requisições/minuto (espera e repete); // - datas "/Date(ms)/" (lerData / converterDatas). // // Documentação: https://www.gransoftweb.com/developers/llms.txt const URL_AUTENTICACAO = "https://www.gransoftweb.com/API/ObterAutenticacao"; const MAX_POR_MINUTO = 45; // abaixo do limite do servidor (50), com folga const TENTATIVAS_LIMITE = 4; // repetições quando o servidor acusa limite de requisições const TEMPO_LIMITE_MS = 60_000; // timeout de cada chamada /** Erro da API do GransoftWeb. `tipo`: "api" | "indisponivel" | "limite" | "autenticacao" | "login_necessario" | "bloqueado". */ export class GransoftErro extends Error { constructor(mensagem, { tipo = "api", metodo = "", acao, resposta } = {}) { super(mensagem); this.name = "GransoftErro"; this.tipo = tipo; this.metodo = metodo; this.acao = acao; this.resposta = resposta; } } /** Converte "/Date(1731283200000)/" em Date. Devolve null se não for uma data nesse formato. */ export function lerData(valor) { const m = /^\/Date\((-?\d+)([+-]\d{4})?\)\/$/.exec(typeof valor === "string" ? valor : ""); return m ? new Date(Number(m[1])) : null; } /** Percorre o objeto e troca todas as datas "/Date(ms)/" por Date. Altera e devolve o próprio objeto. */ export function converterDatas(obj) { if (Array.isArray(obj)) { obj.forEach((v, i) => { obj[i] = converterDatas(v); }); return obj; } if (obj && typeof obj === "object") { for (const k of Object.keys(obj)) obj[k] = converterDatas(obj[k]); return obj; } return lerData(obj) ?? obj; } /** Remove de um texto de busca os caracteres que alguns métodos antigos não tratam (aspas, %, ;). */ export function limparBusca(texto) { return String(texto ?? "").replace(/['"%;]/g, " ").replace(/\s+/g, " ").trim(); } /** Mostra só o começo e o fim do userHash, para logs. */ export function mascarar(userHash) { const s = String(userHash ?? ""); return s.length <= 10 ? "***" : `${s.slice(0, 4)}…${s.slice(-4)}`; } const esperar = (ms) => new Promise((r) => setTimeout(r, ms)); /** Indica se o texto de Erro é o aviso de limite de requisições do servidor. */ function ehLimite(erro) { return /limite de requisi/i.test(erro ?? ""); } export class GransoftClient { /** * @param {object} opcoes * @param {string} opcoes.userHash userHash do usuário (variável de ambiente; nunca no código). * @param {string} opcoes.servidor Servidor da empresa, ex.: https://www.server1.gransoftweb.com * @param {(msg: string) => void} [opcoes.log] Função de log opcional (nunca recebe o userHash). */ constructor({ userHash, servidor, log } = {}) { if (!userHash) throw new GransoftErro("Informe o userHash (ex.: variável de ambiente GRANSOFT_USERHASH).", { tipo: "autenticacao" }); if (!servidor) throw new GransoftErro("Informe o servidor da empresa (ex.: https://www.server1.gransoftweb.com).", { tipo: "autenticacao" }); this.userHash = userHash; this.servidor = servidor.replace(/\/+$/, "").replace(/\/API$/i, ""); this.log = log ?? (() => {}); this._chamadas = []; } /** * Autentica com e-mail e senha e devolve um cliente pronto. * Lança GransoftErro com tipo "autenticacao", "login_necessario" (envie `login`), "bloqueado" ou "indisponivel". */ static async autenticar({ email, senha, login, log } = {}) { const corpo = { Email: email ?? "", Senha: senha ?? "" }; if (login) corpo.Login = login; const r = await postJson(URL_AUTENTICACAO, corpo, "ObterAutenticacao"); if (r.Erro) { const tecnico = /^(The |Não foi possível obter os dados do Servidor|Erro ao Deserializar Objeto)/.test(r.Erro); throw new GransoftErro(tecnico ? "Serviço de autenticação indisponível. Tente novamente." : r.Erro, { tipo: tecnico ? "indisponivel" : "autenticacao", metodo: "ObterAutenticacao" }); } if (r.Situacao === 3) throw new GransoftErro("Há mais de um usuário com este e-mail. Informe também o login.", { tipo: "login_necessario", metodo: "ObterAutenticacao" }); if (!r.Hash) throw new GransoftErro("E-mail, login ou senha inválidos.", { tipo: "autenticacao", metodo: "ObterAutenticacao" }); if (r.Bloqueado || r.Situacao === 2) throw new GransoftErro("Usuário bloqueado no GransoftWeb.", { tipo: "bloqueado", metodo: "ObterAutenticacao" }); const cliente = new GransoftClient({ userHash: r.Hash, servidor: r.Servidor, log }); cliente.usuario = { nome: r.Nome, empresa: r.NomeEmpresa, empresaId: r.EmpresaID }; return cliente; } /** * Chama POST {servidor}/API/{metodo} com o corpo JSON (o userHash é incluído automaticamente). * Devolve a resposta já verificada (Erro vazio). Lança GransoftErro em caso de erro. */ async chamar(metodo, corpo = {}) { const url = `${this.servidor}/API/${metodo}`; return this._comRepeticao(metodo, corpo.Acao, async () => { const r = await postJson(url, { ...corpo, userHash: this.userHash }, metodo); return r; }); } /** Atalho para AcoesDocumentoVendaMarmoraria: venda(1, { Hash }) etc. */ async venda(acao, corpo = {}) { return this.chamar("AcoesDocumentoVendaMarmoraria", { ...corpo, Acao: acao }); } /** * Listas de apoio (ObterLista). Devolve o array `Dados` ([{ Codigo, Descricao }]). * tipo 16 = clientes (search = texto), 18 = materiais (search = "1"), 1 = condições de pagamento, 8 = tabelas… */ async obterLista(tipo, search = "") { const url = `${this.servidor}/API/ObterLista?userHash=${encodeURIComponent(this.userHash)}`; const r = await this._comRepeticao("ObterLista", undefined, () => postJson(url, { Tipo: [tipo], Search: search }, "ObterLista")); return r.Dados ?? []; } // --------------------------------------------------------------------------- async _comRepeticao(metodo, acao, executar) { for (let tentativa = 1; ; tentativa++) { await this._respeitarLimite(); const inicio = Date.now(); const resposta = await executar(); const bloco = Array.isArray(resposta) ? (resposta[0] ?? {}) : resposta; const erro = bloco?.Erro; this.log(`${metodo}${acao ? ` (Acao ${acao})` : ""}: ${erro ? "erro" : "ok"} em ${Date.now() - inicio} ms`); if (!erro) return bloco; if (ehLimite(erro) && tentativa < TENTATIVAS_LIMITE) { await esperar(5_000 * tentativa); continue; } throw new GransoftErro(erro, { tipo: ehLimite(erro) ? "limite" : "api", metodo, acao, resposta: bloco }); } } /** Mantém no máximo MAX_POR_MINUTO chamadas em qualquer janela de 60 s. */ async _respeitarLimite() { const agora = Date.now(); this._chamadas = this._chamadas.filter((t) => agora - t < 60_000); if (this._chamadas.length >= MAX_POR_MINUTO) { await esperar(60_000 - (agora - this._chamadas[0]) + 50); return this._respeitarLimite(); } this._chamadas.push(Date.now()); } } /** POST JSON e leitura da resposta; lança GransoftErro "indisponivel" se não vier JSON. */ async function postJson(url, corpo, metodo) { let resp; try { resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8", Accept: "application/json" }, body: JSON.stringify(corpo), signal: AbortSignal.timeout(TEMPO_LIMITE_MS), }); } catch (e) { throw new GransoftErro(`Falha de comunicação com o GransoftWeb: ${e.message}`, { tipo: "indisponivel", metodo }); } const texto = await resp.text(); if (!resp.ok) throw new GransoftErro(`GransoftWeb respondeu HTTP ${resp.status}.`, { tipo: "indisponivel", metodo }); try { return JSON.parse(texto); } catch { throw new GransoftErro("Resposta do GransoftWeb não é JSON (serviço indisponível ou método inexistente).", { tipo: "indisponivel", metodo }); } } ``` --- ## Exemplo: chapas disponíveis por material ```js // exemplo-estoque.js — chapas disponíveis de um material, com totais por material. // // Uso: node exemplo-estoque.js "verde ubatuba" // Requer as variáveis GRANSOFT_USERHASH e GRANSOFT_SERVIDOR (veja env.example). // Guia: https://www.gransoftweb.com/developers/guias/consultar-estoque-chapas.md import { GransoftClient, GransoftErro, limparBusca } from "./gransoft-client.js"; const material = process.argv[2]; if (!material) { console.error('Informe o material. Ex.: node exemplo-estoque.js "verde ubatuba"'); process.exit(1); } const gs = new GransoftClient({ userHash: process.env.GRANSOFT_USERHASH, servidor: process.env.GRANSOFT_SERVIDOR, }); try { const r = await gs.chamar("ListaEstoqueChapas", { Search: { Keyword: limparBusca(material), Status: 0, // 0 = em estoque Reserva: 0, // 0 = sem reserva (disponíveis); 2 = todas Filial: 0, // 0 = filial do usuário MaxResult: 2000, // alto, para os totais saírem completos }, }); const chapas = r.Chapas ?? []; if (chapas.length === 0) { console.log(`Nenhuma chapa disponível para "${material}".`); process.exit(0); } // Totais por material (uma busca por texto pode trazer vários materiais). const porMaterial = new Map(); for (const c of chapas) { const nome = c.Material?.Descricao ?? "(sem material)"; const t = porMaterial.get(nome) ?? { chapas: 0, m2: 0 }; t.chapas++; t.m2 += c.Dimensoes?.QtdLiqM2 ?? 0; porMaterial.set(nome, t); } console.log(`\n${chapas.length} chapa(s) disponível(is) para "${material}":\n`); for (const [nome, t] of porMaterial) console.log(` ${nome}: ${t.chapas} chapa(s), ${t.m2.toFixed(2)} m²`); console.log("\nChapas:"); console.table(chapas.slice(0, 50).map((c) => ({ chapa: c.Codigo, material: c.Material?.Descricao, "comp x alt (m)": `${c.Dimensoes?.ComprLiq} x ${c.Dimensoes?.AltLiq}`, m2: c.Dimensoes?.QtdLiqM2, lote: c.Localizacao?.txtNroLote ?? "", local: [c.Localizacao?.Local1, c.Localizacao?.Local2, c.Localizacao?.Local3].filter(Boolean).join(" / "), }))); if (chapas.length > 50) console.log(`(mostrando 50 de ${chapas.length})`); } catch (e) { if (e instanceof GransoftErro) { console.error(`GransoftWeb recusou a consulta (${e.tipo}): ${e.message}`); process.exit(2); } throw e; } ``` --- ## Exemplo: buscar clientes ```js // exemplo-clientes.js — busca clientes pelo nome e mostra os dados do escolhido. // // Uso: node exemplo-clientes.js "maria souza" (lista os encontrados) // node exemplo-clientes.js "maria souza" 1523 (dados completos do cliente 1523) // Requer as variáveis GRANSOFT_USERHASH e GRANSOFT_SERVIDOR (veja env.example). // Guia: https://www.gransoftweb.com/developers/guias/consultar-clientes.md import { GransoftClient, GransoftErro, limparBusca } from "./gransoft-client.js"; const [texto, codigo] = process.argv.slice(2); if (!texto || limparBusca(texto).length < 3) { console.error('Informe ao menos 3 letras do nome. Ex.: node exemplo-clientes.js "maria souza"'); process.exit(1); } const gs = new GransoftClient({ userHash: process.env.GRANSOFT_USERHASH, servidor: process.env.GRANSOFT_SERVIDOR, }); try { const clientes = await gs.obterLista(16, limparBusca(texto)); if (clientes.length === 0) { console.log("Nenhum cliente encontrado."); process.exit(0); } if (!codigo) { console.table(clientes.map((c) => ({ codigo: c.Codigo, cliente: c.Descricao }))); if (clientes.length > 1) console.log("Há mais de um cliente: rode de novo informando o código escolhido."); process.exit(0); } const escolhido = clientes.find((c) => String(c.Codigo) === String(codigo)); if (!escolhido) { console.error(`O código ${codigo} não está entre os clientes encontrados.`); process.exit(1); } const { DadosCliente: d } = await gs.venda(44, { DadosCliente: { Codigo: escolhido.Codigo } }); console.log({ codigo: d.Codigo, nome: d.Nome, documento: d.CpfCnpj, endereco: [d.Rua, d.Numero, d.EndCompl, d.Bairro].filter(Boolean).join(", "), cidade: `${d.Cidade}/${d.Estado}`, cep: d.CEP, celular: d.Celular, email: d.Email, }); } catch (e) { if (e instanceof GransoftErro) { console.error(`GransoftWeb recusou a consulta (${e.tipo}): ${e.message}`); process.exit(2); } throw e; } ``` --- ## Exemplo: criar orçamento com peça sob medida ```js // exemplo-orcamento.js — cria um orçamento com uma peça sob medida (ex.: pia) e um acabamento. // // ATENÇÃO: grava no GransoftWeb. Use um usuário e um cliente de teste. // Sem --gravar, só mostra o que faria. // // Uso: // node exemplo-orcamento.js --cliente 1523 --produto 55 --material 120 \ // --pecas "Tampo 1,20 x 0,60; Frontão 1,20 x 0,10" --acabamento 77:2.4 --gravar // // --cliente código do cliente (ObterLista tipo 16 / exemplo-clientes.js) // --produto produto de produção, o tipo da peça (ObterLista tipo 5) // --material material/pedra (ObterLista tipo 18, Search "1") // --pecas partes da peça em metros, separadas por ";" // --acabamento opcional, "codigo:metros_lineares" (ObterLista tipo 7) // // Requer as variáveis GRANSOFT_USERHASH e GRANSOFT_SERVIDOR (veja env.example). // Guia: https://www.gransoftweb.com/developers/guias/criar-orcamento.md import { parseArgs } from "node:util"; import { GransoftClient, GransoftErro } from "./gransoft-client.js"; const { values: arg } = parseArgs({ options: { cliente: { type: "string" }, produto: { type: "string" }, material: { type: "string" }, pecas: { type: "string" }, acabamento: { type: "string" }, gravar: { type: "boolean", default: false }, }, }); const cliente = Number(arg.cliente), produto = Number(arg.produto), material = Number(arg.material); if (!cliente || !produto || !material || !arg.pecas) { console.error("Informe --cliente, --produto, --material e --pecas. Veja o cabeçalho do arquivo."); process.exit(1); } let acabamento = null; if (arg.acabamento) { const [cod, ml] = arg.acabamento.split(":"); acabamento = { codigo: Number(cod), ml: Number(String(ml).replace(",", ".")) }; if (!acabamento.codigo || !(acabamento.ml > 0)) { console.error('--acabamento deve ser "codigo:metros_lineares", ex.: 77:2.4'); process.exit(1); } } if (!arg.gravar) { console.log("Simulação (nada foi gravado). Passos que seriam executados:"); console.log(` 1. Acao 2 — criar orçamento para o cliente ${cliente}`); console.log(` 2. Acao 19 — criar item: produto ${produto}, material ${material}`); console.log(` 3. Acao 39 — medidas: ${arg.pecas}`); console.log(" 4. Acao 40 — remover peças-modelo que ficarem com área zero"); if (acabamento) console.log(` 5. Acao 20 — acabamento ${acabamento.codigo}: ${acabamento.ml} ML`); console.log(" 6. Acao 1 — conferir totais; Acao 37 — encerrar a edição"); console.log("\nRode de novo com --gravar para executar."); process.exit(0); } const gs = new GransoftClient({ userHash: process.env.GRANSOFT_USERHASH, servidor: process.env.GRANSOFT_SERVIDOR, log: (m) => console.log(" ·", m), }); let hash = ""; try { // 1. Criar o orçamento. const doc = await gs.venda(2, { DadosCliente: { Codigo: cliente } }); hash = doc.Hash; console.log("Orçamento criado."); // 2. Criar o item de produção com a pedra, sem medidas. // TipoItem 0 = o servidor deduz o tipo pelo produto. // Um Erro pedindo as medidas é esperado aqui quando o item foi criado. let item; try { item = (await gs.venda(19, { Hash: hash, Itens: [{ CodProduto: produto, TipoItem: 0, Qtd: 1, Medidas: [{ Medida: "", Material: { CodMaterial: material } }] }], })).Itens?.[0]; } catch (e) { item = e instanceof GransoftErro && /medida/i.test(e.message) ? e.resposta?.Itens?.[0] : null; if (!item?.Codigo) throw e; } if (!item?.Codigo) throw new Error("O GransoftWeb não devolveu o item criado."); // Peças-modelo vazias que o sistema cria ao escolher a pedra. const modelos = (item.Medidas ?? []).filter((m) => m.Codigo > 0).map((m) => m.Codigo); // 3. Medidas reais (várias partes separadas por ";"). const comMedidas = (await gs.venda(39, { Hash: hash, Itens: [{ Codigo: item.Codigo, Medidas: [{ Medida: arg.pecas, Material: { CodMaterial: material } }] }], })).Itens?.[0]; // 4. Remover peças-modelo que continuaram vazias (bloqueiam a geração do pedido). for (const m of comMedidas?.Medidas ?? []) { if (modelos.includes(m.Codigo) && !(m.QtdTotalM2 > 0)) await gs.venda(40, { Hash: hash, Itens: [{ Codigo: item.Codigo, Medidas: [{ Codigo: m.Codigo }] }] }); } // 5. Acabamento em metros lineares (o sistema não calcula a quantidade). if (acabamento) await gs.venda(20, { Hash: hash, Itens: [{ Codigo: item.Codigo, CodProduto: acabamento.codigo, Qtd: acabamento.ml }] }); // 6. Conferir. const final = await gs.venda(1, { Hash: hash }); const itens = (await gs.venda(25, { Hash: hash })).Itens ?? []; console.log(`\nOrçamento ${final.NroDocumento} — ${final.DadosCliente?.Nome}`); for (const it of itens) { console.log(` Item ${it.Codigo}: ${it.Descricao} — ${it.QtdM2 ?? ""} m² — R$ ${it.vrTotal}`); for (const m of it.Medidas ?? []) console.log(` ${m.Descricao}: ${m.Qtd} x ${m.Comp} x ${m.Larg} m = ${m.QtdTotalM2} m²`); } console.log(`Total do orçamento: R$ ${final.Detalhe?.ValorTotalDocumento ?? final.ValorTotalDocumento}`); } catch (e) { console.error(e instanceof GransoftErro ? `GransoftWeb recusou (${e.metodo} ${e.acao ?? ""}): ${e.message}` : e); if (hash) console.error("O orçamento pode ter sido criado em parte. Confira no GransoftWeb."); process.exitCode = 2; } finally { // 7. Encerrar a edição no servidor. if (hash) await gs.venda(37, { Hash: hash }).catch(() => {}); } ``` --- # AGENTS.md — integração com a API do GransoftWeb Instruções para agentes de IA (Claude Code, Cursor, Copilot, Codex…) que trabalham neste projeto. Copie este arquivo para a raiz do projeto. Se usar Claude Code, copie também como `CLAUDE.md`. ## Fonte da verdade - Documentação: https://www.gransoftweb.com/developers/llms.txt (leia antes de escrever código de integração). - MCP de documentação (se estiver conectado): use `buscar_documentacao` e `obter_documento` antes de chamar um método novo. - Swagger: http://www.server1.gepersistemas.com.br:8082/swagger/ui/index - **Não invente métodos, campos ou números de `Acao`.** Se não estiver na documentação ou no Swagger, pergunte ao desenvolvedor. ## Regras obrigatórias 1. Use sempre o cliente `gransoft-client.js` (`GransoftClient`). Não chame `fetch` direto para a API. 2. O `userHash` e o servidor vêm de variáveis de ambiente (`GRANSOFT_USERHASH`, `GRANSOFT_SERVIDOR`). Nunca escreva o `userHash` no código, em arquivos versionados, no front-end ou em logs. 3. A API responde HTTP 200 mesmo com erro. O cliente já lança `GransoftErro` quando o campo `Erro` vem preenchido: trate essa exceção e mostre `e.message` ao usuário. 4. O `userHash` vai no corpo JSON (o cliente já faz isso). `ObterLista` é a exceção (query string), tratada por `obterLista()`. 5. Limite de 50 requisições por minuto. O cliente controla o ritmo; não dispare chamadas em paralelo em massa. 6. Datas chegam como `"/Date(ms)/"`: use `lerData()` ou `converterDatas()`. 7. Textos de busca passam por `limparBusca()`. 8. Códigos (cliente, produto, material, tabela) vêm das listas (`obterLista`) ou de consultas. Nunca fixe códigos no código. 9. Se a busca devolver mais de um cliente, produto ou material compatível, pergunte ao usuário qual usar. 10. Medidas em metros (`1.20`); acabamentos de borda em metros lineares. ## Gravações - Toda chamada de gravação (orçamento, agenda, cadastro) grava no banco na hora. Não há "salvar" final nem desfazer. - Teste gravações apenas com o usuário e dados de teste. Peça confirmação ao desenvolvedor antes de rodar código que grava. - Em `AcoesDocumentoVendaMarmoraria`: - envie sempre `FieldsToUpdate` nas ações `3` e `21`; - nas ações `42`/`43` envie o endereço completo (elas substituem todos os campos); - pedidos (`Tipo` = 1) não podem ser alterados. ## Estilo do código de integração - Node.js 18+ com ES modules (`import`). - Funções pequenas por tarefa (ex.: `listarChapasDisponiveis(material)`), cada uma com tratamento de `GransoftErro`. - Nada de segredo em `console.log`. Para depurar, use `mascarar(userHash)`. --- # Histórico de alterações da documentação ## 2026-11-11 — versão 1.0 - Portal de desenvolvedores em https://www.gransoftweb.com/developers. - Guias: início rápido, autenticação, convenções, estoque de chapas, clientes e criação de orçamento. - Kit em Node.js: `gransoft-client.js` e exemplos de estoque, clientes e orçamento; `AGENTS.md` para agentes de IA. - Índice para IAs (`llms.txt`, `llms-full.txt`) e MCP de documentação (`/developers/mcp`). - Correções em relação a documentos anteriores: - o `userHash` vai no corpo JSON: `AcoesDocumentoVendaMarmoraria` ignora o header `Authorization`; - `AdicionarMedidas` (ação 39) aceita várias partes separadas por `;` ou campos separados (`Qtd`, `Comp`, `Larg`); - as ações 42 e 43 substituem todos os campos de endereço; - a ação 3 sem `FieldsToUpdate` grava todos os campos do documento; - `TipoItem: 0` na criação do item faz o servidor deduzir o tipo pelo produto; - peças-modelo vazias criadas ao escolher a pedra devem ser removidas.