sienge-ecbiesek-mcp 1.1.1__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sienge-ecbiesek-mcp might be problematic. Click here for more details.
- {sienge_ecbiesek_mcp-1.1.1.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/METADATA +30 -8
- sienge_ecbiesek_mcp-1.2.0.dist-info/RECORD +11 -0
- sienge_mcp/metadata.py +43 -0
- sienge_mcp/server.py +1884 -391
- sienge_mcp/server2.py +2679 -0
- sienge_mcp/utils/logger.py +24 -25
- sienge_ecbiesek_mcp-1.1.1.dist-info/RECORD +0 -9
- {sienge_ecbiesek_mcp-1.1.1.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/WHEEL +0 -0
- {sienge_ecbiesek_mcp-1.1.1.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/entry_points.txt +0 -0
- {sienge_ecbiesek_mcp-1.1.1.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {sienge_ecbiesek_mcp-1.1.1.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/top_level.txt +0 -0
sienge_mcp/server.py
CHANGED
|
@@ -2,22 +2,32 @@
|
|
|
2
2
|
"""
|
|
3
3
|
SIENGE MCP COMPLETO - FastMCP com Autenticação Flexível
|
|
4
4
|
Suporta Bearer Token e Basic Auth
|
|
5
|
+
CORREÇÕES IMPLEMENTADAS:
|
|
6
|
+
1. Separação entre camada MCP e camada de serviços
|
|
7
|
+
2. Alias compatíveis com checklist
|
|
8
|
+
3. Normalização de parâmetros (camelCase + arrays)
|
|
9
|
+
4. Bulk-data assíncrono com polling
|
|
10
|
+
5. Observabilidade mínima (X-Request-ID, cache, logs)
|
|
11
|
+
6. Ajustes de compatibilidade pontuais
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
14
|
from fastmcp import FastMCP
|
|
8
15
|
import httpx
|
|
9
|
-
import
|
|
10
|
-
from typing import Dict, List, Optional, Any
|
|
11
|
-
import json
|
|
16
|
+
from typing import Dict, List, Optional, Any, Union
|
|
12
17
|
import os
|
|
13
18
|
from dotenv import load_dotenv
|
|
14
|
-
from datetime import datetime
|
|
15
|
-
import
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
import uuid
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import time
|
|
24
|
+
import logging
|
|
25
|
+
from functools import wraps
|
|
16
26
|
|
|
17
27
|
# Carrega as variáveis de ambiente
|
|
18
28
|
load_dotenv()
|
|
19
29
|
|
|
20
|
-
mcp = FastMCP("Sienge API Integration 🏗️")
|
|
30
|
+
mcp = FastMCP("Sienge API Integration 🏗️ - ChatGPT Compatible")
|
|
21
31
|
|
|
22
32
|
# Configurações da API do Sienge
|
|
23
33
|
SIENGE_BASE_URL = os.getenv("SIENGE_BASE_URL", "https://api.sienge.com.br")
|
|
@@ -27,165 +37,635 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
|
|
|
27
37
|
SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
|
|
28
38
|
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
|
|
29
39
|
|
|
40
|
+
# Cache simples em memória
|
|
41
|
+
_cache = {}
|
|
42
|
+
CACHE_TTL = 300 # 5 minutos
|
|
43
|
+
|
|
44
|
+
# Configurar logging estruturado
|
|
45
|
+
logging.basicConfig(
|
|
46
|
+
level=logging.INFO,
|
|
47
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
48
|
+
)
|
|
49
|
+
logger = logging.getLogger("sienge-mcp")
|
|
50
|
+
|
|
51
|
+
|
|
30
52
|
class SiengeAPIError(Exception):
|
|
31
53
|
"""Exceção customizada para erros da API do Sienge"""
|
|
32
54
|
pass
|
|
33
55
|
|
|
34
|
-
|
|
56
|
+
|
|
57
|
+
# ============ HELPERS DE NORMALIZAÇÃO E OBSERVABILIDADE ============
|
|
58
|
+
|
|
59
|
+
def _camel(s: str) -> str:
|
|
60
|
+
"""Converte snake_case para camelCase"""
|
|
61
|
+
if '_' not in s:
|
|
62
|
+
return s
|
|
63
|
+
parts = s.split('_')
|
|
64
|
+
return parts[0] + ''.join(x.capitalize() for x in parts[1:])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def to_query(params: dict) -> dict:
|
|
68
|
+
"""
|
|
69
|
+
Converte parâmetros para query string normalizada:
|
|
70
|
+
- snake_case → camelCase
|
|
71
|
+
- listas/tuplas → CSV string
|
|
72
|
+
- booleanos → 'true'/'false' (minúsculo)
|
|
73
|
+
- remove valores None
|
|
74
|
+
"""
|
|
75
|
+
if not params:
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
out = {}
|
|
79
|
+
for k, v in params.items():
|
|
80
|
+
if v is None:
|
|
81
|
+
continue
|
|
82
|
+
key = _camel(k)
|
|
83
|
+
if isinstance(v, (list, tuple)):
|
|
84
|
+
out[key] = ','.join(map(str, v))
|
|
85
|
+
elif isinstance(v, bool):
|
|
86
|
+
out[key] = 'true' if v else 'false'
|
|
87
|
+
else:
|
|
88
|
+
out[key] = v
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def to_camel_json(obj: Any) -> Any:
|
|
93
|
+
"""
|
|
94
|
+
Normaliza payload JSON recursivamente:
|
|
95
|
+
- snake_case → camelCase nas chaves
|
|
96
|
+
- remove valores None
|
|
97
|
+
- mantém estrutura de listas e objetos aninhados
|
|
98
|
+
"""
|
|
99
|
+
if isinstance(obj, dict):
|
|
100
|
+
return {_camel(k): to_camel_json(v) for k, v in obj.items() if v is not None}
|
|
101
|
+
elif isinstance(obj, list):
|
|
102
|
+
return [to_camel_json(x) for x in obj]
|
|
103
|
+
else:
|
|
104
|
+
return obj
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _normalize_url(base_url: str, subdomain: str) -> str:
|
|
108
|
+
"""Normaliza URL evitando //public/api quando subdomain está vazio"""
|
|
109
|
+
if not subdomain or subdomain.strip() == "":
|
|
110
|
+
return f"{base_url.rstrip('/')}/public/api"
|
|
111
|
+
return f"{base_url.rstrip('/')}/{subdomain.strip()}/public/api"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _parse_numeric_value(value: Any) -> float:
|
|
115
|
+
"""Sanitiza valores numéricos, lidando com vírgulas decimais"""
|
|
116
|
+
if value is None:
|
|
117
|
+
return 0.0
|
|
118
|
+
|
|
119
|
+
if isinstance(value, (int, float)):
|
|
120
|
+
return float(value)
|
|
121
|
+
|
|
122
|
+
# Se for string, tentar converter
|
|
123
|
+
str_value = str(value).strip()
|
|
124
|
+
if not str_value:
|
|
125
|
+
return 0.0
|
|
126
|
+
|
|
127
|
+
# Trocar vírgula por ponto decimal
|
|
128
|
+
str_value = str_value.replace(',', '.')
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
return float(str_value)
|
|
132
|
+
except (ValueError, TypeError):
|
|
133
|
+
logger.warning(f"Não foi possível converter '{value}' para número")
|
|
134
|
+
return 0.0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _get_cache_key(endpoint: str, params: dict = None) -> str:
|
|
138
|
+
"""Gera chave de cache baseada no endpoint e parâmetros"""
|
|
139
|
+
cache_params = json.dumps(params or {}, sort_keys=True)
|
|
140
|
+
return f"{endpoint}:{hash(cache_params)}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _set_cache(key: str, data: Any) -> None:
|
|
144
|
+
"""Armazena dados no cache com TTL"""
|
|
145
|
+
_cache[key] = {
|
|
146
|
+
"data": data,
|
|
147
|
+
"timestamp": time.time(),
|
|
148
|
+
"ttl": CACHE_TTL
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _get_cache(key: str) -> Optional[Dict]:
|
|
153
|
+
"""Recupera dados do cache se ainda válidos - CORRIGIDO: mantém shape original"""
|
|
154
|
+
if key not in _cache:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
cached = _cache[key]
|
|
158
|
+
if time.time() - cached["timestamp"] > cached["ttl"]:
|
|
159
|
+
del _cache[key] # Remove cache expirado
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
# cached["data"] já é o "result" completo salvo em _set_cache
|
|
163
|
+
result = dict(cached["data"]) # cópia rasa
|
|
164
|
+
result["cache"] = {
|
|
165
|
+
"hit": True,
|
|
166
|
+
"ttl_s": cached["ttl"] - (time.time() - cached["timestamp"])
|
|
167
|
+
}
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _log_request(method: str, endpoint: str, status_code: int, latency: float, request_id: str) -> None:
|
|
172
|
+
"""Log estruturado das requisições"""
|
|
173
|
+
logger.info(
|
|
174
|
+
f"HTTP {method} {endpoint} - Status: {status_code} - "
|
|
175
|
+
f"Latency: {latency:.3f}s - RequestID: {request_id}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _extract_items_and_total(resp_data: Any) -> tuple:
|
|
180
|
+
"""
|
|
181
|
+
Extrai items e total count de resposta padronizada da API Sienge
|
|
182
|
+
Retorna: (items_list, total_count)
|
|
183
|
+
"""
|
|
184
|
+
items = resp_data.get("results", []) if isinstance(resp_data, dict) else (resp_data or [])
|
|
185
|
+
meta = resp_data.get("resultSetMetadata", {}) if isinstance(resp_data, dict) else {}
|
|
186
|
+
total = meta.get("count", len(items))
|
|
187
|
+
return items, total
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def make_sienge_request(
|
|
191
|
+
method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None, use_cache: bool = True
|
|
192
|
+
) -> Dict:
|
|
35
193
|
"""
|
|
36
194
|
Função auxiliar para fazer requisições à API do Sienge (v1)
|
|
37
195
|
Suporta tanto Bearer Token quanto Basic Auth
|
|
196
|
+
MELHORADO: Observabilidade, cache, normalização de parâmetros
|
|
38
197
|
"""
|
|
198
|
+
start_time = time.time()
|
|
199
|
+
req_id = str(uuid.uuid4())
|
|
200
|
+
|
|
39
201
|
try:
|
|
202
|
+
# Normalizar parâmetros
|
|
203
|
+
normalized_params = to_query(params) if params else None
|
|
204
|
+
|
|
205
|
+
# Verificar cache para operações GET
|
|
206
|
+
cache_key = None
|
|
207
|
+
if method.upper() == "GET" and use_cache and endpoint in ["/customer-types", "/creditors", "/customers"]:
|
|
208
|
+
cache_key = _get_cache_key(endpoint, normalized_params)
|
|
209
|
+
cached_result = _get_cache(cache_key)
|
|
210
|
+
if cached_result:
|
|
211
|
+
cached_result["request_id"] = req_id
|
|
212
|
+
return cached_result
|
|
213
|
+
|
|
40
214
|
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
41
215
|
headers = {
|
|
42
|
-
"Content-Type": "application/json",
|
|
43
|
-
"Accept": "application/json"
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
"Accept": "application/json",
|
|
218
|
+
"X-Request-ID": req_id
|
|
44
219
|
}
|
|
45
|
-
|
|
46
|
-
# Configurar autenticação e URL
|
|
220
|
+
|
|
221
|
+
# Configurar autenticação e URL (corrigindo URLs duplas)
|
|
47
222
|
auth = None
|
|
48
|
-
|
|
223
|
+
base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
|
|
224
|
+
|
|
49
225
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
50
|
-
# Bearer Token (Recomendado)
|
|
51
226
|
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
52
|
-
url = f"{
|
|
227
|
+
url = f"{base_normalized}/v1{endpoint}"
|
|
53
228
|
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
54
|
-
# Basic Auth usando httpx.BasicAuth
|
|
55
229
|
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
56
|
-
url = f"{
|
|
230
|
+
url = f"{base_normalized}/v1{endpoint}"
|
|
57
231
|
else:
|
|
58
232
|
return {
|
|
59
233
|
"success": False,
|
|
60
234
|
"error": "No Authentication",
|
|
61
|
-
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
|
|
235
|
+
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
236
|
+
"request_id": req_id
|
|
62
237
|
}
|
|
63
|
-
|
|
238
|
+
|
|
64
239
|
response = await client.request(
|
|
65
|
-
method=method,
|
|
66
|
-
url=url,
|
|
67
|
-
headers=headers,
|
|
68
|
-
params=
|
|
69
|
-
json=json_data,
|
|
240
|
+
method=method,
|
|
241
|
+
url=url,
|
|
242
|
+
headers=headers,
|
|
243
|
+
params=normalized_params,
|
|
244
|
+
json=json_data,
|
|
70
245
|
auth=auth
|
|
71
246
|
)
|
|
72
247
|
|
|
248
|
+
latency = time.time() - start_time
|
|
249
|
+
_log_request(method, endpoint, response.status_code, latency, req_id)
|
|
250
|
+
|
|
73
251
|
if response.status_code in [200, 201]:
|
|
74
252
|
try:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
"
|
|
253
|
+
data = response.json()
|
|
254
|
+
result = {
|
|
255
|
+
"success": True,
|
|
256
|
+
"data": data,
|
|
257
|
+
"status_code": response.status_code,
|
|
258
|
+
"request_id": req_id,
|
|
259
|
+
"latency_ms": round(latency * 1000, 2)
|
|
79
260
|
}
|
|
80
|
-
|
|
261
|
+
|
|
262
|
+
# Armazenar no cache se aplicável
|
|
263
|
+
if cache_key and method.upper() == "GET":
|
|
264
|
+
_set_cache(cache_key, result)
|
|
265
|
+
result["cache"] = {"hit": False, "ttl_s": CACHE_TTL}
|
|
266
|
+
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
except Exception:
|
|
81
270
|
return {
|
|
82
|
-
"success": True,
|
|
83
|
-
"data": {"message": "Success"},
|
|
84
|
-
"status_code": response.status_code
|
|
271
|
+
"success": True,
|
|
272
|
+
"data": {"message": "Success"},
|
|
273
|
+
"status_code": response.status_code,
|
|
274
|
+
"request_id": req_id,
|
|
275
|
+
"latency_ms": round(latency * 1000, 2)
|
|
85
276
|
}
|
|
86
277
|
else:
|
|
87
278
|
return {
|
|
88
279
|
"success": False,
|
|
89
280
|
"error": f"HTTP {response.status_code}",
|
|
90
281
|
"message": response.text,
|
|
91
|
-
"status_code": response.status_code
|
|
282
|
+
"status_code": response.status_code,
|
|
283
|
+
"request_id": req_id,
|
|
284
|
+
"latency_ms": round(latency * 1000, 2)
|
|
92
285
|
}
|
|
93
|
-
|
|
286
|
+
|
|
94
287
|
except httpx.TimeoutException:
|
|
288
|
+
latency = time.time() - start_time
|
|
289
|
+
_log_request(method, endpoint, 408, latency, req_id)
|
|
95
290
|
return {
|
|
96
|
-
"success": False,
|
|
97
|
-
"error": "Timeout",
|
|
98
|
-
"message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"
|
|
291
|
+
"success": False,
|
|
292
|
+
"error": "Timeout",
|
|
293
|
+
"message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s",
|
|
294
|
+
"request_id": req_id,
|
|
295
|
+
"latency_ms": round(latency * 1000, 2)
|
|
99
296
|
}
|
|
100
297
|
except Exception as e:
|
|
298
|
+
latency = time.time() - start_time
|
|
299
|
+
_log_request(method, endpoint, 500, latency, req_id)
|
|
101
300
|
return {
|
|
102
|
-
"success": False,
|
|
103
|
-
"error": str(e),
|
|
104
|
-
"message": f"Erro na requisição: {str(e)}"
|
|
301
|
+
"success": False,
|
|
302
|
+
"error": str(e),
|
|
303
|
+
"message": f"Erro na requisição: {str(e)}",
|
|
304
|
+
"request_id": req_id,
|
|
305
|
+
"latency_ms": round(latency * 1000, 2)
|
|
105
306
|
}
|
|
106
307
|
|
|
107
|
-
|
|
308
|
+
|
|
309
|
+
async def make_sienge_bulk_request(
|
|
310
|
+
method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
|
|
311
|
+
) -> Dict:
|
|
108
312
|
"""
|
|
109
313
|
Função auxiliar para fazer requisições à API bulk-data do Sienge
|
|
110
314
|
Suporta tanto Bearer Token quanto Basic Auth
|
|
315
|
+
MELHORADO: Observabilidade e normalização de parâmetros
|
|
111
316
|
"""
|
|
317
|
+
start_time = time.time()
|
|
318
|
+
req_id = str(uuid.uuid4())
|
|
319
|
+
|
|
112
320
|
try:
|
|
321
|
+
# Normalizar parâmetros
|
|
322
|
+
normalized_params = to_query(params) if params else None
|
|
323
|
+
|
|
113
324
|
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
114
325
|
headers = {
|
|
115
|
-
"Content-Type": "application/json",
|
|
116
|
-
"Accept": "application/json"
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
"Accept": "application/json",
|
|
328
|
+
"X-Request-ID": req_id
|
|
117
329
|
}
|
|
118
|
-
|
|
119
|
-
# Configurar autenticação e URL para bulk-data
|
|
330
|
+
|
|
331
|
+
# Configurar autenticação e URL para bulk-data (corrigindo URLs duplas)
|
|
120
332
|
auth = None
|
|
121
|
-
|
|
333
|
+
base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
|
|
334
|
+
|
|
122
335
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
123
|
-
# Bearer Token (Recomendado)
|
|
124
336
|
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
125
|
-
url = f"{
|
|
337
|
+
url = f"{base_normalized}/bulk-data/v1{endpoint}"
|
|
126
338
|
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
127
|
-
# Basic Auth usando httpx.BasicAuth
|
|
128
339
|
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
129
|
-
url = f"{
|
|
340
|
+
url = f"{base_normalized}/bulk-data/v1{endpoint}"
|
|
130
341
|
else:
|
|
131
342
|
return {
|
|
132
343
|
"success": False,
|
|
133
344
|
"error": "No Authentication",
|
|
134
|
-
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
|
|
345
|
+
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
346
|
+
"request_id": req_id
|
|
135
347
|
}
|
|
136
|
-
|
|
348
|
+
|
|
137
349
|
response = await client.request(
|
|
138
|
-
method=method,
|
|
139
|
-
url=url,
|
|
140
|
-
headers=headers,
|
|
141
|
-
params=
|
|
142
|
-
json=json_data,
|
|
350
|
+
method=method,
|
|
351
|
+
url=url,
|
|
352
|
+
headers=headers,
|
|
353
|
+
params=normalized_params,
|
|
354
|
+
json=json_data,
|
|
143
355
|
auth=auth
|
|
144
356
|
)
|
|
145
357
|
|
|
146
|
-
|
|
358
|
+
latency = time.time() - start_time
|
|
359
|
+
_log_request(method, f"bulk-data{endpoint}", response.status_code, latency, req_id)
|
|
360
|
+
|
|
361
|
+
if response.status_code in [200, 201, 202]:
|
|
147
362
|
try:
|
|
148
363
|
return {
|
|
149
|
-
"success": True,
|
|
150
|
-
"data": response.json(),
|
|
151
|
-
"status_code": response.status_code
|
|
364
|
+
"success": True,
|
|
365
|
+
"data": response.json(),
|
|
366
|
+
"status_code": response.status_code,
|
|
367
|
+
"request_id": req_id,
|
|
368
|
+
"latency_ms": round(latency * 1000, 2)
|
|
152
369
|
}
|
|
153
|
-
except:
|
|
370
|
+
except Exception:
|
|
154
371
|
return {
|
|
155
|
-
"success": True,
|
|
156
|
-
"data": {"message": "Success"},
|
|
157
|
-
"status_code": response.status_code
|
|
372
|
+
"success": True,
|
|
373
|
+
"data": {"message": "Success"},
|
|
374
|
+
"status_code": response.status_code,
|
|
375
|
+
"request_id": req_id,
|
|
376
|
+
"latency_ms": round(latency * 1000, 2)
|
|
158
377
|
}
|
|
159
378
|
else:
|
|
160
379
|
return {
|
|
161
380
|
"success": False,
|
|
162
381
|
"error": f"HTTP {response.status_code}",
|
|
163
382
|
"message": response.text,
|
|
164
|
-
"status_code": response.status_code
|
|
383
|
+
"status_code": response.status_code,
|
|
384
|
+
"request_id": req_id,
|
|
385
|
+
"latency_ms": round(latency * 1000, 2)
|
|
165
386
|
}
|
|
166
|
-
|
|
387
|
+
|
|
167
388
|
except httpx.TimeoutException:
|
|
389
|
+
latency = time.time() - start_time
|
|
390
|
+
_log_request(method, f"bulk-data{endpoint}", 408, latency, req_id)
|
|
168
391
|
return {
|
|
169
|
-
"success": False,
|
|
170
|
-
"error": "Timeout",
|
|
171
|
-
"message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s"
|
|
392
|
+
"success": False,
|
|
393
|
+
"error": "Timeout",
|
|
394
|
+
"message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s",
|
|
395
|
+
"request_id": req_id,
|
|
396
|
+
"latency_ms": round(latency * 1000, 2)
|
|
172
397
|
}
|
|
173
398
|
except Exception as e:
|
|
399
|
+
latency = time.time() - start_time
|
|
400
|
+
_log_request(method, f"bulk-data{endpoint}", 500, latency, req_id)
|
|
174
401
|
return {
|
|
175
|
-
"success": False,
|
|
176
|
-
"error": str(e),
|
|
177
|
-
"message": f"Erro na requisição bulk-data: {str(e)}"
|
|
402
|
+
"success": False,
|
|
403
|
+
"error": str(e),
|
|
404
|
+
"message": f"Erro na requisição bulk-data: {str(e)}",
|
|
405
|
+
"request_id": req_id,
|
|
406
|
+
"latency_ms": round(latency * 1000, 2)
|
|
178
407
|
}
|
|
179
408
|
|
|
409
|
+
|
|
410
|
+
async def _fetch_bulk_with_polling(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
|
|
411
|
+
"""
|
|
412
|
+
Faz requisição bulk com polling automático para requests assíncronos (202)
|
|
413
|
+
"""
|
|
414
|
+
correlation_id = str(uuid.uuid4())
|
|
415
|
+
|
|
416
|
+
# Fazer requisição inicial
|
|
417
|
+
result = await make_sienge_bulk_request(method, endpoint, params, json_data)
|
|
418
|
+
|
|
419
|
+
# Se não foi 202 ou não tem identifier, retornar resultado direto
|
|
420
|
+
if result.get("status_code") != 202:
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
data = result.get("data", {})
|
|
424
|
+
if not isinstance(data, dict) or not data.get("identifier"):
|
|
425
|
+
return result
|
|
426
|
+
|
|
427
|
+
# Processar requisição assíncrona com polling
|
|
428
|
+
identifier = data["identifier"]
|
|
429
|
+
request_id = result.get("request_id")
|
|
430
|
+
|
|
431
|
+
logger.info(f"Iniciando polling para bulk request - Identifier: {identifier} - RequestID: {request_id}")
|
|
432
|
+
|
|
433
|
+
max_attempts = 30 # Máximo 5 minutos (30 * 10s)
|
|
434
|
+
attempt = 0
|
|
435
|
+
backoff_delay = 2 # Começar com 2 segundos
|
|
436
|
+
|
|
437
|
+
while attempt < max_attempts:
|
|
438
|
+
attempt += 1
|
|
439
|
+
await asyncio.sleep(backoff_delay)
|
|
440
|
+
|
|
441
|
+
# Verificar status do processamento
|
|
442
|
+
status_result = await make_sienge_bulk_request("GET", f"/async/{identifier}")
|
|
443
|
+
|
|
444
|
+
if not status_result["success"]:
|
|
445
|
+
logger.error(f"Erro ao verificar status do bulk request {identifier}: {status_result.get('error')}")
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
status_data = status_result.get("data", {})
|
|
449
|
+
status = status_data.get("status", "unknown")
|
|
450
|
+
|
|
451
|
+
logger.info(f"Polling attempt {attempt} - Status: {status} - Identifier: {identifier}")
|
|
452
|
+
|
|
453
|
+
if status == "completed":
|
|
454
|
+
# Buscar resultados finais
|
|
455
|
+
all_chunks = []
|
|
456
|
+
chunk_count = status_data.get("chunk_count", 1)
|
|
457
|
+
|
|
458
|
+
chunks_downloaded = 0
|
|
459
|
+
for chunk_num in range(chunk_count):
|
|
460
|
+
try:
|
|
461
|
+
# CORRIGIDO: endpoint aninhado sob /async
|
|
462
|
+
chunk_result = await make_sienge_bulk_request("GET", f"/async/{identifier}/result/{chunk_num}")
|
|
463
|
+
if chunk_result["success"]:
|
|
464
|
+
chunk_data = chunk_result.get("data", {}).get("data", [])
|
|
465
|
+
if isinstance(chunk_data, list):
|
|
466
|
+
all_chunks.extend(chunk_data)
|
|
467
|
+
chunks_downloaded += 1
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.warning(f"Erro ao buscar chunk {chunk_num}: {e}")
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
"success": True,
|
|
473
|
+
"data": all_chunks,
|
|
474
|
+
"async_identifier": identifier,
|
|
475
|
+
"correlation_id": correlation_id,
|
|
476
|
+
"chunks_downloaded": chunks_downloaded,
|
|
477
|
+
"rows_returned": len(all_chunks),
|
|
478
|
+
"polling_attempts": attempt,
|
|
479
|
+
"request_id": request_id
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
elif status == "failed" or status == "error":
|
|
483
|
+
return {
|
|
484
|
+
"success": False,
|
|
485
|
+
"error": "Bulk processing failed",
|
|
486
|
+
"message": status_data.get("error_message", "Processamento bulk falhou"),
|
|
487
|
+
"async_identifier": identifier,
|
|
488
|
+
"correlation_id": correlation_id,
|
|
489
|
+
"polling_attempts": attempt,
|
|
490
|
+
"request_id": request_id
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# Aumentar delay progressivamente (backoff exponencial limitado)
|
|
494
|
+
backoff_delay = min(backoff_delay * 1.5, 10) # Máximo 10 segundos
|
|
495
|
+
|
|
496
|
+
# Timeout do polling
|
|
497
|
+
return {
|
|
498
|
+
"success": False,
|
|
499
|
+
"error": "Polling timeout",
|
|
500
|
+
"message": f"Processamento bulk não completou em {max_attempts} tentativas",
|
|
501
|
+
"async_identifier": identifier,
|
|
502
|
+
"correlation_id": correlation_id,
|
|
503
|
+
"polling_attempts": attempt,
|
|
504
|
+
"request_id": request_id
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# ============ CAMADA DE SERVIÇOS (FUNÇÕES INTERNAS) ============
|
|
509
|
+
|
|
510
|
+
async def _svc_get_customer_types() -> Dict:
|
|
511
|
+
"""Serviço interno: buscar tipos de clientes"""
|
|
512
|
+
return await make_sienge_request("GET", "/customer-types", use_cache=True)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
async def _svc_get_customers(*, limit: int = 50, offset: int = 0, search: str = None, customer_type_id: str = None) -> Dict:
|
|
516
|
+
"""Serviço interno: buscar clientes"""
|
|
517
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
518
|
+
if search:
|
|
519
|
+
params["search"] = search
|
|
520
|
+
if customer_type_id:
|
|
521
|
+
params["customer_type_id"] = customer_type_id
|
|
522
|
+
|
|
523
|
+
return await make_sienge_request("GET", "/customers", params=params)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
async def _svc_get_creditors(*, limit: int = 50, offset: int = 0, search: str = None) -> Dict:
|
|
527
|
+
"""Serviço interno: buscar credores/fornecedores"""
|
|
528
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
529
|
+
if search:
|
|
530
|
+
params["search"] = search
|
|
531
|
+
|
|
532
|
+
return await make_sienge_request("GET", "/creditors", params=params)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
async def _svc_get_creditor_bank_info(*, creditor_id: str) -> Dict:
|
|
536
|
+
"""Serviço interno: informações bancárias de credor"""
|
|
537
|
+
return await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
async def _svc_get_projects(*, limit: int = 100, offset: int = 0, company_id: int = None,
|
|
541
|
+
enterprise_type: int = None, receivable_register: str = None,
|
|
542
|
+
only_buildings_enabled: bool = False) -> Dict:
|
|
543
|
+
"""Serviço interno: buscar empreendimentos/projetos"""
|
|
544
|
+
params = {"limit": min(limit or 100, 200), "offset": offset or 0}
|
|
545
|
+
|
|
546
|
+
if company_id:
|
|
547
|
+
params["company_id"] = company_id
|
|
548
|
+
if enterprise_type:
|
|
549
|
+
params["type"] = enterprise_type
|
|
550
|
+
if receivable_register:
|
|
551
|
+
params["receivable_register"] = receivable_register
|
|
552
|
+
if only_buildings_enabled:
|
|
553
|
+
params["only_buildings_enabled_for_integration"] = only_buildings_enabled
|
|
554
|
+
|
|
555
|
+
return await make_sienge_request("GET", "/enterprises", params=params)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
async def _svc_get_bills(*, start_date: str = None, end_date: str = None, creditor_id: str = None,
|
|
559
|
+
status: str = None, limit: int = 50) -> Dict:
|
|
560
|
+
"""Serviço interno: buscar títulos a pagar"""
|
|
561
|
+
# Se start_date não fornecido, usar últimos 30 dias
|
|
562
|
+
if not start_date:
|
|
563
|
+
start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
564
|
+
|
|
565
|
+
# Se end_date não fornecido, usar hoje
|
|
566
|
+
if not end_date:
|
|
567
|
+
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
568
|
+
|
|
569
|
+
params = {"start_date": start_date, "end_date": end_date, "limit": min(limit or 50, 200)}
|
|
570
|
+
|
|
571
|
+
if creditor_id:
|
|
572
|
+
params["creditor_id"] = creditor_id
|
|
573
|
+
if status:
|
|
574
|
+
params["status"] = status
|
|
575
|
+
|
|
576
|
+
return await make_sienge_request("GET", "/bills", params=params)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
async def _svc_get_accounts_receivable(*, start_date: str, end_date: str, selection_type: str = "D",
|
|
580
|
+
company_id: int = None, cost_centers_id: List[int] = None,
|
|
581
|
+
correction_indexer_id: int = None, correction_date: str = None,
|
|
582
|
+
change_start_date: str = None, completed_bills: str = None,
|
|
583
|
+
origins_ids: List[str] = None, bearers_id_in: List[int] = None,
|
|
584
|
+
bearers_id_not_in: List[int] = None) -> Dict:
|
|
585
|
+
"""Serviço interno: buscar contas a receber via bulk-data"""
|
|
586
|
+
params = {"start_date": start_date, "end_date": end_date, "selection_type": selection_type}
|
|
587
|
+
|
|
588
|
+
if company_id:
|
|
589
|
+
params["company_id"] = company_id
|
|
590
|
+
if cost_centers_id:
|
|
591
|
+
params["cost_centers_id"] = cost_centers_id
|
|
592
|
+
if correction_indexer_id:
|
|
593
|
+
params["correction_indexer_id"] = correction_indexer_id
|
|
594
|
+
if correction_date:
|
|
595
|
+
params["correction_date"] = correction_date
|
|
596
|
+
if change_start_date:
|
|
597
|
+
params["change_start_date"] = change_start_date
|
|
598
|
+
if completed_bills:
|
|
599
|
+
params["completed_bills"] = completed_bills
|
|
600
|
+
if origins_ids:
|
|
601
|
+
params["origins_ids"] = origins_ids
|
|
602
|
+
if bearers_id_in:
|
|
603
|
+
params["bearers_id_in"] = bearers_id_in
|
|
604
|
+
if bearers_id_not_in:
|
|
605
|
+
params["bearers_id_not_in"] = bearers_id_not_in
|
|
606
|
+
|
|
607
|
+
return await _fetch_bulk_with_polling("GET", "/income", params=params)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
async def _svc_get_accounts_receivable_by_bills(*, bills_ids: List[int], correction_indexer_id: int = None,
|
|
611
|
+
correction_date: str = None) -> Dict:
|
|
612
|
+
"""Serviço interno: buscar contas a receber por títulos específicos"""
|
|
613
|
+
params = {"bills_ids": bills_ids}
|
|
614
|
+
|
|
615
|
+
if correction_indexer_id:
|
|
616
|
+
params["correction_indexer_id"] = correction_indexer_id
|
|
617
|
+
if correction_date:
|
|
618
|
+
params["correction_date"] = correction_date
|
|
619
|
+
|
|
620
|
+
return await _fetch_bulk_with_polling("GET", "/income/by-bills", params=params)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
async def _svc_get_purchase_orders(*, purchase_order_id: str = None, status: str = None,
|
|
624
|
+
date_from: str = None, limit: int = 50) -> Dict:
|
|
625
|
+
"""Serviço interno: buscar pedidos de compra"""
|
|
626
|
+
if purchase_order_id:
|
|
627
|
+
return await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
|
|
628
|
+
|
|
629
|
+
params = {"limit": min(limit or 50, 200)}
|
|
630
|
+
if status:
|
|
631
|
+
params["status"] = status
|
|
632
|
+
if date_from:
|
|
633
|
+
params["date_from"] = date_from
|
|
634
|
+
|
|
635
|
+
return await make_sienge_request("GET", "/purchase-orders", params=params)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
async def _svc_get_purchase_requests(*, purchase_request_id: str = None, limit: int = 50, status: str = None) -> Dict:
|
|
639
|
+
"""Serviço interno: buscar solicitações de compra"""
|
|
640
|
+
if purchase_request_id:
|
|
641
|
+
return await make_sienge_request("GET", f"/purchase-requests/{purchase_request_id}")
|
|
642
|
+
|
|
643
|
+
params = {"limit": min(limit or 50, 200)}
|
|
644
|
+
if status:
|
|
645
|
+
params["status"] = status
|
|
646
|
+
|
|
647
|
+
return await make_sienge_request("GET", "/purchase-requests", params=params)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
async def _svc_get_purchase_invoices(*, limit: int = 50, date_from: str = None) -> Dict:
|
|
651
|
+
"""Serviço interno: listar notas fiscais de compra"""
|
|
652
|
+
params = {"limit": min(limit or 50, 200)}
|
|
653
|
+
if date_from:
|
|
654
|
+
params["date_from"] = date_from
|
|
655
|
+
|
|
656
|
+
return await make_sienge_request("GET", "/purchase-invoices", params=params)
|
|
657
|
+
|
|
658
|
+
|
|
180
659
|
# ============ CONEXÃO E TESTE ============
|
|
181
660
|
|
|
661
|
+
|
|
182
662
|
@mcp.tool
|
|
183
663
|
async def test_sienge_connection() -> Dict:
|
|
184
664
|
"""Testa a conexão com a API do Sienge"""
|
|
185
665
|
try:
|
|
186
|
-
#
|
|
187
|
-
result = await
|
|
188
|
-
|
|
666
|
+
# Usar serviço interno
|
|
667
|
+
result = await _svc_get_customer_types()
|
|
668
|
+
|
|
189
669
|
if result["success"]:
|
|
190
670
|
auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
|
|
191
671
|
return {
|
|
@@ -193,7 +673,10 @@ async def test_sienge_connection() -> Dict:
|
|
|
193
673
|
"message": "✅ Conexão com API do Sienge estabelecida com sucesso!",
|
|
194
674
|
"api_status": "Online",
|
|
195
675
|
"auth_method": auth_method,
|
|
196
|
-
"timestamp": datetime.now().isoformat()
|
|
676
|
+
"timestamp": datetime.now().isoformat(),
|
|
677
|
+
"request_id": result.get("request_id"),
|
|
678
|
+
"latency_ms": result.get("latency_ms"),
|
|
679
|
+
"cache": result.get("cache")
|
|
197
680
|
}
|
|
198
681
|
else:
|
|
199
682
|
return {
|
|
@@ -201,162 +684,391 @@ async def test_sienge_connection() -> Dict:
|
|
|
201
684
|
"message": "❌ Falha ao conectar com API do Sienge",
|
|
202
685
|
"error": result.get("error"),
|
|
203
686
|
"details": result.get("message"),
|
|
204
|
-
"timestamp": datetime.now().isoformat()
|
|
687
|
+
"timestamp": datetime.now().isoformat(),
|
|
688
|
+
"request_id": result.get("request_id")
|
|
205
689
|
}
|
|
206
690
|
except Exception as e:
|
|
207
691
|
return {
|
|
208
692
|
"success": False,
|
|
209
693
|
"message": "❌ Erro ao testar conexão",
|
|
210
694
|
"error": str(e),
|
|
211
|
-
"timestamp": datetime.now().isoformat()
|
|
695
|
+
"timestamp": datetime.now().isoformat(),
|
|
212
696
|
}
|
|
213
697
|
|
|
698
|
+
|
|
214
699
|
# ============ CLIENTES ============
|
|
215
700
|
|
|
701
|
+
|
|
216
702
|
@mcp.tool
|
|
217
|
-
async def get_sienge_customers(
|
|
703
|
+
async def get_sienge_customers(
|
|
704
|
+
limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None
|
|
705
|
+
) -> Dict:
|
|
218
706
|
"""
|
|
219
707
|
Busca clientes no Sienge com filtros
|
|
220
|
-
|
|
708
|
+
|
|
221
709
|
Args:
|
|
222
710
|
limit: Máximo de registros (padrão: 50)
|
|
223
711
|
offset: Pular registros (padrão: 0)
|
|
224
712
|
search: Buscar por nome ou documento
|
|
225
713
|
customer_type_id: Filtrar por tipo de cliente
|
|
226
714
|
"""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
715
|
+
# Usar serviço interno
|
|
716
|
+
result = await _svc_get_customers(
|
|
717
|
+
limit=limit or 50,
|
|
718
|
+
offset=offset or 0,
|
|
719
|
+
search=search,
|
|
720
|
+
customer_type_id=customer_type_id
|
|
721
|
+
)
|
|
722
|
+
|
|
236
723
|
if result["success"]:
|
|
237
724
|
data = result["data"]
|
|
238
725
|
customers = data.get("results", []) if isinstance(data, dict) else data
|
|
239
726
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
240
727
|
total_count = metadata.get("count", len(customers))
|
|
241
|
-
|
|
728
|
+
|
|
242
729
|
return {
|
|
243
730
|
"success": True,
|
|
244
731
|
"message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
|
|
245
732
|
"customers": customers,
|
|
246
733
|
"count": len(customers),
|
|
247
|
-
"
|
|
734
|
+
"total_count": total_count,
|
|
735
|
+
"filters_applied": {
|
|
736
|
+
"limit": limit, "offset": offset, "search": search, "customer_type_id": customer_type_id
|
|
737
|
+
},
|
|
738
|
+
"request_id": result.get("request_id"),
|
|
739
|
+
"latency_ms": result.get("latency_ms"),
|
|
740
|
+
"cache": result.get("cache")
|
|
248
741
|
}
|
|
249
|
-
|
|
742
|
+
|
|
250
743
|
return {
|
|
251
744
|
"success": False,
|
|
252
745
|
"message": "❌ Erro ao buscar clientes",
|
|
253
746
|
"error": result.get("error"),
|
|
254
|
-
"details": result.get("message")
|
|
747
|
+
"details": result.get("message"),
|
|
748
|
+
"request_id": result.get("request_id")
|
|
255
749
|
}
|
|
256
750
|
|
|
751
|
+
|
|
257
752
|
@mcp.tool
|
|
258
753
|
async def get_sienge_customer_types() -> Dict:
|
|
259
754
|
"""Lista tipos de clientes disponíveis"""
|
|
260
|
-
|
|
261
|
-
|
|
755
|
+
# Usar serviço interno
|
|
756
|
+
result = await _svc_get_customer_types()
|
|
757
|
+
|
|
262
758
|
if result["success"]:
|
|
263
759
|
data = result["data"]
|
|
264
760
|
customer_types = data.get("results", []) if isinstance(data, dict) else data
|
|
265
761
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
266
762
|
total_count = metadata.get("count", len(customer_types))
|
|
267
|
-
|
|
763
|
+
|
|
268
764
|
return {
|
|
269
765
|
"success": True,
|
|
270
766
|
"message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
|
|
271
767
|
"customer_types": customer_types,
|
|
272
|
-
"count": len(customer_types)
|
|
768
|
+
"count": len(customer_types),
|
|
769
|
+
"total_count": total_count,
|
|
770
|
+
"request_id": result.get("request_id"),
|
|
771
|
+
"latency_ms": result.get("latency_ms"),
|
|
772
|
+
"cache": result.get("cache")
|
|
273
773
|
}
|
|
274
|
-
|
|
774
|
+
|
|
275
775
|
return {
|
|
276
776
|
"success": False,
|
|
277
777
|
"message": "❌ Erro ao buscar tipos de clientes",
|
|
278
778
|
"error": result.get("error"),
|
|
279
|
-
"details": result.get("message")
|
|
779
|
+
"details": result.get("message"),
|
|
780
|
+
"request_id": result.get("request_id")
|
|
280
781
|
}
|
|
281
782
|
|
|
783
|
+
|
|
784
|
+
# ============ ALIAS COMPATÍVEIS COM CHECKLIST ============
|
|
785
|
+
|
|
786
|
+
@mcp.tool
|
|
787
|
+
async def get_sienge_enterprises(
|
|
788
|
+
limit: int = 100, offset: int = 0, company_id: int = None, enterprise_type: int = None
|
|
789
|
+
) -> Dict:
|
|
790
|
+
"""
|
|
791
|
+
ALIAS: get_sienge_projects → get_sienge_enterprises
|
|
792
|
+
Busca empreendimentos/obras (compatibilidade com checklist)
|
|
793
|
+
"""
|
|
794
|
+
return await get_sienge_projects(
|
|
795
|
+
limit=limit,
|
|
796
|
+
offset=offset,
|
|
797
|
+
company_id=company_id,
|
|
798
|
+
enterprise_type=enterprise_type
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
@mcp.tool
|
|
803
|
+
async def get_sienge_suppliers(
|
|
804
|
+
limit: int = 50,
|
|
805
|
+
offset: int = 0,
|
|
806
|
+
search: str = None
|
|
807
|
+
) -> Dict:
|
|
808
|
+
"""
|
|
809
|
+
ALIAS: get_sienge_creditors → get_sienge_suppliers
|
|
810
|
+
Busca fornecedores (compatibilidade com checklist)
|
|
811
|
+
"""
|
|
812
|
+
return await get_sienge_creditors(limit=limit, offset=offset, search=search)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@mcp.tool
|
|
816
|
+
async def search_sienge_finances(
|
|
817
|
+
period_start: str,
|
|
818
|
+
period_end: str,
|
|
819
|
+
account_type: Optional[str] = None,
|
|
820
|
+
cost_center: Optional[str] = None, # ignorado por enquanto (não suportado na API atual)
|
|
821
|
+
amount_filter: Optional[str] = None,
|
|
822
|
+
customer_creditor: Optional[str] = None
|
|
823
|
+
) -> Dict:
|
|
824
|
+
"""
|
|
825
|
+
ALIAS: search_sienge_financial_data → search_sienge_finances
|
|
826
|
+
- account_type: receivable | payable | both
|
|
827
|
+
- amount_filter: "100..500", ">=1000", "<=500", ">100", "<200", "=750"
|
|
828
|
+
- customer_creditor: termo de busca (cliente/credor)
|
|
829
|
+
"""
|
|
830
|
+
# 1) mapear tipo
|
|
831
|
+
search_type = (account_type or "both").lower()
|
|
832
|
+
if search_type not in {"receivable", "payable", "both"}:
|
|
833
|
+
search_type = "both"
|
|
834
|
+
|
|
835
|
+
# 2) parse de faixa de valores
|
|
836
|
+
amount_min = amount_max = None
|
|
837
|
+
if amount_filter:
|
|
838
|
+
s = amount_filter.replace(" ", "")
|
|
839
|
+
try:
|
|
840
|
+
if ".." in s:
|
|
841
|
+
lo, hi = s.split("..", 1)
|
|
842
|
+
amount_min = float(lo) if lo else None
|
|
843
|
+
amount_max = float(hi) if hi else None
|
|
844
|
+
elif s.startswith(">="):
|
|
845
|
+
amount_min = float(s[2:])
|
|
846
|
+
elif s.startswith("<="):
|
|
847
|
+
amount_max = float(s[2:])
|
|
848
|
+
elif s.startswith(">"):
|
|
849
|
+
# >x → min = x (estrito não suportado; aproximamos)
|
|
850
|
+
amount_min = float(s[1:])
|
|
851
|
+
elif s.startswith("<"):
|
|
852
|
+
amount_max = float(s[1:])
|
|
853
|
+
elif s.startswith("="):
|
|
854
|
+
v = float(s[1:])
|
|
855
|
+
amount_min = v
|
|
856
|
+
amount_max = v
|
|
857
|
+
else:
|
|
858
|
+
# número puro → min
|
|
859
|
+
amount_min = float(s)
|
|
860
|
+
except ValueError:
|
|
861
|
+
# filtro inválido → ignora silenciosamente
|
|
862
|
+
amount_min = amount_max = None
|
|
863
|
+
|
|
864
|
+
return await search_sienge_financial_data(
|
|
865
|
+
period_start=period_start,
|
|
866
|
+
period_end=period_end,
|
|
867
|
+
search_type=search_type,
|
|
868
|
+
amount_min=amount_min,
|
|
869
|
+
amount_max=amount_max,
|
|
870
|
+
customer_creditor_search=customer_creditor
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
@mcp.tool
|
|
875
|
+
async def get_sienge_accounts_payable(
|
|
876
|
+
start_date: str = None, end_date: str = None, creditor_id: str = None,
|
|
877
|
+
status: str = None, limit: int = 50
|
|
878
|
+
) -> Dict:
|
|
879
|
+
"""
|
|
880
|
+
ALIAS: get_sienge_bills → get_sienge_accounts_payable
|
|
881
|
+
Busca contas a pagar (compatibilidade com checklist)
|
|
882
|
+
"""
|
|
883
|
+
return await get_sienge_bills(
|
|
884
|
+
start_date=start_date,
|
|
885
|
+
end_date=end_date,
|
|
886
|
+
creditor_id=creditor_id,
|
|
887
|
+
status=status,
|
|
888
|
+
limit=limit
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@mcp.tool
|
|
893
|
+
async def list_sienge_purchase_invoices(limit: int = 50, date_from: str = None) -> Dict:
|
|
894
|
+
"""
|
|
895
|
+
Lista notas fiscais de compra (versão list/plural esperada pelo checklist)
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
899
|
+
date_from: Data inicial (YYYY-MM-DD)
|
|
900
|
+
"""
|
|
901
|
+
# Usar serviço interno
|
|
902
|
+
result = await _svc_get_purchase_invoices(limit=limit, date_from=date_from)
|
|
903
|
+
|
|
904
|
+
if result["success"]:
|
|
905
|
+
data = result["data"]
|
|
906
|
+
invoices = data.get("results", []) if isinstance(data, dict) else data
|
|
907
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
908
|
+
total_count = metadata.get("count", len(invoices))
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
"success": True,
|
|
912
|
+
"message": f"✅ Encontradas {len(invoices)} notas fiscais de compra (total: {total_count})",
|
|
913
|
+
"purchase_invoices": invoices,
|
|
914
|
+
"count": len(invoices),
|
|
915
|
+
"total_count": total_count,
|
|
916
|
+
"filters_applied": {"limit": limit, "date_from": date_from},
|
|
917
|
+
"request_id": result.get("request_id"),
|
|
918
|
+
"latency_ms": result.get("latency_ms"),
|
|
919
|
+
"cache": result.get("cache")
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
"success": False,
|
|
924
|
+
"message": "❌ Erro ao buscar notas fiscais de compra",
|
|
925
|
+
"error": result.get("error"),
|
|
926
|
+
"details": result.get("message"),
|
|
927
|
+
"request_id": result.get("request_id")
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
@mcp.tool
|
|
932
|
+
async def list_sienge_purchase_requests(limit: int = 50, status: str = None) -> Dict:
|
|
933
|
+
"""
|
|
934
|
+
Lista solicitações de compra (versão list/plural esperada pelo checklist)
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
938
|
+
status: Status da solicitação
|
|
939
|
+
"""
|
|
940
|
+
# Usar serviço interno
|
|
941
|
+
result = await _svc_get_purchase_requests(limit=limit, status=status)
|
|
942
|
+
|
|
943
|
+
if result["success"]:
|
|
944
|
+
data = result["data"]
|
|
945
|
+
requests = data.get("results", []) if isinstance(data, dict) else data
|
|
946
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
947
|
+
total_count = metadata.get("count", len(requests))
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
"success": True,
|
|
951
|
+
"message": f"✅ Encontradas {len(requests)} solicitações de compra (total: {total_count})",
|
|
952
|
+
"purchase_requests": requests,
|
|
953
|
+
"count": len(requests),
|
|
954
|
+
"total_count": total_count,
|
|
955
|
+
"filters_applied": {"limit": limit, "status": status},
|
|
956
|
+
"request_id": result.get("request_id"),
|
|
957
|
+
"latency_ms": result.get("latency_ms"),
|
|
958
|
+
"cache": result.get("cache")
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
"success": False,
|
|
963
|
+
"message": "❌ Erro ao buscar solicitações de compra",
|
|
964
|
+
"error": result.get("error"),
|
|
965
|
+
"details": result.get("message"),
|
|
966
|
+
"request_id": result.get("request_id")
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
|
|
282
970
|
# ============ CREDORES ============
|
|
283
971
|
|
|
972
|
+
|
|
284
973
|
@mcp.tool
|
|
285
974
|
async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None) -> Dict:
|
|
286
975
|
"""
|
|
287
976
|
Busca credores/fornecedores
|
|
288
|
-
|
|
977
|
+
|
|
289
978
|
Args:
|
|
290
979
|
limit: Máximo de registros (padrão: 50)
|
|
291
980
|
offset: Pular registros (padrão: 0)
|
|
292
981
|
search: Buscar por nome
|
|
293
982
|
"""
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
983
|
+
# Usar serviço interno
|
|
984
|
+
result = await _svc_get_creditors(
|
|
985
|
+
limit=limit or 50,
|
|
986
|
+
offset=offset or 0,
|
|
987
|
+
search=search
|
|
988
|
+
)
|
|
989
|
+
|
|
300
990
|
if result["success"]:
|
|
301
991
|
data = result["data"]
|
|
302
992
|
creditors = data.get("results", []) if isinstance(data, dict) else data
|
|
303
993
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
304
994
|
total_count = metadata.get("count", len(creditors))
|
|
305
|
-
|
|
995
|
+
|
|
306
996
|
return {
|
|
307
997
|
"success": True,
|
|
308
998
|
"message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
|
|
309
999
|
"creditors": creditors,
|
|
310
|
-
"count": len(creditors)
|
|
1000
|
+
"count": len(creditors),
|
|
1001
|
+
"total_count": total_count,
|
|
1002
|
+
"filters_applied": {"limit": limit, "offset": offset, "search": search},
|
|
1003
|
+
"request_id": result.get("request_id"),
|
|
1004
|
+
"latency_ms": result.get("latency_ms"),
|
|
1005
|
+
"cache": result.get("cache")
|
|
311
1006
|
}
|
|
312
|
-
|
|
1007
|
+
|
|
313
1008
|
return {
|
|
314
1009
|
"success": False,
|
|
315
1010
|
"message": "❌ Erro ao buscar credores",
|
|
316
1011
|
"error": result.get("error"),
|
|
317
|
-
"details": result.get("message")
|
|
1012
|
+
"details": result.get("message"),
|
|
1013
|
+
"request_id": result.get("request_id")
|
|
318
1014
|
}
|
|
319
1015
|
|
|
1016
|
+
|
|
320
1017
|
@mcp.tool
|
|
321
1018
|
async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
|
|
322
1019
|
"""
|
|
323
1020
|
Consulta informações bancárias de um credor
|
|
324
|
-
|
|
1021
|
+
|
|
325
1022
|
Args:
|
|
326
1023
|
creditor_id: ID do credor (obrigatório)
|
|
327
1024
|
"""
|
|
328
|
-
|
|
329
|
-
|
|
1025
|
+
# Usar serviço interno
|
|
1026
|
+
result = await _svc_get_creditor_bank_info(creditor_id=creditor_id)
|
|
1027
|
+
|
|
330
1028
|
if result["success"]:
|
|
331
1029
|
return {
|
|
332
1030
|
"success": True,
|
|
333
1031
|
"message": f"✅ Informações bancárias do credor {creditor_id}",
|
|
334
1032
|
"creditor_id": creditor_id,
|
|
335
|
-
"bank_info": result["data"]
|
|
1033
|
+
"bank_info": result["data"],
|
|
1034
|
+
"request_id": result.get("request_id"),
|
|
1035
|
+
"latency_ms": result.get("latency_ms")
|
|
336
1036
|
}
|
|
337
|
-
|
|
1037
|
+
|
|
338
1038
|
return {
|
|
339
1039
|
"success": False,
|
|
340
1040
|
"message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
|
|
341
1041
|
"error": result.get("error"),
|
|
342
|
-
"details": result.get("message")
|
|
1042
|
+
"details": result.get("message"),
|
|
1043
|
+
"request_id": result.get("request_id")
|
|
343
1044
|
}
|
|
344
1045
|
|
|
1046
|
+
|
|
345
1047
|
# ============ FINANCEIRO ============
|
|
346
1048
|
|
|
1049
|
+
|
|
347
1050
|
@mcp.tool
|
|
348
|
-
async def get_sienge_accounts_receivable(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1051
|
+
async def get_sienge_accounts_receivable(
|
|
1052
|
+
start_date: str,
|
|
1053
|
+
end_date: str,
|
|
1054
|
+
selection_type: str = "D",
|
|
1055
|
+
company_id: Optional[int] = None,
|
|
1056
|
+
cost_centers_id: Optional[List[int]] = None,
|
|
1057
|
+
correction_indexer_id: Optional[int] = None,
|
|
1058
|
+
correction_date: Optional[str] = None,
|
|
1059
|
+
change_start_date: Optional[str] = None,
|
|
1060
|
+
completed_bills: Optional[str] = None,
|
|
1061
|
+
origins_ids: Optional[List[str]] = None,
|
|
1062
|
+
bearers_id_in: Optional[List[int]] = None,
|
|
1063
|
+
bearers_id_not_in: Optional[List[int]] = None,
|
|
1064
|
+
) -> Dict:
|
|
354
1065
|
"""
|
|
355
1066
|
Consulta parcelas do contas a receber via API bulk-data
|
|
356
|
-
|
|
1067
|
+
MELHORADO: Suporte a polling assíncrono para requests 202
|
|
1068
|
+
|
|
357
1069
|
Args:
|
|
358
1070
|
start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
|
|
359
|
-
end_date: Data do fim do período (YYYY-MM-DD) - OBRIGATÓRIO
|
|
1071
|
+
end_date: Data do fim do período (YYYY-MM-DD) - OBRIGATÓRIO
|
|
360
1072
|
selection_type: Seleção da data do período (I=emissão, D=vencimento, P=pagamento, B=competência) - padrão: D
|
|
361
1073
|
company_id: Código da empresa
|
|
362
1074
|
cost_centers_id: Lista de códigos de centro de custo
|
|
@@ -368,103 +1080,135 @@ async def get_sienge_accounts_receivable(start_date: str, end_date: str, selecti
|
|
|
368
1080
|
bearers_id_in: Filtrar parcelas com códigos de portador específicos
|
|
369
1081
|
bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
|
|
370
1082
|
"""
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if completed_bills:
|
|
388
|
-
params["completedBills"] = completed_bills
|
|
389
|
-
if origins_ids:
|
|
390
|
-
params["originsIds"] = origins_ids
|
|
391
|
-
if bearers_id_in:
|
|
392
|
-
params["bearersIdIn"] = bearers_id_in
|
|
393
|
-
if bearers_id_not_in:
|
|
394
|
-
params["bearersIdNotIn"] = bearers_id_not_in
|
|
395
|
-
|
|
396
|
-
result = await make_sienge_bulk_request("GET", "/income", params=params)
|
|
397
|
-
|
|
1083
|
+
# Usar serviço interno com polling assíncrono
|
|
1084
|
+
result = await _svc_get_accounts_receivable(
|
|
1085
|
+
start_date=start_date,
|
|
1086
|
+
end_date=end_date,
|
|
1087
|
+
selection_type=selection_type,
|
|
1088
|
+
company_id=company_id,
|
|
1089
|
+
cost_centers_id=cost_centers_id,
|
|
1090
|
+
correction_indexer_id=correction_indexer_id,
|
|
1091
|
+
correction_date=correction_date,
|
|
1092
|
+
change_start_date=change_start_date,
|
|
1093
|
+
completed_bills=completed_bills,
|
|
1094
|
+
origins_ids=origins_ids,
|
|
1095
|
+
bearers_id_in=bearers_id_in,
|
|
1096
|
+
bearers_id_not_in=bearers_id_not_in
|
|
1097
|
+
)
|
|
1098
|
+
|
|
398
1099
|
if result["success"]:
|
|
399
|
-
|
|
400
|
-
income_data =
|
|
1100
|
+
# Para requests normais (200) e assíncronos processados
|
|
1101
|
+
income_data = result.get("data", [])
|
|
401
1102
|
|
|
402
|
-
|
|
1103
|
+
response = {
|
|
403
1104
|
"success": True,
|
|
404
1105
|
"message": f"✅ Encontradas {len(income_data)} parcelas a receber",
|
|
405
1106
|
"income_data": income_data,
|
|
406
1107
|
"count": len(income_data),
|
|
407
1108
|
"period": f"{start_date} a {end_date}",
|
|
408
1109
|
"selection_type": selection_type,
|
|
409
|
-
"
|
|
1110
|
+
"request_id": result.get("request_id"),
|
|
1111
|
+
"latency_ms": result.get("latency_ms")
|
|
410
1112
|
}
|
|
411
|
-
|
|
1113
|
+
|
|
1114
|
+
# Se foi processamento assíncrono, incluir informações extras
|
|
1115
|
+
if result.get("async_identifier"):
|
|
1116
|
+
response.update({
|
|
1117
|
+
"async_processing": {
|
|
1118
|
+
"identifier": result.get("async_identifier"),
|
|
1119
|
+
"correlation_id": result.get("correlation_id"),
|
|
1120
|
+
"chunks_downloaded": result.get("chunks_downloaded"),
|
|
1121
|
+
"rows_returned": result.get("rows_returned"),
|
|
1122
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1123
|
+
}
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
return response
|
|
1127
|
+
|
|
412
1128
|
return {
|
|
413
1129
|
"success": False,
|
|
414
1130
|
"message": "❌ Erro ao buscar parcelas a receber",
|
|
415
1131
|
"error": result.get("error"),
|
|
416
|
-
"details": result.get("message")
|
|
1132
|
+
"details": result.get("message"),
|
|
1133
|
+
"request_id": result.get("request_id"),
|
|
1134
|
+
"async_info": {
|
|
1135
|
+
"identifier": result.get("async_identifier"),
|
|
1136
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1137
|
+
} if result.get("async_identifier") else None
|
|
417
1138
|
}
|
|
418
1139
|
|
|
1140
|
+
|
|
419
1141
|
@mcp.tool
|
|
420
|
-
async def get_sienge_accounts_receivable_by_bills(
|
|
421
|
-
|
|
1142
|
+
async def get_sienge_accounts_receivable_by_bills(
|
|
1143
|
+
bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None
|
|
1144
|
+
) -> Dict:
|
|
422
1145
|
"""
|
|
423
1146
|
Consulta parcelas dos títulos informados via API bulk-data
|
|
424
|
-
|
|
1147
|
+
MELHORADO: Suporte a polling assíncrono para requests 202
|
|
1148
|
+
|
|
425
1149
|
Args:
|
|
426
1150
|
bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
|
|
427
1151
|
correction_indexer_id: Código do indexador de correção
|
|
428
1152
|
correction_date: Data para correção do indexador (YYYY-MM-DD)
|
|
429
1153
|
"""
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
params["correctionDate"] = correction_date
|
|
438
|
-
|
|
439
|
-
result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
|
|
440
|
-
|
|
1154
|
+
# Usar serviço interno com polling assíncrono
|
|
1155
|
+
result = await _svc_get_accounts_receivable_by_bills(
|
|
1156
|
+
bills_ids=bills_ids,
|
|
1157
|
+
correction_indexer_id=correction_indexer_id,
|
|
1158
|
+
correction_date=correction_date
|
|
1159
|
+
)
|
|
1160
|
+
|
|
441
1161
|
if result["success"]:
|
|
442
|
-
|
|
443
|
-
income_data = data.get("data", []) if isinstance(data, dict) else data
|
|
1162
|
+
income_data = result.get("data", [])
|
|
444
1163
|
|
|
445
|
-
|
|
1164
|
+
response = {
|
|
446
1165
|
"success": True,
|
|
447
1166
|
"message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
|
|
448
1167
|
"income_data": income_data,
|
|
449
1168
|
"count": len(income_data),
|
|
450
1169
|
"bills_consulted": bills_ids,
|
|
451
|
-
"
|
|
1170
|
+
"request_id": result.get("request_id"),
|
|
1171
|
+
"latency_ms": result.get("latency_ms")
|
|
452
1172
|
}
|
|
453
|
-
|
|
1173
|
+
|
|
1174
|
+
# Se foi processamento assíncrono, incluir informações extras
|
|
1175
|
+
if result.get("async_identifier"):
|
|
1176
|
+
response.update({
|
|
1177
|
+
"async_processing": {
|
|
1178
|
+
"identifier": result.get("async_identifier"),
|
|
1179
|
+
"correlation_id": result.get("correlation_id"),
|
|
1180
|
+
"chunks_downloaded": result.get("chunks_downloaded"),
|
|
1181
|
+
"rows_returned": result.get("rows_returned"),
|
|
1182
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1183
|
+
}
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
return response
|
|
1187
|
+
|
|
454
1188
|
return {
|
|
455
1189
|
"success": False,
|
|
456
1190
|
"message": "❌ Erro ao buscar parcelas dos títulos informados",
|
|
457
1191
|
"error": result.get("error"),
|
|
458
|
-
"details": result.get("message")
|
|
1192
|
+
"details": result.get("message"),
|
|
1193
|
+
"request_id": result.get("request_id"),
|
|
1194
|
+
"async_info": {
|
|
1195
|
+
"identifier": result.get("async_identifier"),
|
|
1196
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1197
|
+
} if result.get("async_identifier") else None
|
|
459
1198
|
}
|
|
460
1199
|
|
|
1200
|
+
|
|
461
1201
|
@mcp.tool
|
|
462
|
-
async def get_sienge_bills(
|
|
463
|
-
|
|
464
|
-
|
|
1202
|
+
async def get_sienge_bills(
|
|
1203
|
+
start_date: Optional[str] = None,
|
|
1204
|
+
end_date: Optional[str] = None,
|
|
1205
|
+
creditor_id: Optional[str] = None,
|
|
1206
|
+
status: Optional[str] = None,
|
|
1207
|
+
limit: Optional[int] = 50,
|
|
1208
|
+
) -> Dict:
|
|
465
1209
|
"""
|
|
466
1210
|
Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
|
|
467
|
-
|
|
1211
|
+
|
|
468
1212
|
Args:
|
|
469
1213
|
start_date: Data inicial obrigatória (YYYY-MM-DD) - padrão últimos 30 dias
|
|
470
1214
|
end_date: Data final (YYYY-MM-DD) - padrão hoje
|
|
@@ -472,62 +1216,72 @@ async def get_sienge_bills(start_date: Optional[str] = None, end_date: Optional[
|
|
|
472
1216
|
status: Status do título (ex: open, paid, cancelled)
|
|
473
1217
|
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
474
1218
|
"""
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
# Parâmetros obrigatórios
|
|
486
|
-
params = {
|
|
487
|
-
"startDate": start_date, # OBRIGATÓRIO pela API
|
|
488
|
-
"endDate": end_date,
|
|
489
|
-
"limit": min(limit or 50, 200)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
# Parâmetros opcionais
|
|
493
|
-
if creditor_id:
|
|
494
|
-
params["creditor_id"] = creditor_id
|
|
495
|
-
if status:
|
|
496
|
-
params["status"] = status
|
|
497
|
-
|
|
498
|
-
result = await make_sienge_request("GET", "/bills", params=params)
|
|
499
|
-
|
|
1219
|
+
# Usar serviço interno
|
|
1220
|
+
result = await _svc_get_bills(
|
|
1221
|
+
start_date=start_date,
|
|
1222
|
+
end_date=end_date,
|
|
1223
|
+
creditor_id=creditor_id,
|
|
1224
|
+
status=status,
|
|
1225
|
+
limit=limit or 50
|
|
1226
|
+
)
|
|
1227
|
+
|
|
500
1228
|
if result["success"]:
|
|
501
1229
|
data = result["data"]
|
|
502
1230
|
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
503
1231
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
504
1232
|
total_count = metadata.get("count", len(bills))
|
|
505
|
-
|
|
1233
|
+
|
|
1234
|
+
# Aplicar parsing numérico nos valores
|
|
1235
|
+
for bill in bills:
|
|
1236
|
+
if "amount" in bill:
|
|
1237
|
+
bill["amount"] = _parse_numeric_value(bill["amount"])
|
|
1238
|
+
if "paid_amount" in bill:
|
|
1239
|
+
bill["paid_amount"] = _parse_numeric_value(bill["paid_amount"])
|
|
1240
|
+
if "remaining_amount" in bill:
|
|
1241
|
+
bill["remaining_amount"] = _parse_numeric_value(bill["remaining_amount"])
|
|
1242
|
+
|
|
1243
|
+
# Usar datas padrão se não fornecidas
|
|
1244
|
+
actual_start = start_date or (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
1245
|
+
actual_end = end_date or datetime.now().strftime("%Y-%m-%d")
|
|
1246
|
+
|
|
506
1247
|
return {
|
|
507
1248
|
"success": True,
|
|
508
|
-
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {
|
|
1249
|
+
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {actual_start} a {actual_end}",
|
|
509
1250
|
"bills": bills,
|
|
510
1251
|
"count": len(bills),
|
|
511
1252
|
"total_count": total_count,
|
|
512
|
-
"period": {"start_date":
|
|
513
|
-
"
|
|
1253
|
+
"period": {"start_date": actual_start, "end_date": actual_end},
|
|
1254
|
+
"filters_applied": {
|
|
1255
|
+
"start_date": actual_start, "end_date": actual_end,
|
|
1256
|
+
"creditor_id": creditor_id, "status": status, "limit": limit
|
|
1257
|
+
},
|
|
1258
|
+
"request_id": result.get("request_id"),
|
|
1259
|
+
"latency_ms": result.get("latency_ms"),
|
|
1260
|
+
"cache": result.get("cache")
|
|
514
1261
|
}
|
|
515
|
-
|
|
1262
|
+
|
|
516
1263
|
return {
|
|
517
1264
|
"success": False,
|
|
518
1265
|
"message": "❌ Erro ao buscar títulos a pagar",
|
|
519
1266
|
"error": result.get("error"),
|
|
520
|
-
"details": result.get("message")
|
|
1267
|
+
"details": result.get("message"),
|
|
1268
|
+
"request_id": result.get("request_id")
|
|
521
1269
|
}
|
|
522
1270
|
|
|
1271
|
+
|
|
523
1272
|
# ============ COMPRAS ============
|
|
524
1273
|
|
|
1274
|
+
|
|
525
1275
|
@mcp.tool
|
|
526
|
-
async def get_sienge_purchase_orders(
|
|
527
|
-
|
|
1276
|
+
async def get_sienge_purchase_orders(
|
|
1277
|
+
purchase_order_id: Optional[str] = None,
|
|
1278
|
+
status: Optional[str] = None,
|
|
1279
|
+
date_from: Optional[str] = None,
|
|
1280
|
+
limit: Optional[int] = 50,
|
|
1281
|
+
) -> Dict:
|
|
528
1282
|
"""
|
|
529
1283
|
Consulta pedidos de compra
|
|
530
|
-
|
|
1284
|
+
|
|
531
1285
|
Args:
|
|
532
1286
|
purchase_order_id: ID específico do pedido
|
|
533
1287
|
status: Status do pedido
|
|
@@ -537,73 +1291,75 @@ async def get_sienge_purchase_orders(purchase_order_id: Optional[str] = None, st
|
|
|
537
1291
|
if purchase_order_id:
|
|
538
1292
|
result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
|
|
539
1293
|
if result["success"]:
|
|
540
|
-
return {
|
|
541
|
-
"success": True,
|
|
542
|
-
"message": f"✅ Pedido {purchase_order_id} encontrado",
|
|
543
|
-
"purchase_order": result["data"]
|
|
544
|
-
}
|
|
1294
|
+
return {"success": True, "message": f"✅ Pedido {purchase_order_id} encontrado", "purchase_order": result["data"]}
|
|
545
1295
|
return result
|
|
546
|
-
|
|
1296
|
+
|
|
547
1297
|
params = {"limit": min(limit or 50, 200)}
|
|
548
1298
|
if status:
|
|
549
1299
|
params["status"] = status
|
|
550
1300
|
if date_from:
|
|
551
1301
|
params["date_from"] = date_from
|
|
552
|
-
|
|
1302
|
+
|
|
553
1303
|
result = await make_sienge_request("GET", "/purchase-orders", params=params)
|
|
554
|
-
|
|
1304
|
+
|
|
555
1305
|
if result["success"]:
|
|
556
1306
|
data = result["data"]
|
|
557
1307
|
orders = data.get("results", []) if isinstance(data, dict) else data
|
|
558
|
-
|
|
1308
|
+
|
|
559
1309
|
return {
|
|
560
1310
|
"success": True,
|
|
561
1311
|
"message": f"✅ Encontrados {len(orders)} pedidos de compra",
|
|
562
1312
|
"purchase_orders": orders,
|
|
563
|
-
"count": len(orders)
|
|
1313
|
+
"count": len(orders),
|
|
1314
|
+
"request_id": result.get("request_id"),
|
|
1315
|
+
"latency_ms": result.get("latency_ms")
|
|
564
1316
|
}
|
|
565
|
-
|
|
1317
|
+
|
|
566
1318
|
return {
|
|
567
1319
|
"success": False,
|
|
568
1320
|
"message": "❌ Erro ao buscar pedidos de compra",
|
|
569
1321
|
"error": result.get("error"),
|
|
570
|
-
"details": result.get("message")
|
|
1322
|
+
"details": result.get("message"),
|
|
1323
|
+
"request_id": result.get("request_id"),
|
|
1324
|
+
"latency_ms": result.get("latency_ms")
|
|
571
1325
|
}
|
|
572
1326
|
|
|
1327
|
+
|
|
573
1328
|
@mcp.tool
|
|
574
1329
|
async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
|
|
575
1330
|
"""
|
|
576
1331
|
Consulta itens de um pedido de compra específico
|
|
577
|
-
|
|
1332
|
+
|
|
578
1333
|
Args:
|
|
579
1334
|
purchase_order_id: ID do pedido (obrigatório)
|
|
580
1335
|
"""
|
|
581
1336
|
result = await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}/items")
|
|
582
|
-
|
|
1337
|
+
|
|
583
1338
|
if result["success"]:
|
|
584
1339
|
data = result["data"]
|
|
585
1340
|
items = data.get("results", []) if isinstance(data, dict) else data
|
|
586
|
-
|
|
1341
|
+
|
|
587
1342
|
return {
|
|
588
1343
|
"success": True,
|
|
589
1344
|
"message": f"✅ Encontrados {len(items)} itens no pedido {purchase_order_id}",
|
|
590
1345
|
"purchase_order_id": purchase_order_id,
|
|
591
1346
|
"items": items,
|
|
592
|
-
"count": len(items)
|
|
1347
|
+
"count": len(items),
|
|
593
1348
|
}
|
|
594
|
-
|
|
1349
|
+
|
|
595
1350
|
return {
|
|
596
1351
|
"success": False,
|
|
597
1352
|
"message": f"❌ Erro ao buscar itens do pedido {purchase_order_id}",
|
|
598
1353
|
"error": result.get("error"),
|
|
599
|
-
"details": result.get("message")
|
|
1354
|
+
"details": result.get("message"),
|
|
600
1355
|
}
|
|
601
1356
|
|
|
1357
|
+
|
|
602
1358
|
@mcp.tool
|
|
603
1359
|
async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
|
|
604
1360
|
"""
|
|
605
1361
|
Consulta solicitações de compra
|
|
606
|
-
|
|
1362
|
+
|
|
607
1363
|
Args:
|
|
608
1364
|
purchase_request_id: ID específico da solicitação
|
|
609
1365
|
limit: Máximo de registros
|
|
@@ -614,36 +1370,37 @@ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None
|
|
|
614
1370
|
return {
|
|
615
1371
|
"success": True,
|
|
616
1372
|
"message": f"✅ Solicitação {purchase_request_id} encontrada",
|
|
617
|
-
"purchase_request": result["data"]
|
|
1373
|
+
"purchase_request": result["data"],
|
|
618
1374
|
}
|
|
619
1375
|
return result
|
|
620
|
-
|
|
1376
|
+
|
|
621
1377
|
params = {"limit": min(limit or 50, 200)}
|
|
622
1378
|
result = await make_sienge_request("GET", "/purchase-requests", params=params)
|
|
623
|
-
|
|
1379
|
+
|
|
624
1380
|
if result["success"]:
|
|
625
1381
|
data = result["data"]
|
|
626
1382
|
requests = data.get("results", []) if isinstance(data, dict) else data
|
|
627
|
-
|
|
1383
|
+
|
|
628
1384
|
return {
|
|
629
1385
|
"success": True,
|
|
630
1386
|
"message": f"✅ Encontradas {len(requests)} solicitações de compra",
|
|
631
1387
|
"purchase_requests": requests,
|
|
632
|
-
"count": len(requests)
|
|
1388
|
+
"count": len(requests),
|
|
633
1389
|
}
|
|
634
|
-
|
|
1390
|
+
|
|
635
1391
|
return {
|
|
636
1392
|
"success": False,
|
|
637
1393
|
"message": "❌ Erro ao buscar solicitações de compra",
|
|
638
1394
|
"error": result.get("error"),
|
|
639
|
-
"details": result.get("message")
|
|
1395
|
+
"details": result.get("message"),
|
|
640
1396
|
}
|
|
641
1397
|
|
|
1398
|
+
|
|
642
1399
|
@mcp.tool
|
|
643
1400
|
async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]) -> Dict:
|
|
644
1401
|
"""
|
|
645
1402
|
Cria nova solicitação de compra
|
|
646
|
-
|
|
1403
|
+
|
|
647
1404
|
Args:
|
|
648
1405
|
description: Descrição da solicitação
|
|
649
1406
|
project_id: ID do projeto/obra
|
|
@@ -653,89 +1410,102 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
653
1410
|
"description": description,
|
|
654
1411
|
"project_id": project_id,
|
|
655
1412
|
"items": items,
|
|
656
|
-
"date": datetime.now().strftime("%Y-%m-%d")
|
|
1413
|
+
"date": datetime.now().strftime("%Y-%m-%d"),
|
|
657
1414
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
1415
|
+
|
|
1416
|
+
# CORRIGIDO: Normalizar JSON payload
|
|
1417
|
+
json_data = to_camel_json(request_data)
|
|
1418
|
+
result = await make_sienge_request("POST", "/purchase-requests", json_data=json_data)
|
|
1419
|
+
|
|
661
1420
|
if result["success"]:
|
|
662
1421
|
return {
|
|
663
1422
|
"success": True,
|
|
664
1423
|
"message": "✅ Solicitação de compra criada com sucesso",
|
|
665
|
-
"request_id": result
|
|
666
|
-
"
|
|
1424
|
+
"request_id": result.get("request_id"),
|
|
1425
|
+
"purchase_request_id": result["data"].get("id"),
|
|
1426
|
+
"data": result["data"],
|
|
1427
|
+
"latency_ms": result.get("latency_ms")
|
|
667
1428
|
}
|
|
668
|
-
|
|
1429
|
+
|
|
669
1430
|
return {
|
|
670
1431
|
"success": False,
|
|
671
1432
|
"message": "❌ Erro ao criar solicitação de compra",
|
|
672
1433
|
"error": result.get("error"),
|
|
673
|
-
"details": result.get("message")
|
|
1434
|
+
"details": result.get("message"),
|
|
1435
|
+
"request_id": result.get("request_id")
|
|
674
1436
|
}
|
|
675
1437
|
|
|
1438
|
+
|
|
676
1439
|
# ============ NOTAS FISCAIS DE COMPRA ============
|
|
677
1440
|
|
|
1441
|
+
|
|
678
1442
|
@mcp.tool
|
|
679
1443
|
async def get_sienge_purchase_invoice(sequential_number: int) -> Dict:
|
|
680
1444
|
"""
|
|
681
1445
|
Consulta nota fiscal de compra por número sequencial
|
|
682
|
-
|
|
1446
|
+
|
|
683
1447
|
Args:
|
|
684
1448
|
sequential_number: Número sequencial da nota fiscal
|
|
685
1449
|
"""
|
|
686
1450
|
result = await make_sienge_request("GET", f"/purchase-invoices/{sequential_number}")
|
|
687
|
-
|
|
1451
|
+
|
|
688
1452
|
if result["success"]:
|
|
689
|
-
return {
|
|
690
|
-
|
|
691
|
-
"message": f"✅ Nota fiscal {sequential_number} encontrada",
|
|
692
|
-
"invoice": result["data"]
|
|
693
|
-
}
|
|
694
|
-
|
|
1453
|
+
return {"success": True, "message": f"✅ Nota fiscal {sequential_number} encontrada", "invoice": result["data"]}
|
|
1454
|
+
|
|
695
1455
|
return {
|
|
696
1456
|
"success": False,
|
|
697
1457
|
"message": f"❌ Erro ao buscar nota fiscal {sequential_number}",
|
|
698
1458
|
"error": result.get("error"),
|
|
699
|
-
"details": result.get("message")
|
|
1459
|
+
"details": result.get("message"),
|
|
700
1460
|
}
|
|
701
1461
|
|
|
1462
|
+
|
|
702
1463
|
@mcp.tool
|
|
703
1464
|
async def get_sienge_purchase_invoice_items(sequential_number: int) -> Dict:
|
|
704
1465
|
"""
|
|
705
1466
|
Consulta itens de uma nota fiscal de compra
|
|
706
|
-
|
|
1467
|
+
|
|
707
1468
|
Args:
|
|
708
1469
|
sequential_number: Número sequencial da nota fiscal
|
|
709
1470
|
"""
|
|
710
1471
|
result = await make_sienge_request("GET", f"/purchase-invoices/{sequential_number}/items")
|
|
711
|
-
|
|
1472
|
+
|
|
712
1473
|
if result["success"]:
|
|
713
1474
|
data = result["data"]
|
|
714
1475
|
items = data.get("results", []) if isinstance(data, dict) else data
|
|
715
1476
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
716
|
-
|
|
1477
|
+
|
|
717
1478
|
return {
|
|
718
1479
|
"success": True,
|
|
719
1480
|
"message": f"✅ Encontrados {len(items)} itens na nota fiscal {sequential_number}",
|
|
720
1481
|
"items": items,
|
|
721
1482
|
"count": len(items),
|
|
722
|
-
"metadata": metadata
|
|
1483
|
+
"metadata": metadata,
|
|
723
1484
|
}
|
|
724
|
-
|
|
1485
|
+
|
|
725
1486
|
return {
|
|
726
1487
|
"success": False,
|
|
727
1488
|
"message": f"❌ Erro ao buscar itens da nota fiscal {sequential_number}",
|
|
728
1489
|
"error": result.get("error"),
|
|
729
|
-
"details": result.get("message")
|
|
1490
|
+
"details": result.get("message"),
|
|
730
1491
|
}
|
|
731
1492
|
|
|
1493
|
+
|
|
732
1494
|
@mcp.tool
|
|
733
|
-
async def create_sienge_purchase_invoice(
|
|
734
|
-
|
|
735
|
-
|
|
1495
|
+
async def create_sienge_purchase_invoice(
|
|
1496
|
+
document_id: str,
|
|
1497
|
+
number: str,
|
|
1498
|
+
supplier_id: int,
|
|
1499
|
+
company_id: int,
|
|
1500
|
+
movement_type_id: int,
|
|
1501
|
+
movement_date: str,
|
|
1502
|
+
issue_date: str,
|
|
1503
|
+
series: Optional[str] = None,
|
|
1504
|
+
notes: Optional[str] = None,
|
|
1505
|
+
) -> Dict:
|
|
736
1506
|
"""
|
|
737
1507
|
Cadastra uma nova nota fiscal de compra
|
|
738
|
-
|
|
1508
|
+
|
|
739
1509
|
Args:
|
|
740
1510
|
document_id: ID do documento (ex: "NF")
|
|
741
1511
|
number: Número da nota fiscal
|
|
@@ -748,43 +1518,44 @@ async def create_sienge_purchase_invoice(document_id: str, number: str, supplier
|
|
|
748
1518
|
notes: Observações (opcional)
|
|
749
1519
|
"""
|
|
750
1520
|
invoice_data = {
|
|
751
|
-
"
|
|
1521
|
+
"document_id": document_id,
|
|
752
1522
|
"number": number,
|
|
753
|
-
"
|
|
754
|
-
"
|
|
755
|
-
"
|
|
756
|
-
"
|
|
757
|
-
"
|
|
1523
|
+
"supplier_id": supplier_id,
|
|
1524
|
+
"company_id": company_id,
|
|
1525
|
+
"movement_type_id": movement_type_id,
|
|
1526
|
+
"movement_date": movement_date,
|
|
1527
|
+
"issue_date": issue_date,
|
|
758
1528
|
}
|
|
759
|
-
|
|
1529
|
+
|
|
760
1530
|
if series:
|
|
761
1531
|
invoice_data["series"] = series
|
|
762
1532
|
if notes:
|
|
763
1533
|
invoice_data["notes"] = notes
|
|
764
|
-
|
|
765
|
-
result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
|
|
766
|
-
|
|
1534
|
+
|
|
1535
|
+
result = await make_sienge_request("POST", "/purchase-invoices", json_data=to_camel_json(invoice_data))
|
|
1536
|
+
|
|
767
1537
|
if result["success"]:
|
|
768
|
-
return {
|
|
769
|
-
|
|
770
|
-
"message": f"✅ Nota fiscal {number} criada com sucesso",
|
|
771
|
-
"invoice": result["data"]
|
|
772
|
-
}
|
|
773
|
-
|
|
1538
|
+
return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
|
|
1539
|
+
|
|
774
1540
|
return {
|
|
775
1541
|
"success": False,
|
|
776
1542
|
"message": f"❌ Erro ao criar nota fiscal {number}",
|
|
777
1543
|
"error": result.get("error"),
|
|
778
|
-
"details": result.get("message")
|
|
1544
|
+
"details": result.get("message"),
|
|
779
1545
|
}
|
|
780
1546
|
|
|
1547
|
+
|
|
781
1548
|
@mcp.tool
|
|
782
|
-
async def add_items_to_purchase_invoice(
|
|
783
|
-
|
|
784
|
-
|
|
1549
|
+
async def add_items_to_purchase_invoice(
|
|
1550
|
+
sequential_number: int,
|
|
1551
|
+
deliveries_order: List[Dict[str, Any]],
|
|
1552
|
+
copy_notes_purchase_orders: bool = True,
|
|
1553
|
+
copy_notes_resources: bool = False,
|
|
1554
|
+
copy_attachments_purchase_orders: bool = True,
|
|
1555
|
+
) -> Dict:
|
|
785
1556
|
"""
|
|
786
1557
|
Insere itens em uma nota fiscal a partir de entregas de pedidos de compra
|
|
787
|
-
|
|
1558
|
+
|
|
788
1559
|
Args:
|
|
789
1560
|
sequential_number: Número sequencial da nota fiscal
|
|
790
1561
|
deliveries_order: Lista de entregas com estrutura:
|
|
@@ -798,36 +1569,44 @@ async def add_items_to_purchase_invoice(sequential_number: int, deliveries_order
|
|
|
798
1569
|
copy_attachments_purchase_orders: Copiar anexos dos pedidos de compra
|
|
799
1570
|
"""
|
|
800
1571
|
item_data = {
|
|
801
|
-
"
|
|
802
|
-
"
|
|
803
|
-
"
|
|
804
|
-
"
|
|
1572
|
+
"deliveries_order": deliveries_order,
|
|
1573
|
+
"copy_notes_purchase_orders": copy_notes_purchase_orders,
|
|
1574
|
+
"copy_notes_resources": copy_notes_resources,
|
|
1575
|
+
"copy_attachments_purchase_orders": copy_attachments_purchase_orders,
|
|
805
1576
|
}
|
|
806
|
-
|
|
807
|
-
result = await make_sienge_request(
|
|
808
|
-
|
|
1577
|
+
|
|
1578
|
+
result = await make_sienge_request(
|
|
1579
|
+
"POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=to_camel_json(item_data)
|
|
1580
|
+
)
|
|
1581
|
+
|
|
809
1582
|
if result["success"]:
|
|
810
1583
|
return {
|
|
811
1584
|
"success": True,
|
|
812
1585
|
"message": f"✅ Itens adicionados à nota fiscal {sequential_number} com sucesso",
|
|
813
|
-
"item": result["data"]
|
|
1586
|
+
"item": result["data"],
|
|
814
1587
|
}
|
|
815
|
-
|
|
1588
|
+
|
|
816
1589
|
return {
|
|
817
1590
|
"success": False,
|
|
818
1591
|
"message": f"❌ Erro ao adicionar itens à nota fiscal {sequential_number}",
|
|
819
1592
|
"error": result.get("error"),
|
|
820
|
-
"details": result.get("message")
|
|
1593
|
+
"details": result.get("message"),
|
|
821
1594
|
}
|
|
822
1595
|
|
|
1596
|
+
|
|
823
1597
|
@mcp.tool
|
|
824
|
-
async def get_sienge_purchase_invoices_deliveries_attended(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1598
|
+
async def get_sienge_purchase_invoices_deliveries_attended(
|
|
1599
|
+
bill_id: Optional[int] = None,
|
|
1600
|
+
sequential_number: Optional[int] = None,
|
|
1601
|
+
purchase_order_id: Optional[int] = None,
|
|
1602
|
+
invoice_item_number: Optional[int] = None,
|
|
1603
|
+
purchase_order_item_number: Optional[int] = None,
|
|
1604
|
+
limit: Optional[int] = 100,
|
|
1605
|
+
offset: Optional[int] = 0,
|
|
1606
|
+
) -> Dict:
|
|
828
1607
|
"""
|
|
829
1608
|
Lista entregas atendidas entre pedidos de compra e notas fiscais
|
|
830
|
-
|
|
1609
|
+
|
|
831
1610
|
Args:
|
|
832
1611
|
bill_id: ID do título da nota fiscal
|
|
833
1612
|
sequential_number: Número sequencial da nota fiscal
|
|
@@ -838,7 +1617,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(bill_id: Optional[int
|
|
|
838
1617
|
offset: Deslocamento (padrão: 0)
|
|
839
1618
|
"""
|
|
840
1619
|
params = {"limit": min(limit or 100, 200), "offset": offset or 0}
|
|
841
|
-
|
|
1620
|
+
|
|
842
1621
|
if bill_id:
|
|
843
1622
|
params["billId"] = bill_id
|
|
844
1623
|
if sequential_number:
|
|
@@ -849,37 +1628,39 @@ async def get_sienge_purchase_invoices_deliveries_attended(bill_id: Optional[int
|
|
|
849
1628
|
params["invoiceItemNumber"] = invoice_item_number
|
|
850
1629
|
if purchase_order_item_number:
|
|
851
1630
|
params["purchaseOrderItemNumber"] = purchase_order_item_number
|
|
852
|
-
|
|
1631
|
+
|
|
853
1632
|
result = await make_sienge_request("GET", "/purchase-invoices/deliveries-attended", params=params)
|
|
854
|
-
|
|
1633
|
+
|
|
855
1634
|
if result["success"]:
|
|
856
1635
|
data = result["data"]
|
|
857
1636
|
deliveries = data.get("results", []) if isinstance(data, dict) else data
|
|
858
1637
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
859
|
-
|
|
1638
|
+
|
|
860
1639
|
return {
|
|
861
1640
|
"success": True,
|
|
862
1641
|
"message": f"✅ Encontradas {len(deliveries)} entregas atendidas",
|
|
863
1642
|
"deliveries": deliveries,
|
|
864
1643
|
"count": len(deliveries),
|
|
865
1644
|
"metadata": metadata,
|
|
866
|
-
"filters": params
|
|
1645
|
+
"filters": params,
|
|
867
1646
|
}
|
|
868
|
-
|
|
1647
|
+
|
|
869
1648
|
return {
|
|
870
1649
|
"success": False,
|
|
871
1650
|
"message": "❌ Erro ao buscar entregas atendidas",
|
|
872
1651
|
"error": result.get("error"),
|
|
873
|
-
"details": result.get("message")
|
|
1652
|
+
"details": result.get("message"),
|
|
874
1653
|
}
|
|
875
1654
|
|
|
1655
|
+
|
|
876
1656
|
# ============ ESTOQUE ============
|
|
877
1657
|
|
|
1658
|
+
|
|
878
1659
|
@mcp.tool
|
|
879
1660
|
async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None) -> Dict:
|
|
880
1661
|
"""
|
|
881
1662
|
Consulta inventário de estoque por centro de custo
|
|
882
|
-
|
|
1663
|
+
|
|
883
1664
|
Args:
|
|
884
1665
|
cost_center_id: ID do centro de custo (obrigatório)
|
|
885
1666
|
resource_id: ID do insumo específico (opcional)
|
|
@@ -888,67 +1669,76 @@ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[
|
|
|
888
1669
|
endpoint = f"/stock-inventories/{cost_center_id}/items/{resource_id}"
|
|
889
1670
|
else:
|
|
890
1671
|
endpoint = f"/stock-inventories/{cost_center_id}/items"
|
|
891
|
-
|
|
1672
|
+
|
|
892
1673
|
result = await make_sienge_request("GET", endpoint)
|
|
893
|
-
|
|
1674
|
+
|
|
894
1675
|
if result["success"]:
|
|
895
1676
|
data = result["data"]
|
|
896
1677
|
items = data.get("results", []) if isinstance(data, dict) else data
|
|
897
1678
|
count = len(items) if isinstance(items, list) else 1
|
|
898
|
-
|
|
1679
|
+
|
|
899
1680
|
return {
|
|
900
1681
|
"success": True,
|
|
901
1682
|
"message": f"✅ Inventário do centro de custo {cost_center_id}",
|
|
902
1683
|
"cost_center_id": cost_center_id,
|
|
903
1684
|
"inventory": items,
|
|
904
|
-
"count": count
|
|
1685
|
+
"count": count,
|
|
905
1686
|
}
|
|
906
|
-
|
|
1687
|
+
|
|
907
1688
|
return {
|
|
908
1689
|
"success": False,
|
|
909
1690
|
"message": f"❌ Erro ao consultar estoque do centro {cost_center_id}",
|
|
910
1691
|
"error": result.get("error"),
|
|
911
|
-
"details": result.get("message")
|
|
1692
|
+
"details": result.get("message"),
|
|
912
1693
|
}
|
|
913
1694
|
|
|
1695
|
+
|
|
914
1696
|
@mcp.tool
|
|
915
1697
|
async def get_sienge_stock_reservations(limit: Optional[int] = 50) -> Dict:
|
|
916
1698
|
"""
|
|
917
1699
|
Lista reservas de estoque
|
|
918
|
-
|
|
1700
|
+
|
|
919
1701
|
Args:
|
|
920
1702
|
limit: Máximo de registros
|
|
921
1703
|
"""
|
|
922
1704
|
params = {"limit": min(limit or 50, 200)}
|
|
923
1705
|
result = await make_sienge_request("GET", "/stock-reservations", params=params)
|
|
924
|
-
|
|
1706
|
+
|
|
925
1707
|
if result["success"]:
|
|
926
1708
|
data = result["data"]
|
|
927
1709
|
reservations = data.get("results", []) if isinstance(data, dict) else data
|
|
928
|
-
|
|
1710
|
+
|
|
929
1711
|
return {
|
|
930
1712
|
"success": True,
|
|
931
1713
|
"message": f"✅ Encontradas {len(reservations)} reservas de estoque",
|
|
932
1714
|
"reservations": reservations,
|
|
933
|
-
"count": len(reservations)
|
|
1715
|
+
"count": len(reservations),
|
|
934
1716
|
}
|
|
935
|
-
|
|
1717
|
+
|
|
936
1718
|
return {
|
|
937
1719
|
"success": False,
|
|
938
1720
|
"message": "❌ Erro ao buscar reservas de estoque",
|
|
939
1721
|
"error": result.get("error"),
|
|
940
|
-
"details": result.get("message")
|
|
1722
|
+
"details": result.get("message"),
|
|
941
1723
|
}
|
|
942
1724
|
|
|
1725
|
+
|
|
943
1726
|
# ============ PROJETOS/OBRAS ============
|
|
944
1727
|
|
|
1728
|
+
|
|
945
1729
|
@mcp.tool
|
|
946
|
-
async def get_sienge_projects(
|
|
947
|
-
|
|
948
|
-
|
|
1730
|
+
async def get_sienge_projects(
|
|
1731
|
+
limit: Optional[int] = 100,
|
|
1732
|
+
offset: Optional[int] = 0,
|
|
1733
|
+
company_id: Optional[int] = None,
|
|
1734
|
+
enterprise_type: Optional[int] = None,
|
|
1735
|
+
receivable_register: Optional[str] = None,
|
|
1736
|
+
only_buildings_enabled: Optional[bool] = False,
|
|
1737
|
+
) -> Dict:
|
|
949
1738
|
"""
|
|
950
1739
|
Busca empreendimentos/obras no Sienge
|
|
951
|
-
|
|
1740
|
+
CORRIGIDO: Mapeamento correto da chave de resposta
|
|
1741
|
+
|
|
952
1742
|
Args:
|
|
953
1743
|
limit: Máximo de registros (padrão: 100, máximo: 200)
|
|
954
1744
|
offset: Pular registros (padrão: 0)
|
|
@@ -957,130 +1747,149 @@ async def get_sienge_projects(limit: Optional[int] = 100, offset: Optional[int]
|
|
|
957
1747
|
receivable_register: Filtro de registro de recebíveis (B3, CERC)
|
|
958
1748
|
only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
|
|
959
1749
|
"""
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
result = await make_sienge_request("GET", "/enterprises", params=params)
|
|
972
|
-
|
|
1750
|
+
# Usar serviço interno
|
|
1751
|
+
result = await _svc_get_projects(
|
|
1752
|
+
limit=limit or 100,
|
|
1753
|
+
offset=offset or 0,
|
|
1754
|
+
company_id=company_id,
|
|
1755
|
+
enterprise_type=enterprise_type,
|
|
1756
|
+
receivable_register=receivable_register,
|
|
1757
|
+
only_buildings_enabled=only_buildings_enabled or False
|
|
1758
|
+
)
|
|
1759
|
+
|
|
973
1760
|
if result["success"]:
|
|
974
1761
|
data = result["data"]
|
|
1762
|
+
# CORREÇÃO: API retorna em "results", mas paginador espera em "enterprises"
|
|
975
1763
|
enterprises = data.get("results", []) if isinstance(data, dict) else data
|
|
976
1764
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
977
|
-
|
|
1765
|
+
total_count = metadata.get("count", len(enterprises))
|
|
1766
|
+
|
|
978
1767
|
return {
|
|
979
1768
|
"success": True,
|
|
980
|
-
"message": f"✅ Encontrados {len(enterprises)} empreendimentos",
|
|
981
|
-
"enterprises": enterprises,
|
|
1769
|
+
"message": f"✅ Encontrados {len(enterprises)} empreendimentos (total: {total_count})",
|
|
1770
|
+
"enterprises": enterprises, # Manter consistência para paginador
|
|
1771
|
+
"projects": enterprises, # Alias para compatibilidade
|
|
982
1772
|
"count": len(enterprises),
|
|
1773
|
+
"total_count": total_count,
|
|
983
1774
|
"metadata": metadata,
|
|
984
|
-
"
|
|
1775
|
+
"filters_applied": {
|
|
1776
|
+
"limit": limit, "offset": offset, "company_id": company_id,
|
|
1777
|
+
"enterprise_type": enterprise_type, "receivable_register": receivable_register,
|
|
1778
|
+
"only_buildings_enabled": only_buildings_enabled
|
|
1779
|
+
},
|
|
1780
|
+
"request_id": result.get("request_id"),
|
|
1781
|
+
"latency_ms": result.get("latency_ms"),
|
|
1782
|
+
"cache": result.get("cache")
|
|
985
1783
|
}
|
|
986
|
-
|
|
1784
|
+
|
|
987
1785
|
return {
|
|
988
1786
|
"success": False,
|
|
989
1787
|
"message": "❌ Erro ao buscar empreendimentos",
|
|
990
1788
|
"error": result.get("error"),
|
|
991
|
-
"details": result.get("message")
|
|
1789
|
+
"details": result.get("message"),
|
|
1790
|
+
"request_id": result.get("request_id")
|
|
992
1791
|
}
|
|
993
1792
|
|
|
1793
|
+
|
|
994
1794
|
@mcp.tool
|
|
995
1795
|
async def get_sienge_enterprise_by_id(enterprise_id: int) -> Dict:
|
|
996
1796
|
"""
|
|
997
1797
|
Busca um empreendimento específico por ID no Sienge
|
|
998
|
-
|
|
1798
|
+
|
|
999
1799
|
Args:
|
|
1000
1800
|
enterprise_id: ID do empreendimento
|
|
1001
1801
|
"""
|
|
1002
1802
|
result = await make_sienge_request("GET", f"/enterprises/{enterprise_id}")
|
|
1003
|
-
|
|
1803
|
+
|
|
1004
1804
|
if result["success"]:
|
|
1005
|
-
return {
|
|
1006
|
-
|
|
1007
|
-
"message": f"✅ Empreendimento {enterprise_id} encontrado",
|
|
1008
|
-
"enterprise": result["data"]
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1805
|
+
return {"success": True, "message": f"✅ Empreendimento {enterprise_id} encontrado", "enterprise": result["data"]}
|
|
1806
|
+
|
|
1011
1807
|
return {
|
|
1012
1808
|
"success": False,
|
|
1013
1809
|
"message": f"❌ Erro ao buscar empreendimento {enterprise_id}",
|
|
1014
1810
|
"error": result.get("error"),
|
|
1015
|
-
"details": result.get("message")
|
|
1811
|
+
"details": result.get("message"),
|
|
1016
1812
|
}
|
|
1017
1813
|
|
|
1814
|
+
|
|
1018
1815
|
@mcp.tool
|
|
1019
1816
|
async def get_sienge_enterprise_groupings(enterprise_id: int) -> Dict:
|
|
1020
1817
|
"""
|
|
1021
1818
|
Busca agrupamentos de unidades de um empreendimento específico
|
|
1022
|
-
|
|
1819
|
+
|
|
1023
1820
|
Args:
|
|
1024
1821
|
enterprise_id: ID do empreendimento
|
|
1025
1822
|
"""
|
|
1026
1823
|
result = await make_sienge_request("GET", f"/enterprises/{enterprise_id}/groupings")
|
|
1027
|
-
|
|
1824
|
+
|
|
1028
1825
|
if result["success"]:
|
|
1029
1826
|
groupings = result["data"]
|
|
1030
1827
|
return {
|
|
1031
1828
|
"success": True,
|
|
1032
1829
|
"message": f"✅ Agrupamentos do empreendimento {enterprise_id} encontrados",
|
|
1033
1830
|
"groupings": groupings,
|
|
1034
|
-
"count": len(groupings) if isinstance(groupings, list) else 0
|
|
1831
|
+
"count": len(groupings) if isinstance(groupings, list) else 0,
|
|
1035
1832
|
}
|
|
1036
|
-
|
|
1833
|
+
|
|
1037
1834
|
return {
|
|
1038
1835
|
"success": False,
|
|
1039
1836
|
"message": f"❌ Erro ao buscar agrupamentos do empreendimento {enterprise_id}",
|
|
1040
1837
|
"error": result.get("error"),
|
|
1041
|
-
"details": result.get("message")
|
|
1838
|
+
"details": result.get("message"),
|
|
1042
1839
|
}
|
|
1043
1840
|
|
|
1841
|
+
|
|
1044
1842
|
@mcp.tool
|
|
1045
1843
|
async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0) -> Dict:
|
|
1046
1844
|
"""
|
|
1047
1845
|
Consulta unidades cadastradas no Sienge
|
|
1048
|
-
|
|
1846
|
+
|
|
1049
1847
|
Args:
|
|
1050
1848
|
limit: Máximo de registros (padrão: 50)
|
|
1051
1849
|
offset: Pular registros (padrão: 0)
|
|
1052
1850
|
"""
|
|
1053
1851
|
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
1054
1852
|
result = await make_sienge_request("GET", "/units", params=params)
|
|
1055
|
-
|
|
1853
|
+
|
|
1056
1854
|
if result["success"]:
|
|
1057
1855
|
data = result["data"]
|
|
1058
1856
|
units = data.get("results", []) if isinstance(data, dict) else data
|
|
1059
1857
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
1060
1858
|
total_count = metadata.get("count", len(units))
|
|
1061
|
-
|
|
1859
|
+
|
|
1062
1860
|
return {
|
|
1063
1861
|
"success": True,
|
|
1064
1862
|
"message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
|
|
1065
1863
|
"units": units,
|
|
1066
|
-
"count": len(units)
|
|
1864
|
+
"count": len(units),
|
|
1865
|
+
"total_count": total_count,
|
|
1866
|
+
"request_id": result.get("request_id"),
|
|
1867
|
+
"latency_ms": result.get("latency_ms")
|
|
1067
1868
|
}
|
|
1068
|
-
|
|
1869
|
+
|
|
1069
1870
|
return {
|
|
1070
1871
|
"success": False,
|
|
1071
1872
|
"message": "❌ Erro ao buscar unidades",
|
|
1072
1873
|
"error": result.get("error"),
|
|
1073
|
-
"details": result.get("message")
|
|
1874
|
+
"details": result.get("message"),
|
|
1875
|
+
"request_id": result.get("request_id"),
|
|
1876
|
+
"latency_ms": result.get("latency_ms")
|
|
1074
1877
|
}
|
|
1075
1878
|
|
|
1879
|
+
|
|
1076
1880
|
# ============ CUSTOS ============
|
|
1077
1881
|
|
|
1882
|
+
|
|
1078
1883
|
@mcp.tool
|
|
1079
|
-
async def get_sienge_unit_cost_tables(
|
|
1080
|
-
|
|
1884
|
+
async def get_sienge_unit_cost_tables(
|
|
1885
|
+
table_code: Optional[str] = None,
|
|
1886
|
+
description: Optional[str] = None,
|
|
1887
|
+
status: Optional[str] = "Active",
|
|
1888
|
+
integration_enabled: Optional[bool] = None,
|
|
1889
|
+
) -> Dict:
|
|
1081
1890
|
"""
|
|
1082
1891
|
Consulta tabelas de custos unitários
|
|
1083
|
-
|
|
1892
|
+
|
|
1084
1893
|
Args:
|
|
1085
1894
|
table_code: Código da tabela (opcional)
|
|
1086
1895
|
description: Descrição da tabela (opcional)
|
|
@@ -1088,80 +1897,763 @@ async def get_sienge_unit_cost_tables(table_code: Optional[str] = None, descript
|
|
|
1088
1897
|
integration_enabled: Se habilitada para integração
|
|
1089
1898
|
"""
|
|
1090
1899
|
params = {"status": status or "Active"}
|
|
1091
|
-
|
|
1900
|
+
|
|
1092
1901
|
if table_code:
|
|
1093
1902
|
params["table_code"] = table_code
|
|
1094
1903
|
if description:
|
|
1095
1904
|
params["description"] = description
|
|
1096
1905
|
if integration_enabled is not None:
|
|
1097
1906
|
params["integration_enabled"] = integration_enabled
|
|
1098
|
-
|
|
1907
|
+
|
|
1099
1908
|
result = await make_sienge_request("GET", "/unit-cost-tables", params=params)
|
|
1100
|
-
|
|
1909
|
+
|
|
1101
1910
|
if result["success"]:
|
|
1102
1911
|
data = result["data"]
|
|
1103
1912
|
tables = data.get("results", []) if isinstance(data, dict) else data
|
|
1104
|
-
|
|
1913
|
+
|
|
1105
1914
|
return {
|
|
1106
1915
|
"success": True,
|
|
1107
1916
|
"message": f"✅ Encontradas {len(tables)} tabelas de custos",
|
|
1108
1917
|
"cost_tables": tables,
|
|
1109
|
-
"count": len(tables)
|
|
1918
|
+
"count": len(tables),
|
|
1110
1919
|
}
|
|
1111
|
-
|
|
1920
|
+
|
|
1112
1921
|
return {
|
|
1113
1922
|
"success": False,
|
|
1114
1923
|
"message": "❌ Erro ao buscar tabelas de custos",
|
|
1115
1924
|
"error": result.get("error"),
|
|
1116
|
-
"details": result.get("message")
|
|
1925
|
+
"details": result.get("message"),
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
# ============ SEARCH UNIVERSAL (COMPATIBILIDADE CHATGPT) ============
|
|
1930
|
+
|
|
1931
|
+
|
|
1932
|
+
@mcp.tool
|
|
1933
|
+
async def search_sienge_data(
|
|
1934
|
+
query: str,
|
|
1935
|
+
entity_type: Optional[str] = None,
|
|
1936
|
+
limit: Optional[int] = 20,
|
|
1937
|
+
filters: Optional[Dict[str, Any]] = None
|
|
1938
|
+
) -> Dict:
|
|
1939
|
+
"""
|
|
1940
|
+
Busca universal no Sienge - compatível com ChatGPT/OpenAI MCP
|
|
1941
|
+
|
|
1942
|
+
Permite buscar em múltiplas entidades do Sienge de forma unificada.
|
|
1943
|
+
|
|
1944
|
+
Args:
|
|
1945
|
+
query: Termo de busca (nome, código, descrição, etc.)
|
|
1946
|
+
entity_type: Tipo de entidade (customers, creditors, projects, bills, purchase_orders, etc.)
|
|
1947
|
+
limit: Máximo de registros (padrão: 20, máximo: 100)
|
|
1948
|
+
filters: Filtros específicos por tipo de entidade
|
|
1949
|
+
"""
|
|
1950
|
+
search_results = []
|
|
1951
|
+
limit = min(limit or 20, 100)
|
|
1952
|
+
|
|
1953
|
+
# Se entity_type específico, buscar apenas nele
|
|
1954
|
+
if entity_type:
|
|
1955
|
+
result = await _search_specific_entity(entity_type, query, limit, filters or {})
|
|
1956
|
+
if result["success"]:
|
|
1957
|
+
return result
|
|
1958
|
+
else:
|
|
1959
|
+
return {
|
|
1960
|
+
"success": False,
|
|
1961
|
+
"message": f"❌ Erro na busca em {entity_type}",
|
|
1962
|
+
"error": result.get("error"),
|
|
1963
|
+
"query": query,
|
|
1964
|
+
"entity_type": entity_type
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
# Busca universal em múltiplas entidades
|
|
1968
|
+
entities_to_search = [
|
|
1969
|
+
("customers", "clientes"),
|
|
1970
|
+
("creditors", "credores/fornecedores"),
|
|
1971
|
+
("projects", "empreendimentos/obras"),
|
|
1972
|
+
("bills", "títulos a pagar"),
|
|
1973
|
+
("purchase_orders", "pedidos de compra")
|
|
1974
|
+
]
|
|
1975
|
+
|
|
1976
|
+
total_found = 0
|
|
1977
|
+
|
|
1978
|
+
for entity_key, entity_name in entities_to_search:
|
|
1979
|
+
try:
|
|
1980
|
+
entity_result = await _search_specific_entity(entity_key, query, min(5, limit), {})
|
|
1981
|
+
if entity_result["success"] and entity_result.get("count", 0) > 0:
|
|
1982
|
+
search_results.append({
|
|
1983
|
+
"entity_type": entity_key,
|
|
1984
|
+
"entity_name": entity_name,
|
|
1985
|
+
"count": entity_result["count"],
|
|
1986
|
+
"results": entity_result["data"][:5], # Limitar a 5 por entidade na busca universal
|
|
1987
|
+
"has_more": entity_result["count"] > 5
|
|
1988
|
+
})
|
|
1989
|
+
total_found += entity_result["count"]
|
|
1990
|
+
except Exception as e:
|
|
1991
|
+
# Continuar com outras entidades se uma falhar
|
|
1992
|
+
continue
|
|
1993
|
+
|
|
1994
|
+
if search_results:
|
|
1995
|
+
return {
|
|
1996
|
+
"success": True,
|
|
1997
|
+
"message": f"✅ Busca '{query}' encontrou resultados em {len(search_results)} entidades (total: {total_found} registros)",
|
|
1998
|
+
"query": query,
|
|
1999
|
+
"total_entities": len(search_results),
|
|
2000
|
+
"total_records": total_found,
|
|
2001
|
+
"results_by_entity": search_results,
|
|
2002
|
+
"suggestion": "Use entity_type para buscar especificamente em uma entidade e obter mais resultados"
|
|
2003
|
+
}
|
|
2004
|
+
else:
|
|
2005
|
+
return {
|
|
2006
|
+
"success": False,
|
|
2007
|
+
"message": f"❌ Nenhum resultado encontrado para '{query}'",
|
|
2008
|
+
"query": query,
|
|
2009
|
+
"searched_entities": [name for _, name in entities_to_search],
|
|
2010
|
+
"suggestion": "Tente termos mais específicos ou use os tools específicos de cada entidade"
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
|
|
2014
|
+
async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
|
|
2015
|
+
"""
|
|
2016
|
+
Função auxiliar para buscar em uma entidade específica
|
|
2017
|
+
CORRIGIDO: Usa serviços internos, nunca outras tools
|
|
2018
|
+
"""
|
|
2019
|
+
|
|
2020
|
+
if entity_type == "customers":
|
|
2021
|
+
result = await _svc_get_customers(limit=limit, search=query)
|
|
2022
|
+
if result["success"]:
|
|
2023
|
+
data = result["data"]
|
|
2024
|
+
customers = data.get("results", []) if isinstance(data, dict) else data
|
|
2025
|
+
return {
|
|
2026
|
+
"success": True,
|
|
2027
|
+
"data": customers,
|
|
2028
|
+
"count": len(customers),
|
|
2029
|
+
"entity_type": "customers"
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
elif entity_type == "creditors":
|
|
2033
|
+
result = await _svc_get_creditors(limit=limit, search=query)
|
|
2034
|
+
if result["success"]:
|
|
2035
|
+
data = result["data"]
|
|
2036
|
+
creditors = data.get("results", []) if isinstance(data, dict) else data
|
|
2037
|
+
return {
|
|
2038
|
+
"success": True,
|
|
2039
|
+
"data": creditors,
|
|
2040
|
+
"count": len(creditors),
|
|
2041
|
+
"entity_type": "creditors"
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
elif entity_type == "projects" or entity_type == "enterprises":
|
|
2045
|
+
# Para projetos, usar filtros mais específicos se disponível
|
|
2046
|
+
company_id = filters.get("company_id")
|
|
2047
|
+
result = await _svc_get_projects(limit=limit, company_id=company_id)
|
|
2048
|
+
if result["success"]:
|
|
2049
|
+
data = result["data"]
|
|
2050
|
+
projects = data.get("results", []) if isinstance(data, dict) else data
|
|
2051
|
+
|
|
2052
|
+
# Filtrar por query se fornecida
|
|
2053
|
+
if query:
|
|
2054
|
+
projects = [
|
|
2055
|
+
p for p in projects
|
|
2056
|
+
if query.lower() in str(p.get("description", "")).lower()
|
|
2057
|
+
or query.lower() in str(p.get("name", "")).lower()
|
|
2058
|
+
or query.lower() in str(p.get("code", "")).lower()
|
|
2059
|
+
]
|
|
2060
|
+
return {
|
|
2061
|
+
"success": True,
|
|
2062
|
+
"data": projects,
|
|
2063
|
+
"count": len(projects),
|
|
2064
|
+
"entity_type": "projects"
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
elif entity_type == "bills":
|
|
2068
|
+
# Para títulos, usar data padrão se não especificada
|
|
2069
|
+
start_date = filters.get("start_date")
|
|
2070
|
+
end_date = filters.get("end_date")
|
|
2071
|
+
result = await _svc_get_bills(
|
|
2072
|
+
start_date=start_date,
|
|
2073
|
+
end_date=end_date,
|
|
2074
|
+
limit=limit
|
|
2075
|
+
)
|
|
2076
|
+
if result["success"]:
|
|
2077
|
+
data = result["data"]
|
|
2078
|
+
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
2079
|
+
return {
|
|
2080
|
+
"success": True,
|
|
2081
|
+
"data": bills,
|
|
2082
|
+
"count": len(bills),
|
|
2083
|
+
"entity_type": "bills"
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
elif entity_type == "purchase_orders":
|
|
2087
|
+
result = await _svc_get_purchase_orders(limit=limit)
|
|
2088
|
+
if result["success"]:
|
|
2089
|
+
data = result["data"]
|
|
2090
|
+
orders = data.get("results", []) if isinstance(data, dict) else data
|
|
2091
|
+
|
|
2092
|
+
# Filtrar por query se fornecida
|
|
2093
|
+
if query:
|
|
2094
|
+
orders = [
|
|
2095
|
+
o for o in orders
|
|
2096
|
+
if query.lower() in str(o.get("description", "")).lower()
|
|
2097
|
+
or query.lower() in str(o.get("id", "")).lower()
|
|
2098
|
+
]
|
|
2099
|
+
return {
|
|
2100
|
+
"success": True,
|
|
2101
|
+
"data": orders,
|
|
2102
|
+
"count": len(orders),
|
|
2103
|
+
"entity_type": "purchase_orders"
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
# Se chegou aqui, entidade não suportada ou erro
|
|
2107
|
+
return {
|
|
2108
|
+
"success": False,
|
|
2109
|
+
"error": f"Entidade '{entity_type}' não suportada ou erro na busca",
|
|
2110
|
+
"supported_entities": ["customers", "creditors", "projects", "bills", "purchase_orders"]
|
|
1117
2111
|
}
|
|
1118
2112
|
|
|
2113
|
+
|
|
2114
|
+
@mcp.tool
|
|
2115
|
+
async def list_sienge_entities() -> Dict:
|
|
2116
|
+
"""
|
|
2117
|
+
Lista todas as entidades disponíveis no Sienge MCP para busca
|
|
2118
|
+
|
|
2119
|
+
Retorna informações sobre os tipos de dados que podem ser consultados
|
|
2120
|
+
"""
|
|
2121
|
+
entities = [
|
|
2122
|
+
{
|
|
2123
|
+
"type": "customers",
|
|
2124
|
+
"name": "Clientes",
|
|
2125
|
+
"description": "Clientes cadastrados no sistema",
|
|
2126
|
+
"search_fields": ["nome", "documento", "email"],
|
|
2127
|
+
"tools": ["get_sienge_customers", "search_sienge_data"]
|
|
2128
|
+
},
|
|
2129
|
+
{
|
|
2130
|
+
"type": "creditors",
|
|
2131
|
+
"name": "Credores/Fornecedores",
|
|
2132
|
+
"description": "Fornecedores e credores cadastrados",
|
|
2133
|
+
"search_fields": ["nome", "documento"],
|
|
2134
|
+
"tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info", "get_sienge_suppliers"]
|
|
2135
|
+
},
|
|
2136
|
+
{
|
|
2137
|
+
"type": "projects",
|
|
2138
|
+
"name": "Empreendimentos/Obras",
|
|
2139
|
+
"description": "Projetos e obras cadastrados",
|
|
2140
|
+
"search_fields": ["código", "descrição", "nome"],
|
|
2141
|
+
"tools": ["get_sienge_projects", "get_sienge_enterprises", "get_sienge_enterprise_by_id"]
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
"type": "bills",
|
|
2145
|
+
"name": "Títulos a Pagar",
|
|
2146
|
+
"description": "Contas a pagar e títulos financeiros",
|
|
2147
|
+
"search_fields": ["número", "credor", "valor"],
|
|
2148
|
+
"tools": ["get_sienge_bills", "get_sienge_accounts_payable"]
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
"type": "purchase_orders",
|
|
2152
|
+
"name": "Pedidos de Compra",
|
|
2153
|
+
"description": "Pedidos de compra e solicitações",
|
|
2154
|
+
"search_fields": ["id", "descrição", "status"],
|
|
2155
|
+
"tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests", "list_sienge_purchase_requests"]
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
"type": "invoices",
|
|
2159
|
+
"name": "Notas Fiscais",
|
|
2160
|
+
"description": "Notas fiscais de compra",
|
|
2161
|
+
"search_fields": ["número", "série", "fornecedor"],
|
|
2162
|
+
"tools": ["get_sienge_purchase_invoice"]
|
|
2163
|
+
},
|
|
2164
|
+
{
|
|
2165
|
+
"type": "stock",
|
|
2166
|
+
"name": "Estoque",
|
|
2167
|
+
"description": "Inventário e movimentações de estoque",
|
|
2168
|
+
"search_fields": ["centro_custo", "recurso"],
|
|
2169
|
+
"tools": ["get_sienge_stock_inventory", "get_sienge_stock_reservations"]
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
"type": "financial",
|
|
2173
|
+
"name": "Financeiro",
|
|
2174
|
+
"description": "Contas a receber e movimentações financeiras",
|
|
2175
|
+
"search_fields": ["período", "cliente", "valor"],
|
|
2176
|
+
"tools": ["get_sienge_accounts_receivable", "search_sienge_financial_data", "search_sienge_finances"]
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
"type": "suppliers",
|
|
2180
|
+
"name": "Fornecedores",
|
|
2181
|
+
"description": "Fornecedores e credores cadastrados",
|
|
2182
|
+
"search_fields": ["código", "nome", "razão social"],
|
|
2183
|
+
"tools": ["get_sienge_suppliers"]
|
|
2184
|
+
}
|
|
2185
|
+
]
|
|
2186
|
+
|
|
2187
|
+
return {
|
|
2188
|
+
"success": True,
|
|
2189
|
+
"message": f"✅ {len(entities)} tipos de entidades disponíveis no Sienge",
|
|
2190
|
+
"entities": entities,
|
|
2191
|
+
"total_tools": sum(len(e["tools"]) for e in entities),
|
|
2192
|
+
"usage_example": {
|
|
2193
|
+
"search_all": "search_sienge_data('nome_cliente')",
|
|
2194
|
+
"search_specific": "search_sienge_data('nome_cliente', entity_type='customers')",
|
|
2195
|
+
"direct_access": "get_sienge_customers(search='nome_cliente')"
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
|
|
2200
|
+
# ============ PAGINATION E NAVEGAÇÃO ============
|
|
2201
|
+
|
|
2202
|
+
|
|
2203
|
+
@mcp.tool
|
|
2204
|
+
async def get_sienge_data_paginated(
|
|
2205
|
+
entity_type: str,
|
|
2206
|
+
page: int = 1,
|
|
2207
|
+
page_size: int = 20,
|
|
2208
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
2209
|
+
sort_by: Optional[str] = None
|
|
2210
|
+
) -> Dict:
|
|
2211
|
+
"""
|
|
2212
|
+
Busca dados do Sienge com paginação avançada - compatível com ChatGPT
|
|
2213
|
+
|
|
2214
|
+
Args:
|
|
2215
|
+
entity_type: Tipo de entidade (customers, creditors, projects, bills, etc.)
|
|
2216
|
+
page: Número da página (começando em 1)
|
|
2217
|
+
page_size: Registros por página (máximo 50)
|
|
2218
|
+
filters: Filtros específicos da entidade
|
|
2219
|
+
sort_by: Campo para ordenação (se suportado)
|
|
2220
|
+
"""
|
|
2221
|
+
page_size = min(page_size, 50)
|
|
2222
|
+
offset = (page - 1) * page_size
|
|
2223
|
+
|
|
2224
|
+
filters = filters or {}
|
|
2225
|
+
|
|
2226
|
+
# CORRIGIDO: Mapear para serviços internos, não tools
|
|
2227
|
+
if entity_type == "customers":
|
|
2228
|
+
search = filters.get("search")
|
|
2229
|
+
customer_type_id = filters.get("customer_type_id")
|
|
2230
|
+
result = await _svc_get_customers(
|
|
2231
|
+
limit=page_size,
|
|
2232
|
+
offset=offset,
|
|
2233
|
+
search=search,
|
|
2234
|
+
customer_type_id=customer_type_id
|
|
2235
|
+
)
|
|
2236
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2237
|
+
if result["success"]:
|
|
2238
|
+
data = result["data"]
|
|
2239
|
+
items, total = _extract_items_and_total(data)
|
|
2240
|
+
result["customers"] = items
|
|
2241
|
+
result["count"] = len(items)
|
|
2242
|
+
result["total_count"] = total
|
|
2243
|
+
|
|
2244
|
+
elif entity_type == "creditors":
|
|
2245
|
+
search = filters.get("search")
|
|
2246
|
+
result = await _svc_get_creditors(
|
|
2247
|
+
limit=page_size,
|
|
2248
|
+
offset=offset,
|
|
2249
|
+
search=search
|
|
2250
|
+
)
|
|
2251
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2252
|
+
if result["success"]:
|
|
2253
|
+
data = result["data"]
|
|
2254
|
+
items, total = _extract_items_and_total(data)
|
|
2255
|
+
result["creditors"] = items
|
|
2256
|
+
result["count"] = len(items)
|
|
2257
|
+
result["total_count"] = total
|
|
2258
|
+
|
|
2259
|
+
elif entity_type == "projects":
|
|
2260
|
+
result = await _svc_get_projects(
|
|
2261
|
+
limit=page_size,
|
|
2262
|
+
offset=offset,
|
|
2263
|
+
company_id=filters.get("company_id"),
|
|
2264
|
+
enterprise_type=filters.get("enterprise_type")
|
|
2265
|
+
)
|
|
2266
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2267
|
+
if result["success"]:
|
|
2268
|
+
data = result["data"]
|
|
2269
|
+
items, total = _extract_items_and_total(data)
|
|
2270
|
+
result["projects"] = items
|
|
2271
|
+
result["enterprises"] = items # Para compatibilidade
|
|
2272
|
+
result["count"] = len(items)
|
|
2273
|
+
result["total_count"] = total
|
|
2274
|
+
|
|
2275
|
+
elif entity_type == "bills":
|
|
2276
|
+
result = await _svc_get_bills(
|
|
2277
|
+
start_date=filters.get("start_date"),
|
|
2278
|
+
end_date=filters.get("end_date"),
|
|
2279
|
+
creditor_id=filters.get("creditor_id"),
|
|
2280
|
+
status=filters.get("status"),
|
|
2281
|
+
limit=page_size
|
|
2282
|
+
)
|
|
2283
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2284
|
+
if result["success"]:
|
|
2285
|
+
data = result["data"]
|
|
2286
|
+
items, total = _extract_items_and_total(data)
|
|
2287
|
+
result["bills"] = items
|
|
2288
|
+
result["count"] = len(items)
|
|
2289
|
+
result["total_count"] = total
|
|
2290
|
+
|
|
2291
|
+
else:
|
|
2292
|
+
return {
|
|
2293
|
+
"success": False,
|
|
2294
|
+
"message": f"❌ Tipo de entidade '{entity_type}' não suportado para paginação",
|
|
2295
|
+
"supported_types": ["customers", "creditors", "projects", "bills"]
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
if result["success"]:
|
|
2299
|
+
# Calcular informações de paginação
|
|
2300
|
+
total_count = result.get("total_count", result.get("count", 0))
|
|
2301
|
+
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 1
|
|
2302
|
+
|
|
2303
|
+
return {
|
|
2304
|
+
"success": True,
|
|
2305
|
+
"message": f"✅ Página {page} de {total_pages} - {entity_type}",
|
|
2306
|
+
"data": result.get(entity_type, result.get("data", [])),
|
|
2307
|
+
"pagination": {
|
|
2308
|
+
"current_page": page,
|
|
2309
|
+
"page_size": page_size,
|
|
2310
|
+
"total_pages": total_pages,
|
|
2311
|
+
"total_records": total_count,
|
|
2312
|
+
"has_next": page < total_pages,
|
|
2313
|
+
"has_previous": page > 1,
|
|
2314
|
+
"next_page": page + 1 if page < total_pages else None,
|
|
2315
|
+
"previous_page": page - 1 if page > 1 else None
|
|
2316
|
+
},
|
|
2317
|
+
"entity_type": entity_type,
|
|
2318
|
+
"filters_applied": filters
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
return result
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
@mcp.tool
|
|
2325
|
+
async def search_sienge_financial_data(
|
|
2326
|
+
period_start: str,
|
|
2327
|
+
period_end: str,
|
|
2328
|
+
search_type: str = "both",
|
|
2329
|
+
amount_min: Optional[float] = None,
|
|
2330
|
+
amount_max: Optional[float] = None,
|
|
2331
|
+
customer_creditor_search: Optional[str] = None
|
|
2332
|
+
) -> Dict:
|
|
2333
|
+
"""
|
|
2334
|
+
Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
|
|
2335
|
+
|
|
2336
|
+
Args:
|
|
2337
|
+
period_start: Data inicial do período (YYYY-MM-DD)
|
|
2338
|
+
period_end: Data final do período (YYYY-MM-DD)
|
|
2339
|
+
search_type: Tipo de busca ("receivable", "payable", "both")
|
|
2340
|
+
amount_min: Valor mínimo (opcional)
|
|
2341
|
+
amount_max: Valor máximo (opcional)
|
|
2342
|
+
customer_creditor_search: Buscar por nome de cliente/credor (opcional)
|
|
2343
|
+
"""
|
|
2344
|
+
|
|
2345
|
+
financial_results = {
|
|
2346
|
+
"receivable": {"success": False, "data": [], "count": 0, "error": None},
|
|
2347
|
+
"payable": {"success": False, "data": [], "count": 0, "error": None}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
# Buscar contas a receber
|
|
2351
|
+
if search_type in ["receivable", "both"]:
|
|
2352
|
+
try:
|
|
2353
|
+
# CORRIGIDO: Usar serviço interno
|
|
2354
|
+
receivable_result = await _svc_get_accounts_receivable(
|
|
2355
|
+
start_date=period_start,
|
|
2356
|
+
end_date=period_end,
|
|
2357
|
+
selection_type="D" # Por vencimento
|
|
2358
|
+
)
|
|
2359
|
+
|
|
2360
|
+
if receivable_result["success"]:
|
|
2361
|
+
receivable_data = receivable_result.get("data", [])
|
|
2362
|
+
|
|
2363
|
+
# CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
|
|
2364
|
+
if amount_min is not None or amount_max is not None:
|
|
2365
|
+
filtered_data = []
|
|
2366
|
+
for item in receivable_data:
|
|
2367
|
+
amount = _parse_numeric_value(item.get("amount", 0))
|
|
2368
|
+
if amount_min is not None and amount < amount_min:
|
|
2369
|
+
continue
|
|
2370
|
+
if amount_max is not None and amount > amount_max:
|
|
2371
|
+
continue
|
|
2372
|
+
filtered_data.append(item)
|
|
2373
|
+
receivable_data = filtered_data
|
|
2374
|
+
|
|
2375
|
+
# Aplicar filtro de cliente se especificado
|
|
2376
|
+
if customer_creditor_search:
|
|
2377
|
+
search_lower = customer_creditor_search.lower()
|
|
2378
|
+
filtered_data = []
|
|
2379
|
+
for item in receivable_data:
|
|
2380
|
+
customer_name = str(item.get("customer_name", "")).lower()
|
|
2381
|
+
if search_lower in customer_name:
|
|
2382
|
+
filtered_data.append(item)
|
|
2383
|
+
receivable_data = filtered_data
|
|
2384
|
+
|
|
2385
|
+
financial_results["receivable"] = {
|
|
2386
|
+
"success": True,
|
|
2387
|
+
"data": receivable_data,
|
|
2388
|
+
"count": len(receivable_data),
|
|
2389
|
+
"error": None
|
|
2390
|
+
}
|
|
2391
|
+
else:
|
|
2392
|
+
financial_results["receivable"]["error"] = receivable_result.get("error")
|
|
2393
|
+
|
|
2394
|
+
except Exception as e:
|
|
2395
|
+
financial_results["receivable"]["error"] = str(e)
|
|
2396
|
+
|
|
2397
|
+
# Buscar contas a pagar
|
|
2398
|
+
if search_type in ["payable", "both"]:
|
|
2399
|
+
try:
|
|
2400
|
+
# CORRIGIDO: Usar serviço interno
|
|
2401
|
+
payable_result = await _svc_get_bills(
|
|
2402
|
+
start_date=period_start,
|
|
2403
|
+
end_date=period_end,
|
|
2404
|
+
limit=100
|
|
2405
|
+
)
|
|
2406
|
+
|
|
2407
|
+
if payable_result["success"]:
|
|
2408
|
+
data = payable_result["data"]
|
|
2409
|
+
payable_data = data.get("results", []) if isinstance(data, dict) else data
|
|
2410
|
+
|
|
2411
|
+
# CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
|
|
2412
|
+
if amount_min is not None or amount_max is not None:
|
|
2413
|
+
filtered_data = []
|
|
2414
|
+
for item in payable_data:
|
|
2415
|
+
amount = _parse_numeric_value(item.get("amount", 0))
|
|
2416
|
+
if amount_min is not None and amount < amount_min:
|
|
2417
|
+
continue
|
|
2418
|
+
if amount_max is not None and amount > amount_max:
|
|
2419
|
+
continue
|
|
2420
|
+
filtered_data.append(item)
|
|
2421
|
+
payable_data = filtered_data
|
|
2422
|
+
|
|
2423
|
+
# Aplicar filtro de credor se especificado
|
|
2424
|
+
if customer_creditor_search:
|
|
2425
|
+
search_lower = customer_creditor_search.lower()
|
|
2426
|
+
filtered_data = []
|
|
2427
|
+
for item in payable_data:
|
|
2428
|
+
creditor_name = str(item.get("creditor_name", "")).lower()
|
|
2429
|
+
if search_lower in creditor_name:
|
|
2430
|
+
filtered_data.append(item)
|
|
2431
|
+
payable_data = filtered_data
|
|
2432
|
+
|
|
2433
|
+
financial_results["payable"] = {
|
|
2434
|
+
"success": True,
|
|
2435
|
+
"data": payable_data,
|
|
2436
|
+
"count": len(payable_data),
|
|
2437
|
+
"error": None
|
|
2438
|
+
}
|
|
2439
|
+
else:
|
|
2440
|
+
financial_results["payable"]["error"] = payable_result.get("error")
|
|
2441
|
+
|
|
2442
|
+
except Exception as e:
|
|
2443
|
+
financial_results["payable"]["error"] = str(e)
|
|
2444
|
+
|
|
2445
|
+
# Compilar resultado final
|
|
2446
|
+
total_records = financial_results["receivable"]["count"] + financial_results["payable"]["count"]
|
|
2447
|
+
has_errors = bool(financial_results["receivable"]["error"] or financial_results["payable"]["error"])
|
|
2448
|
+
|
|
2449
|
+
summary = {
|
|
2450
|
+
"period": f"{period_start} a {period_end}",
|
|
2451
|
+
"search_type": search_type,
|
|
2452
|
+
"total_records": total_records,
|
|
2453
|
+
"receivable_count": financial_results["receivable"]["count"],
|
|
2454
|
+
"payable_count": financial_results["payable"]["count"],
|
|
2455
|
+
"filters_applied": {
|
|
2456
|
+
"amount_range": f"{amount_min or 'sem mín'} - {amount_max or 'sem máx'}",
|
|
2457
|
+
"customer_creditor": customer_creditor_search or "todos"
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
if total_records > 0:
|
|
2462
|
+
return {
|
|
2463
|
+
"success": True,
|
|
2464
|
+
"message": f"✅ Busca financeira encontrou {total_records} registros no período",
|
|
2465
|
+
"summary": summary,
|
|
2466
|
+
"receivable": financial_results["receivable"],
|
|
2467
|
+
"payable": financial_results["payable"],
|
|
2468
|
+
"has_errors": has_errors
|
|
2469
|
+
}
|
|
2470
|
+
else:
|
|
2471
|
+
return {
|
|
2472
|
+
"success": False,
|
|
2473
|
+
"message": f"❌ Nenhum registro financeiro encontrado no período {period_start} a {period_end}",
|
|
2474
|
+
"summary": summary,
|
|
2475
|
+
"errors": {
|
|
2476
|
+
"receivable": financial_results["receivable"]["error"],
|
|
2477
|
+
"payable": financial_results["payable"]["error"]
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
@mcp.tool
|
|
2483
|
+
async def get_sienge_dashboard_summary() -> Dict:
|
|
2484
|
+
"""
|
|
2485
|
+
Obtém um resumo tipo dashboard com informações gerais do Sienge
|
|
2486
|
+
Útil para visão geral rápida do sistema
|
|
2487
|
+
"""
|
|
2488
|
+
|
|
2489
|
+
# Data atual e períodos
|
|
2490
|
+
today = datetime.now()
|
|
2491
|
+
current_month_start = today.replace(day=1).strftime("%Y-%m-%d")
|
|
2492
|
+
current_month_end = today.strftime("%Y-%m-%d")
|
|
2493
|
+
|
|
2494
|
+
dashboard_data = {}
|
|
2495
|
+
errors = []
|
|
2496
|
+
|
|
2497
|
+
# 1. Testar conexão
|
|
2498
|
+
try:
|
|
2499
|
+
connection_test = await test_sienge_connection()
|
|
2500
|
+
dashboard_data["connection"] = connection_test
|
|
2501
|
+
except Exception as e:
|
|
2502
|
+
errors.append(f"Teste de conexão: {str(e)}")
|
|
2503
|
+
dashboard_data["connection"] = {"success": False, "error": str(e)}
|
|
2504
|
+
|
|
2505
|
+
# 2. Contar clientes (amostra)
|
|
2506
|
+
try:
|
|
2507
|
+
# CORRIGIDO: Usar serviço interno
|
|
2508
|
+
customers_result = await _svc_get_customers(limit=1)
|
|
2509
|
+
dashboard_data["customers"] = {"available": customers_result["success"]}
|
|
2510
|
+
except Exception as e:
|
|
2511
|
+
errors.append(f"Clientes: {str(e)}")
|
|
2512
|
+
dashboard_data["customers"] = {"available": False}
|
|
2513
|
+
|
|
2514
|
+
# 3. Contar projetos (amostra)
|
|
2515
|
+
try:
|
|
2516
|
+
# CORRIGIDO: Usar serviço interno
|
|
2517
|
+
projects_result = await _svc_get_projects(limit=5)
|
|
2518
|
+
if projects_result["success"]:
|
|
2519
|
+
data = projects_result["data"]
|
|
2520
|
+
enterprises = data.get("results", []) if isinstance(data, dict) else data
|
|
2521
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
2522
|
+
|
|
2523
|
+
dashboard_data["projects"] = {
|
|
2524
|
+
"available": True,
|
|
2525
|
+
"sample_count": len(enterprises),
|
|
2526
|
+
"total_count": metadata.get("count", "N/A")
|
|
2527
|
+
}
|
|
2528
|
+
else:
|
|
2529
|
+
dashboard_data["projects"] = {"available": False}
|
|
2530
|
+
except Exception as e:
|
|
2531
|
+
errors.append(f"Projetos: {str(e)}")
|
|
2532
|
+
dashboard_data["projects"] = {"available": False, "error": str(e)}
|
|
2533
|
+
|
|
2534
|
+
# 4. Títulos a pagar do mês atual
|
|
2535
|
+
try:
|
|
2536
|
+
# CORRIGIDO: Usar serviço interno
|
|
2537
|
+
bills_result = await _svc_get_bills(
|
|
2538
|
+
start_date=current_month_start,
|
|
2539
|
+
end_date=current_month_end,
|
|
2540
|
+
limit=10
|
|
2541
|
+
)
|
|
2542
|
+
if bills_result["success"]:
|
|
2543
|
+
data = bills_result["data"]
|
|
2544
|
+
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
2545
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
2546
|
+
|
|
2547
|
+
dashboard_data["monthly_bills"] = {
|
|
2548
|
+
"available": True,
|
|
2549
|
+
"count": len(bills),
|
|
2550
|
+
"total_count": metadata.get("count", len(bills))
|
|
2551
|
+
}
|
|
2552
|
+
else:
|
|
2553
|
+
dashboard_data["monthly_bills"] = {"available": False}
|
|
2554
|
+
except Exception as e:
|
|
2555
|
+
errors.append(f"Títulos mensais: {str(e)}")
|
|
2556
|
+
dashboard_data["monthly_bills"] = {"available": False, "error": str(e)}
|
|
2557
|
+
|
|
2558
|
+
# 5. Tipos de clientes
|
|
2559
|
+
try:
|
|
2560
|
+
# CORRIGIDO: Usar serviço interno
|
|
2561
|
+
customer_types_result = await _svc_get_customer_types()
|
|
2562
|
+
if customer_types_result["success"]:
|
|
2563
|
+
data = customer_types_result["data"]
|
|
2564
|
+
customer_types = data.get("results", []) if isinstance(data, dict) else data
|
|
2565
|
+
|
|
2566
|
+
dashboard_data["customer_types"] = {
|
|
2567
|
+
"available": True,
|
|
2568
|
+
"count": len(customer_types)
|
|
2569
|
+
}
|
|
2570
|
+
else:
|
|
2571
|
+
dashboard_data["customer_types"] = {"available": False}
|
|
2572
|
+
except Exception as e:
|
|
2573
|
+
dashboard_data["customer_types"] = {"available": False, "error": str(e)}
|
|
2574
|
+
|
|
2575
|
+
# Compilar resultado
|
|
2576
|
+
available_modules = sum(1 for key, value in dashboard_data.items()
|
|
2577
|
+
if key != "connection" and isinstance(value, dict) and value.get("available"))
|
|
2578
|
+
|
|
2579
|
+
return {
|
|
2580
|
+
"success": True,
|
|
2581
|
+
"message": f"✅ Dashboard do Sienge - {available_modules} módulos disponíveis",
|
|
2582
|
+
"timestamp": today.isoformat(),
|
|
2583
|
+
"period_analyzed": f"{current_month_start} a {current_month_end}",
|
|
2584
|
+
"modules_status": dashboard_data,
|
|
2585
|
+
"available_modules": available_modules,
|
|
2586
|
+
"errors": errors if errors else None,
|
|
2587
|
+
"quick_actions": [
|
|
2588
|
+
"search_sienge_data('termo_busca') - Busca universal",
|
|
2589
|
+
"list_sienge_entities() - Listar tipos de dados",
|
|
2590
|
+
"get_sienge_customers(search='nome') - Buscar clientes",
|
|
2591
|
+
"get_sienge_projects() - Listar projetos/obras",
|
|
2592
|
+
"search_sienge_financial_data('2024-01-01', '2024-12-31') - Dados financeiros"
|
|
2593
|
+
]
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
|
|
1119
2597
|
# ============ UTILITÁRIOS ============
|
|
1120
2598
|
|
|
2599
|
+
|
|
1121
2600
|
@mcp.tool
|
|
1122
2601
|
def add(a: int, b: int) -> int:
|
|
1123
2602
|
"""Soma dois números (função de teste)"""
|
|
1124
2603
|
return a + b
|
|
1125
2604
|
|
|
2605
|
+
|
|
2606
|
+
def _mask(s: str) -> str:
|
|
2607
|
+
"""Mascara dados sensíveis mantendo apenas o início e fim"""
|
|
2608
|
+
if not s:
|
|
2609
|
+
return None
|
|
2610
|
+
if len(s) == 1:
|
|
2611
|
+
return s + "*"
|
|
2612
|
+
if len(s) == 2:
|
|
2613
|
+
return s
|
|
2614
|
+
if len(s) <= 4:
|
|
2615
|
+
return s[:2] + "*" * (len(s) - 2)
|
|
2616
|
+
# Para strings > 4: usar no máximo 4 asteriscos no meio
|
|
2617
|
+
middle_asterisks = min(4, len(s) - 4)
|
|
2618
|
+
return s[:2] + "*" * middle_asterisks + s[-2:]
|
|
2619
|
+
|
|
2620
|
+
|
|
1126
2621
|
def _get_auth_info_internal() -> Dict:
|
|
1127
2622
|
"""Função interna para verificar configuração de autenticação"""
|
|
1128
2623
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
1129
|
-
return {
|
|
1130
|
-
"auth_method": "Bearer Token",
|
|
1131
|
-
"configured": True,
|
|
1132
|
-
"base_url": SIENGE_BASE_URL,
|
|
1133
|
-
"api_key_configured": True
|
|
1134
|
-
}
|
|
2624
|
+
return {"auth_method": "Bearer Token", "configured": True, "base_url": SIENGE_BASE_URL, "api_key_configured": True}
|
|
1135
2625
|
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
1136
2626
|
return {
|
|
1137
2627
|
"auth_method": "Basic Auth",
|
|
1138
2628
|
"configured": True,
|
|
1139
2629
|
"base_url": SIENGE_BASE_URL,
|
|
1140
2630
|
"subdomain": SIENGE_SUBDOMAIN,
|
|
1141
|
-
"username": SIENGE_USERNAME
|
|
2631
|
+
"username": _mask(SIENGE_USERNAME) if SIENGE_USERNAME else None,
|
|
1142
2632
|
}
|
|
1143
2633
|
else:
|
|
1144
2634
|
return {
|
|
1145
2635
|
"auth_method": "None",
|
|
1146
2636
|
"configured": False,
|
|
1147
|
-
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env"
|
|
2637
|
+
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
1148
2638
|
}
|
|
1149
2639
|
|
|
2640
|
+
|
|
1150
2641
|
@mcp.tool
|
|
1151
2642
|
def get_auth_info() -> Dict:
|
|
1152
2643
|
"""Retorna informações sobre a configuração de autenticação"""
|
|
1153
2644
|
return _get_auth_info_internal()
|
|
1154
2645
|
|
|
2646
|
+
|
|
1155
2647
|
def main():
|
|
1156
2648
|
"""Entry point for the Sienge MCP Server"""
|
|
1157
2649
|
print("* Iniciando Sienge MCP Server (FastMCP)...")
|
|
1158
|
-
|
|
2650
|
+
|
|
1159
2651
|
# Mostrar info de configuração
|
|
1160
2652
|
auth_info = _get_auth_info_internal()
|
|
1161
2653
|
print(f"* Autenticacao: {auth_info['auth_method']}")
|
|
1162
2654
|
print(f"* Configurado: {auth_info['configured']}")
|
|
1163
|
-
|
|
1164
|
-
if not auth_info[
|
|
2655
|
+
|
|
2656
|
+
if not auth_info["configured"]:
|
|
1165
2657
|
print("* ERRO: Autenticacao nao configurada!")
|
|
1166
2658
|
print("Configure as variáveis de ambiente:")
|
|
1167
2659
|
print("- SIENGE_API_KEY (Bearer Token) OU")
|
|
@@ -1179,8 +2671,9 @@ def main():
|
|
|
1179
2671
|
print('export SIENGE_SUBDOMAIN="sua_empresa"')
|
|
1180
2672
|
else:
|
|
1181
2673
|
print("* MCP pronto para uso!")
|
|
1182
|
-
|
|
2674
|
+
|
|
1183
2675
|
mcp.run()
|
|
1184
2676
|
|
|
2677
|
+
|
|
1185
2678
|
if __name__ == "__main__":
|
|
1186
|
-
main()
|
|
2679
|
+
main()
|