sienge-ecbiesek-mcp 1.1.5__py3-none-any.whl → 1.2.1__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.5.dist-info → sienge_ecbiesek_mcp-1.2.1.dist-info}/METADATA +3 -24
- sienge_ecbiesek_mcp-1.2.1.dist-info/RECORD +11 -0
- sienge_mcp/server.py +1115 -219
- sienge_mcp/server2.py +2694 -0
- sienge_ecbiesek_mcp-1.1.5.dist-info/RECORD +0 -10
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.1.dist-info}/WHEEL +0 -0
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.1.dist-info}/entry_points.txt +0 -0
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.1.dist-info}/top_level.txt +0 -0
sienge_mcp/server.py
CHANGED
|
@@ -2,14 +2,27 @@
|
|
|
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
|
-
from typing import Dict, List, Optional, Any
|
|
16
|
+
from typing import Dict, List, Optional, Any, Union
|
|
10
17
|
import os
|
|
11
18
|
from dotenv import load_dotenv
|
|
12
|
-
from datetime import datetime
|
|
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
|
|
13
26
|
|
|
14
27
|
# Carrega as variáveis de ambiente
|
|
15
28
|
load_dotenv()
|
|
@@ -24,61 +37,273 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
|
|
|
24
37
|
SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
|
|
25
38
|
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
|
|
26
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
|
+
|
|
27
51
|
|
|
28
52
|
class SiengeAPIError(Exception):
|
|
29
53
|
"""Exceção customizada para erros da API do Sienge"""
|
|
30
|
-
|
|
31
54
|
pass
|
|
32
55
|
|
|
33
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
|
+
|
|
34
190
|
async def make_sienge_request(
|
|
35
|
-
method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
|
|
191
|
+
method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None, use_cache: bool = True
|
|
36
192
|
) -> Dict:
|
|
37
193
|
"""
|
|
38
194
|
Função auxiliar para fazer requisições à API do Sienge (v1)
|
|
39
195
|
Suporta tanto Bearer Token quanto Basic Auth
|
|
196
|
+
MELHORADO: Observabilidade, cache, normalização de parâmetros
|
|
40
197
|
"""
|
|
198
|
+
start_time = time.time()
|
|
199
|
+
req_id = str(uuid.uuid4())
|
|
200
|
+
|
|
41
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
|
+
|
|
42
214
|
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
43
|
-
headers = {
|
|
215
|
+
headers = {
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
"Accept": "application/json",
|
|
218
|
+
"X-Request-ID": req_id
|
|
219
|
+
}
|
|
44
220
|
|
|
45
|
-
# Configurar autenticação e URL
|
|
221
|
+
# Configurar autenticação e URL (corrigindo URLs duplas)
|
|
46
222
|
auth = None
|
|
223
|
+
base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
|
|
47
224
|
|
|
48
225
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
49
|
-
# Bearer Token (Recomendado)
|
|
50
226
|
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
51
|
-
url = f"{
|
|
227
|
+
url = f"{base_normalized}/v1{endpoint}"
|
|
52
228
|
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
53
|
-
# Basic Auth usando httpx.BasicAuth
|
|
54
229
|
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
55
|
-
url = f"{
|
|
230
|
+
url = f"{base_normalized}/v1{endpoint}"
|
|
56
231
|
else:
|
|
57
232
|
return {
|
|
58
233
|
"success": False,
|
|
59
234
|
"error": "No Authentication",
|
|
60
235
|
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
236
|
+
"request_id": req_id
|
|
61
237
|
}
|
|
62
238
|
|
|
63
|
-
response = await client.request(
|
|
239
|
+
response = await client.request(
|
|
240
|
+
method=method,
|
|
241
|
+
url=url,
|
|
242
|
+
headers=headers,
|
|
243
|
+
params=normalized_params,
|
|
244
|
+
json=json_data,
|
|
245
|
+
auth=auth
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
latency = time.time() - start_time
|
|
249
|
+
_log_request(method, endpoint, response.status_code, latency, req_id)
|
|
64
250
|
|
|
65
251
|
if response.status_code in [200, 201]:
|
|
66
252
|
try:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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)
|
|
260
|
+
}
|
|
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:
|
|
270
|
+
return {
|
|
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)
|
|
276
|
+
}
|
|
70
277
|
else:
|
|
71
278
|
return {
|
|
72
279
|
"success": False,
|
|
73
280
|
"error": f"HTTP {response.status_code}",
|
|
74
281
|
"message": response.text,
|
|
75
282
|
"status_code": response.status_code,
|
|
283
|
+
"request_id": req_id,
|
|
284
|
+
"latency_ms": round(latency * 1000, 2)
|
|
76
285
|
}
|
|
77
286
|
|
|
78
287
|
except httpx.TimeoutException:
|
|
79
|
-
|
|
288
|
+
latency = time.time() - start_time
|
|
289
|
+
_log_request(method, endpoint, 408, latency, req_id)
|
|
290
|
+
return {
|
|
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)
|
|
296
|
+
}
|
|
80
297
|
except Exception as e:
|
|
81
|
-
|
|
298
|
+
latency = time.time() - start_time
|
|
299
|
+
_log_request(method, endpoint, 500, latency, req_id)
|
|
300
|
+
return {
|
|
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)
|
|
306
|
+
}
|
|
82
307
|
|
|
83
308
|
|
|
84
309
|
async def make_sienge_bulk_request(
|
|
@@ -87,59 +312,359 @@ async def make_sienge_bulk_request(
|
|
|
87
312
|
"""
|
|
88
313
|
Função auxiliar para fazer requisições à API bulk-data do Sienge
|
|
89
314
|
Suporta tanto Bearer Token quanto Basic Auth
|
|
315
|
+
MELHORADO: Observabilidade e normalização de parâmetros
|
|
90
316
|
"""
|
|
317
|
+
start_time = time.time()
|
|
318
|
+
req_id = str(uuid.uuid4())
|
|
319
|
+
|
|
91
320
|
try:
|
|
321
|
+
# Normalizar parâmetros
|
|
322
|
+
normalized_params = to_query(params) if params else None
|
|
323
|
+
|
|
92
324
|
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
93
|
-
headers = {
|
|
325
|
+
headers = {
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
"Accept": "application/json",
|
|
328
|
+
"X-Request-ID": req_id
|
|
329
|
+
}
|
|
94
330
|
|
|
95
|
-
# Configurar autenticação e URL para bulk-data
|
|
331
|
+
# Configurar autenticação e URL para bulk-data (corrigindo URLs duplas)
|
|
96
332
|
auth = None
|
|
333
|
+
base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
|
|
97
334
|
|
|
98
335
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
99
|
-
# Bearer Token (Recomendado)
|
|
100
336
|
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
101
|
-
url = f"{
|
|
337
|
+
url = f"{base_normalized}/bulk-data/v1{endpoint}"
|
|
102
338
|
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
103
|
-
# Basic Auth usando httpx.BasicAuth
|
|
104
339
|
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
105
|
-
url = f"{
|
|
340
|
+
url = f"{base_normalized}/bulk-data/v1{endpoint}"
|
|
106
341
|
else:
|
|
107
342
|
return {
|
|
108
343
|
"success": False,
|
|
109
344
|
"error": "No Authentication",
|
|
110
345
|
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
346
|
+
"request_id": req_id
|
|
111
347
|
}
|
|
112
348
|
|
|
113
|
-
response = await client.request(
|
|
349
|
+
response = await client.request(
|
|
350
|
+
method=method,
|
|
351
|
+
url=url,
|
|
352
|
+
headers=headers,
|
|
353
|
+
params=normalized_params,
|
|
354
|
+
json=json_data,
|
|
355
|
+
auth=auth
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
latency = time.time() - start_time
|
|
359
|
+
_log_request(method, f"bulk-data{endpoint}", response.status_code, latency, req_id)
|
|
114
360
|
|
|
115
|
-
if response.status_code in [200, 201]:
|
|
361
|
+
if response.status_code in [200, 201, 202]:
|
|
116
362
|
try:
|
|
117
|
-
return {
|
|
118
|
-
|
|
119
|
-
|
|
363
|
+
return {
|
|
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)
|
|
369
|
+
}
|
|
370
|
+
except Exception:
|
|
371
|
+
return {
|
|
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)
|
|
377
|
+
}
|
|
120
378
|
else:
|
|
121
379
|
return {
|
|
122
380
|
"success": False,
|
|
123
381
|
"error": f"HTTP {response.status_code}",
|
|
124
382
|
"message": response.text,
|
|
125
383
|
"status_code": response.status_code,
|
|
384
|
+
"request_id": req_id,
|
|
385
|
+
"latency_ms": round(latency * 1000, 2)
|
|
126
386
|
}
|
|
127
387
|
|
|
128
388
|
except httpx.TimeoutException:
|
|
129
|
-
|
|
389
|
+
latency = time.time() - start_time
|
|
390
|
+
_log_request(method, f"bulk-data{endpoint}", 408, latency, req_id)
|
|
391
|
+
return {
|
|
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)
|
|
397
|
+
}
|
|
130
398
|
except Exception as e:
|
|
131
|
-
|
|
399
|
+
latency = time.time() - start_time
|
|
400
|
+
_log_request(method, f"bulk-data{endpoint}", 500, latency, req_id)
|
|
401
|
+
return {
|
|
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)
|
|
407
|
+
}
|
|
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)
|
|
132
657
|
|
|
133
658
|
|
|
134
659
|
# ============ CONEXÃO E TESTE ============
|
|
135
660
|
|
|
136
661
|
|
|
137
662
|
@mcp.tool
|
|
138
|
-
async def test_sienge_connection() -> Dict:
|
|
663
|
+
async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
|
|
139
664
|
"""Testa a conexão com a API do Sienge"""
|
|
140
665
|
try:
|
|
141
|
-
#
|
|
142
|
-
result = await
|
|
666
|
+
# Usar serviço interno
|
|
667
|
+
result = await _svc_get_customer_types()
|
|
143
668
|
|
|
144
669
|
if result["success"]:
|
|
145
670
|
auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
|
|
@@ -149,6 +674,9 @@ async def test_sienge_connection() -> Dict:
|
|
|
149
674
|
"api_status": "Online",
|
|
150
675
|
"auth_method": auth_method,
|
|
151
676
|
"timestamp": datetime.now().isoformat(),
|
|
677
|
+
"request_id": result.get("request_id"),
|
|
678
|
+
"latency_ms": result.get("latency_ms"),
|
|
679
|
+
"cache": result.get("cache")
|
|
152
680
|
}
|
|
153
681
|
else:
|
|
154
682
|
return {
|
|
@@ -157,6 +685,7 @@ async def test_sienge_connection() -> Dict:
|
|
|
157
685
|
"error": result.get("error"),
|
|
158
686
|
"details": result.get("message"),
|
|
159
687
|
"timestamp": datetime.now().isoformat(),
|
|
688
|
+
"request_id": result.get("request_id")
|
|
160
689
|
}
|
|
161
690
|
except Exception as e:
|
|
162
691
|
return {
|
|
@@ -172,7 +701,8 @@ async def test_sienge_connection() -> Dict:
|
|
|
172
701
|
|
|
173
702
|
@mcp.tool
|
|
174
703
|
async def get_sienge_customers(
|
|
175
|
-
limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None
|
|
704
|
+
limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None, customer_type_id: Optional[str] = None,
|
|
705
|
+
_meta: Optional[Dict] = None
|
|
176
706
|
) -> Dict:
|
|
177
707
|
"""
|
|
178
708
|
Busca clientes no Sienge com filtros
|
|
@@ -183,14 +713,13 @@ async def get_sienge_customers(
|
|
|
183
713
|
search: Buscar por nome ou documento
|
|
184
714
|
customer_type_id: Filtrar por tipo de cliente
|
|
185
715
|
"""
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
result = await make_sienge_request("GET", "/customers", params=params)
|
|
716
|
+
# Usar serviço interno
|
|
717
|
+
result = await _svc_get_customers(
|
|
718
|
+
limit=limit or 50,
|
|
719
|
+
offset=offset or 0,
|
|
720
|
+
search=search,
|
|
721
|
+
customer_type_id=customer_type_id
|
|
722
|
+
)
|
|
194
723
|
|
|
195
724
|
if result["success"]:
|
|
196
725
|
data = result["data"]
|
|
@@ -203,7 +732,13 @@ async def get_sienge_customers(
|
|
|
203
732
|
"message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
|
|
204
733
|
"customers": customers,
|
|
205
734
|
"count": len(customers),
|
|
206
|
-
"
|
|
735
|
+
"total_count": total_count,
|
|
736
|
+
"filters_applied": {
|
|
737
|
+
"limit": limit, "offset": offset, "search": search, "customer_type_id": customer_type_id
|
|
738
|
+
},
|
|
739
|
+
"request_id": result.get("request_id"),
|
|
740
|
+
"latency_ms": result.get("latency_ms"),
|
|
741
|
+
"cache": result.get("cache")
|
|
207
742
|
}
|
|
208
743
|
|
|
209
744
|
return {
|
|
@@ -211,13 +746,15 @@ async def get_sienge_customers(
|
|
|
211
746
|
"message": "❌ Erro ao buscar clientes",
|
|
212
747
|
"error": result.get("error"),
|
|
213
748
|
"details": result.get("message"),
|
|
749
|
+
"request_id": result.get("request_id")
|
|
214
750
|
}
|
|
215
751
|
|
|
216
752
|
|
|
217
753
|
@mcp.tool
|
|
218
|
-
async def get_sienge_customer_types() -> Dict:
|
|
754
|
+
async def get_sienge_customer_types(_meta: Optional[Dict] = None) -> Dict:
|
|
219
755
|
"""Lista tipos de clientes disponíveis"""
|
|
220
|
-
|
|
756
|
+
# Usar serviço interno
|
|
757
|
+
result = await _svc_get_customer_types()
|
|
221
758
|
|
|
222
759
|
if result["success"]:
|
|
223
760
|
data = result["data"]
|
|
@@ -230,6 +767,10 @@ async def get_sienge_customer_types() -> Dict:
|
|
|
230
767
|
"message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
|
|
231
768
|
"customer_types": customer_types,
|
|
232
769
|
"count": len(customer_types),
|
|
770
|
+
"total_count": total_count,
|
|
771
|
+
"request_id": result.get("request_id"),
|
|
772
|
+
"latency_ms": result.get("latency_ms"),
|
|
773
|
+
"cache": result.get("cache")
|
|
233
774
|
}
|
|
234
775
|
|
|
235
776
|
return {
|
|
@@ -237,6 +778,195 @@ async def get_sienge_customer_types() -> Dict:
|
|
|
237
778
|
"message": "❌ Erro ao buscar tipos de clientes",
|
|
238
779
|
"error": result.get("error"),
|
|
239
780
|
"details": result.get("message"),
|
|
781
|
+
"request_id": result.get("request_id")
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# ============ ALIAS COMPATÍVEIS COM CHECKLIST ============
|
|
786
|
+
|
|
787
|
+
@mcp.tool
|
|
788
|
+
async def get_sienge_enterprises(
|
|
789
|
+
limit: int = 100, offset: int = 0, company_id: int = None, enterprise_type: int = None,
|
|
790
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
791
|
+
"""
|
|
792
|
+
ALIAS: get_sienge_projects → get_sienge_enterprises
|
|
793
|
+
Busca empreendimentos/obras (compatibilidade com checklist)
|
|
794
|
+
"""
|
|
795
|
+
return await get_sienge_projects(
|
|
796
|
+
limit=limit,
|
|
797
|
+
offset=offset,
|
|
798
|
+
company_id=company_id,
|
|
799
|
+
enterprise_type=enterprise_type
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
@mcp.tool
|
|
804
|
+
async def get_sienge_suppliers(
|
|
805
|
+
limit: int = 50,
|
|
806
|
+
offset: int = 0,
|
|
807
|
+
search: str = None,
|
|
808
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
809
|
+
"""
|
|
810
|
+
ALIAS: get_sienge_creditors → get_sienge_suppliers
|
|
811
|
+
Busca fornecedores (compatibilidade com checklist)
|
|
812
|
+
"""
|
|
813
|
+
return await get_sienge_creditors(limit=limit, offset=offset, search=search)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@mcp.tool
|
|
817
|
+
async def search_sienge_finances(
|
|
818
|
+
period_start: str,
|
|
819
|
+
period_end: str,
|
|
820
|
+
account_type: Optional[str] = None,
|
|
821
|
+
cost_center: Optional[str] = None, # ignorado por enquanto (não suportado na API atual)
|
|
822
|
+
amount_filter: Optional[str] = None,
|
|
823
|
+
customer_creditor: Optional[str] = None
|
|
824
|
+
) -> Dict:
|
|
825
|
+
"""
|
|
826
|
+
ALIAS: search_sienge_financial_data → search_sienge_finances
|
|
827
|
+
- account_type: receivable | payable | both
|
|
828
|
+
- amount_filter: "100..500", ">=1000", "<=500", ">100", "<200", "=750"
|
|
829
|
+
- customer_creditor: termo de busca (cliente/credor)
|
|
830
|
+
"""
|
|
831
|
+
# 1) mapear tipo
|
|
832
|
+
search_type = (account_type or "both").lower()
|
|
833
|
+
if search_type not in {"receivable", "payable", "both"}:
|
|
834
|
+
search_type = "both"
|
|
835
|
+
|
|
836
|
+
# 2) parse de faixa de valores
|
|
837
|
+
amount_min = amount_max = None
|
|
838
|
+
if amount_filter:
|
|
839
|
+
s = amount_filter.replace(" ", "")
|
|
840
|
+
try:
|
|
841
|
+
if ".." in s:
|
|
842
|
+
lo, hi = s.split("..", 1)
|
|
843
|
+
amount_min = float(lo) if lo else None
|
|
844
|
+
amount_max = float(hi) if hi else None
|
|
845
|
+
elif s.startswith(">="):
|
|
846
|
+
amount_min = float(s[2:])
|
|
847
|
+
elif s.startswith("<="):
|
|
848
|
+
amount_max = float(s[2:])
|
|
849
|
+
elif s.startswith(">"):
|
|
850
|
+
# >x → min = x (estrito não suportado; aproximamos)
|
|
851
|
+
amount_min = float(s[1:])
|
|
852
|
+
elif s.startswith("<"):
|
|
853
|
+
amount_max = float(s[1:])
|
|
854
|
+
elif s.startswith("="):
|
|
855
|
+
v = float(s[1:])
|
|
856
|
+
amount_min = v
|
|
857
|
+
amount_max = v
|
|
858
|
+
else:
|
|
859
|
+
# número puro → min
|
|
860
|
+
amount_min = float(s)
|
|
861
|
+
except ValueError:
|
|
862
|
+
# filtro inválido → ignora silenciosamente
|
|
863
|
+
amount_min = amount_max = None
|
|
864
|
+
|
|
865
|
+
return await search_sienge_financial_data(
|
|
866
|
+
period_start=period_start,
|
|
867
|
+
period_end=period_end,
|
|
868
|
+
search_type=search_type,
|
|
869
|
+
amount_min=amount_min,
|
|
870
|
+
amount_max=amount_max,
|
|
871
|
+
customer_creditor_search=customer_creditor
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
@mcp.tool
|
|
876
|
+
async def get_sienge_accounts_payable(
|
|
877
|
+
start_date: str = None, end_date: str = None, creditor_id: str = None,
|
|
878
|
+
status: str = None, limit: int = 50,
|
|
879
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
880
|
+
"""
|
|
881
|
+
ALIAS: get_sienge_bills → get_sienge_accounts_payable
|
|
882
|
+
Busca contas a pagar (compatibilidade com checklist)
|
|
883
|
+
"""
|
|
884
|
+
return await get_sienge_bills(
|
|
885
|
+
start_date=start_date,
|
|
886
|
+
end_date=end_date,
|
|
887
|
+
creditor_id=creditor_id,
|
|
888
|
+
status=status,
|
|
889
|
+
limit=limit
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
@mcp.tool
|
|
894
|
+
async def list_sienge_purchase_invoices(limit: int = 50, date_from: str = None,
|
|
895
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
896
|
+
"""
|
|
897
|
+
Lista notas fiscais de compra (versão list/plural esperada pelo checklist)
|
|
898
|
+
|
|
899
|
+
Args:
|
|
900
|
+
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
901
|
+
date_from: Data inicial (YYYY-MM-DD)
|
|
902
|
+
"""
|
|
903
|
+
# Usar serviço interno
|
|
904
|
+
result = await _svc_get_purchase_invoices(limit=limit, date_from=date_from)
|
|
905
|
+
|
|
906
|
+
if result["success"]:
|
|
907
|
+
data = result["data"]
|
|
908
|
+
invoices = data.get("results", []) if isinstance(data, dict) else data
|
|
909
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
910
|
+
total_count = metadata.get("count", len(invoices))
|
|
911
|
+
|
|
912
|
+
return {
|
|
913
|
+
"success": True,
|
|
914
|
+
"message": f"✅ Encontradas {len(invoices)} notas fiscais de compra (total: {total_count})",
|
|
915
|
+
"purchase_invoices": invoices,
|
|
916
|
+
"count": len(invoices),
|
|
917
|
+
"total_count": total_count,
|
|
918
|
+
"filters_applied": {"limit": limit, "date_from": date_from},
|
|
919
|
+
"request_id": result.get("request_id"),
|
|
920
|
+
"latency_ms": result.get("latency_ms"),
|
|
921
|
+
"cache": result.get("cache")
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
"success": False,
|
|
926
|
+
"message": "❌ Erro ao buscar notas fiscais de compra",
|
|
927
|
+
"error": result.get("error"),
|
|
928
|
+
"details": result.get("message"),
|
|
929
|
+
"request_id": result.get("request_id")
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
@mcp.tool
|
|
934
|
+
async def list_sienge_purchase_requests(limit: int = 50, status: str = None,
|
|
935
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
936
|
+
"""
|
|
937
|
+
Lista solicitações de compra (versão list/plural esperada pelo checklist)
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
941
|
+
status: Status da solicitação
|
|
942
|
+
"""
|
|
943
|
+
# Usar serviço interno
|
|
944
|
+
result = await _svc_get_purchase_requests(limit=limit, status=status)
|
|
945
|
+
|
|
946
|
+
if result["success"]:
|
|
947
|
+
data = result["data"]
|
|
948
|
+
requests = data.get("results", []) if isinstance(data, dict) else data
|
|
949
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
950
|
+
total_count = metadata.get("count", len(requests))
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
"success": True,
|
|
954
|
+
"message": f"✅ Encontradas {len(requests)} solicitações de compra (total: {total_count})",
|
|
955
|
+
"purchase_requests": requests,
|
|
956
|
+
"count": len(requests),
|
|
957
|
+
"total_count": total_count,
|
|
958
|
+
"filters_applied": {"limit": limit, "status": status},
|
|
959
|
+
"request_id": result.get("request_id"),
|
|
960
|
+
"latency_ms": result.get("latency_ms"),
|
|
961
|
+
"cache": result.get("cache")
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
"success": False,
|
|
966
|
+
"message": "❌ Erro ao buscar solicitações de compra",
|
|
967
|
+
"error": result.get("error"),
|
|
968
|
+
"details": result.get("message"),
|
|
969
|
+
"request_id": result.get("request_id")
|
|
240
970
|
}
|
|
241
971
|
|
|
242
972
|
|
|
@@ -244,7 +974,8 @@ async def get_sienge_customer_types() -> Dict:
|
|
|
244
974
|
|
|
245
975
|
|
|
246
976
|
@mcp.tool
|
|
247
|
-
async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None
|
|
977
|
+
async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int] = 0, search: Optional[str] = None,
|
|
978
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
248
979
|
"""
|
|
249
980
|
Busca credores/fornecedores
|
|
250
981
|
|
|
@@ -253,11 +984,12 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
253
984
|
offset: Pular registros (padrão: 0)
|
|
254
985
|
search: Buscar por nome
|
|
255
986
|
"""
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
987
|
+
# Usar serviço interno
|
|
988
|
+
result = await _svc_get_creditors(
|
|
989
|
+
limit=limit or 50,
|
|
990
|
+
offset=offset or 0,
|
|
991
|
+
search=search
|
|
992
|
+
)
|
|
261
993
|
|
|
262
994
|
if result["success"]:
|
|
263
995
|
data = result["data"]
|
|
@@ -270,6 +1002,11 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
270
1002
|
"message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
|
|
271
1003
|
"creditors": creditors,
|
|
272
1004
|
"count": len(creditors),
|
|
1005
|
+
"total_count": total_count,
|
|
1006
|
+
"filters_applied": {"limit": limit, "offset": offset, "search": search},
|
|
1007
|
+
"request_id": result.get("request_id"),
|
|
1008
|
+
"latency_ms": result.get("latency_ms"),
|
|
1009
|
+
"cache": result.get("cache")
|
|
273
1010
|
}
|
|
274
1011
|
|
|
275
1012
|
return {
|
|
@@ -277,18 +1014,21 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
277
1014
|
"message": "❌ Erro ao buscar credores",
|
|
278
1015
|
"error": result.get("error"),
|
|
279
1016
|
"details": result.get("message"),
|
|
1017
|
+
"request_id": result.get("request_id")
|
|
280
1018
|
}
|
|
281
1019
|
|
|
282
1020
|
|
|
283
1021
|
@mcp.tool
|
|
284
|
-
async def get_sienge_creditor_bank_info(creditor_id: str
|
|
1022
|
+
async def get_sienge_creditor_bank_info(creditor_id: str,
|
|
1023
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
285
1024
|
"""
|
|
286
1025
|
Consulta informações bancárias de um credor
|
|
287
1026
|
|
|
288
1027
|
Args:
|
|
289
1028
|
creditor_id: ID do credor (obrigatório)
|
|
290
1029
|
"""
|
|
291
|
-
|
|
1030
|
+
# Usar serviço interno
|
|
1031
|
+
result = await _svc_get_creditor_bank_info(creditor_id=creditor_id)
|
|
292
1032
|
|
|
293
1033
|
if result["success"]:
|
|
294
1034
|
return {
|
|
@@ -296,6 +1036,8 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
|
|
|
296
1036
|
"message": f"✅ Informações bancárias do credor {creditor_id}",
|
|
297
1037
|
"creditor_id": creditor_id,
|
|
298
1038
|
"bank_info": result["data"],
|
|
1039
|
+
"request_id": result.get("request_id"),
|
|
1040
|
+
"latency_ms": result.get("latency_ms")
|
|
299
1041
|
}
|
|
300
1042
|
|
|
301
1043
|
return {
|
|
@@ -303,6 +1045,7 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
|
|
|
303
1045
|
"message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
|
|
304
1046
|
"error": result.get("error"),
|
|
305
1047
|
"details": result.get("message"),
|
|
1048
|
+
"request_id": result.get("request_id")
|
|
306
1049
|
}
|
|
307
1050
|
|
|
308
1051
|
|
|
@@ -323,9 +1066,10 @@ async def get_sienge_accounts_receivable(
|
|
|
323
1066
|
origins_ids: Optional[List[str]] = None,
|
|
324
1067
|
bearers_id_in: Optional[List[int]] = None,
|
|
325
1068
|
bearers_id_not_in: Optional[List[int]] = None,
|
|
326
|
-
) -> Dict:
|
|
1069
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
327
1070
|
"""
|
|
328
1071
|
Consulta parcelas do contas a receber via API bulk-data
|
|
1072
|
+
MELHORADO: Suporte a polling assíncrono para requests 202
|
|
329
1073
|
|
|
330
1074
|
Args:
|
|
331
1075
|
start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
|
|
@@ -341,90 +1085,121 @@ async def get_sienge_accounts_receivable(
|
|
|
341
1085
|
bearers_id_in: Filtrar parcelas com códigos de portador específicos
|
|
342
1086
|
bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
|
|
343
1087
|
"""
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
params["originsIds"] = origins_ids
|
|
360
|
-
if bearers_id_in:
|
|
361
|
-
params["bearersIdIn"] = bearers_id_in
|
|
362
|
-
if bearers_id_not_in:
|
|
363
|
-
params["bearersIdNotIn"] = bearers_id_not_in
|
|
364
|
-
|
|
365
|
-
result = await make_sienge_bulk_request("GET", "/income", params=params)
|
|
1088
|
+
# Usar serviço interno com polling assíncrono
|
|
1089
|
+
result = await _svc_get_accounts_receivable(
|
|
1090
|
+
start_date=start_date,
|
|
1091
|
+
end_date=end_date,
|
|
1092
|
+
selection_type=selection_type,
|
|
1093
|
+
company_id=company_id,
|
|
1094
|
+
cost_centers_id=cost_centers_id,
|
|
1095
|
+
correction_indexer_id=correction_indexer_id,
|
|
1096
|
+
correction_date=correction_date,
|
|
1097
|
+
change_start_date=change_start_date,
|
|
1098
|
+
completed_bills=completed_bills,
|
|
1099
|
+
origins_ids=origins_ids,
|
|
1100
|
+
bearers_id_in=bearers_id_in,
|
|
1101
|
+
bearers_id_not_in=bearers_id_not_in
|
|
1102
|
+
)
|
|
366
1103
|
|
|
367
1104
|
if result["success"]:
|
|
368
|
-
|
|
369
|
-
income_data =
|
|
370
|
-
|
|
371
|
-
|
|
1105
|
+
# Para requests normais (200) e assíncronos processados
|
|
1106
|
+
income_data = result.get("data", [])
|
|
1107
|
+
|
|
1108
|
+
response = {
|
|
372
1109
|
"success": True,
|
|
373
1110
|
"message": f"✅ Encontradas {len(income_data)} parcelas a receber",
|
|
374
1111
|
"income_data": income_data,
|
|
375
1112
|
"count": len(income_data),
|
|
376
1113
|
"period": f"{start_date} a {end_date}",
|
|
377
1114
|
"selection_type": selection_type,
|
|
378
|
-
"
|
|
1115
|
+
"request_id": result.get("request_id"),
|
|
1116
|
+
"latency_ms": result.get("latency_ms")
|
|
379
1117
|
}
|
|
1118
|
+
|
|
1119
|
+
# Se foi processamento assíncrono, incluir informações extras
|
|
1120
|
+
if result.get("async_identifier"):
|
|
1121
|
+
response.update({
|
|
1122
|
+
"async_processing": {
|
|
1123
|
+
"identifier": result.get("async_identifier"),
|
|
1124
|
+
"correlation_id": result.get("correlation_id"),
|
|
1125
|
+
"chunks_downloaded": result.get("chunks_downloaded"),
|
|
1126
|
+
"rows_returned": result.get("rows_returned"),
|
|
1127
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1128
|
+
}
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
return response
|
|
380
1132
|
|
|
381
1133
|
return {
|
|
382
1134
|
"success": False,
|
|
383
1135
|
"message": "❌ Erro ao buscar parcelas a receber",
|
|
384
1136
|
"error": result.get("error"),
|
|
385
1137
|
"details": result.get("message"),
|
|
1138
|
+
"request_id": result.get("request_id"),
|
|
1139
|
+
"async_info": {
|
|
1140
|
+
"identifier": result.get("async_identifier"),
|
|
1141
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1142
|
+
} if result.get("async_identifier") else None
|
|
386
1143
|
}
|
|
387
1144
|
|
|
388
1145
|
|
|
389
1146
|
@mcp.tool
|
|
390
1147
|
async def get_sienge_accounts_receivable_by_bills(
|
|
391
|
-
bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None
|
|
392
|
-
) -> Dict:
|
|
1148
|
+
bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None,
|
|
1149
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
393
1150
|
"""
|
|
394
1151
|
Consulta parcelas dos títulos informados via API bulk-data
|
|
1152
|
+
MELHORADO: Suporte a polling assíncrono para requests 202
|
|
395
1153
|
|
|
396
1154
|
Args:
|
|
397
1155
|
bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
|
|
398
1156
|
correction_indexer_id: Código do indexador de correção
|
|
399
1157
|
correction_date: Data para correção do indexador (YYYY-MM-DD)
|
|
400
1158
|
"""
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
|
|
1159
|
+
# Usar serviço interno com polling assíncrono
|
|
1160
|
+
result = await _svc_get_accounts_receivable_by_bills(
|
|
1161
|
+
bills_ids=bills_ids,
|
|
1162
|
+
correction_indexer_id=correction_indexer_id,
|
|
1163
|
+
correction_date=correction_date
|
|
1164
|
+
)
|
|
409
1165
|
|
|
410
1166
|
if result["success"]:
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return {
|
|
1167
|
+
income_data = result.get("data", [])
|
|
1168
|
+
|
|
1169
|
+
response = {
|
|
415
1170
|
"success": True,
|
|
416
1171
|
"message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
|
|
417
1172
|
"income_data": income_data,
|
|
418
1173
|
"count": len(income_data),
|
|
419
1174
|
"bills_consulted": bills_ids,
|
|
420
|
-
"
|
|
1175
|
+
"request_id": result.get("request_id"),
|
|
1176
|
+
"latency_ms": result.get("latency_ms")
|
|
421
1177
|
}
|
|
1178
|
+
|
|
1179
|
+
# Se foi processamento assíncrono, incluir informações extras
|
|
1180
|
+
if result.get("async_identifier"):
|
|
1181
|
+
response.update({
|
|
1182
|
+
"async_processing": {
|
|
1183
|
+
"identifier": result.get("async_identifier"),
|
|
1184
|
+
"correlation_id": result.get("correlation_id"),
|
|
1185
|
+
"chunks_downloaded": result.get("chunks_downloaded"),
|
|
1186
|
+
"rows_returned": result.get("rows_returned"),
|
|
1187
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1188
|
+
}
|
|
1189
|
+
})
|
|
1190
|
+
|
|
1191
|
+
return response
|
|
422
1192
|
|
|
423
1193
|
return {
|
|
424
1194
|
"success": False,
|
|
425
1195
|
"message": "❌ Erro ao buscar parcelas dos títulos informados",
|
|
426
1196
|
"error": result.get("error"),
|
|
427
1197
|
"details": result.get("message"),
|
|
1198
|
+
"request_id": result.get("request_id"),
|
|
1199
|
+
"async_info": {
|
|
1200
|
+
"identifier": result.get("async_identifier"),
|
|
1201
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1202
|
+
} if result.get("async_identifier") else None
|
|
428
1203
|
}
|
|
429
1204
|
|
|
430
1205
|
|
|
@@ -435,7 +1210,7 @@ async def get_sienge_bills(
|
|
|
435
1210
|
creditor_id: Optional[str] = None,
|
|
436
1211
|
status: Optional[str] = None,
|
|
437
1212
|
limit: Optional[int] = 50,
|
|
438
|
-
) -> Dict:
|
|
1213
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
439
1214
|
"""
|
|
440
1215
|
Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
|
|
441
1216
|
|
|
@@ -446,26 +1221,14 @@ async def get_sienge_bills(
|
|
|
446
1221
|
status: Status do título (ex: open, paid, cancelled)
|
|
447
1222
|
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
448
1223
|
"""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
458
|
-
|
|
459
|
-
# Parâmetros obrigatórios
|
|
460
|
-
params = {"startDate": start_date, "endDate": end_date, "limit": min(limit or 50, 200)} # OBRIGATÓRIO pela API
|
|
461
|
-
|
|
462
|
-
# Parâmetros opcionais
|
|
463
|
-
if creditor_id:
|
|
464
|
-
params["creditor_id"] = creditor_id
|
|
465
|
-
if status:
|
|
466
|
-
params["status"] = status
|
|
467
|
-
|
|
468
|
-
result = await make_sienge_request("GET", "/bills", params=params)
|
|
1224
|
+
# Usar serviço interno
|
|
1225
|
+
result = await _svc_get_bills(
|
|
1226
|
+
start_date=start_date,
|
|
1227
|
+
end_date=end_date,
|
|
1228
|
+
creditor_id=creditor_id,
|
|
1229
|
+
status=status,
|
|
1230
|
+
limit=limit or 50
|
|
1231
|
+
)
|
|
469
1232
|
|
|
470
1233
|
if result["success"]:
|
|
471
1234
|
data = result["data"]
|
|
@@ -473,14 +1236,33 @@ async def get_sienge_bills(
|
|
|
473
1236
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
474
1237
|
total_count = metadata.get("count", len(bills))
|
|
475
1238
|
|
|
1239
|
+
# Aplicar parsing numérico nos valores
|
|
1240
|
+
for bill in bills:
|
|
1241
|
+
if "amount" in bill:
|
|
1242
|
+
bill["amount"] = _parse_numeric_value(bill["amount"])
|
|
1243
|
+
if "paid_amount" in bill:
|
|
1244
|
+
bill["paid_amount"] = _parse_numeric_value(bill["paid_amount"])
|
|
1245
|
+
if "remaining_amount" in bill:
|
|
1246
|
+
bill["remaining_amount"] = _parse_numeric_value(bill["remaining_amount"])
|
|
1247
|
+
|
|
1248
|
+
# Usar datas padrão se não fornecidas
|
|
1249
|
+
actual_start = start_date or (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
1250
|
+
actual_end = end_date or datetime.now().strftime("%Y-%m-%d")
|
|
1251
|
+
|
|
476
1252
|
return {
|
|
477
1253
|
"success": True,
|
|
478
|
-
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {
|
|
1254
|
+
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {actual_start} a {actual_end}",
|
|
479
1255
|
"bills": bills,
|
|
480
1256
|
"count": len(bills),
|
|
481
1257
|
"total_count": total_count,
|
|
482
|
-
"period": {"start_date":
|
|
483
|
-
"
|
|
1258
|
+
"period": {"start_date": actual_start, "end_date": actual_end},
|
|
1259
|
+
"filters_applied": {
|
|
1260
|
+
"start_date": actual_start, "end_date": actual_end,
|
|
1261
|
+
"creditor_id": creditor_id, "status": status, "limit": limit
|
|
1262
|
+
},
|
|
1263
|
+
"request_id": result.get("request_id"),
|
|
1264
|
+
"latency_ms": result.get("latency_ms"),
|
|
1265
|
+
"cache": result.get("cache")
|
|
484
1266
|
}
|
|
485
1267
|
|
|
486
1268
|
return {
|
|
@@ -488,6 +1270,7 @@ async def get_sienge_bills(
|
|
|
488
1270
|
"message": "❌ Erro ao buscar títulos a pagar",
|
|
489
1271
|
"error": result.get("error"),
|
|
490
1272
|
"details": result.get("message"),
|
|
1273
|
+
"request_id": result.get("request_id")
|
|
491
1274
|
}
|
|
492
1275
|
|
|
493
1276
|
|
|
@@ -500,7 +1283,7 @@ async def get_sienge_purchase_orders(
|
|
|
500
1283
|
status: Optional[str] = None,
|
|
501
1284
|
date_from: Optional[str] = None,
|
|
502
1285
|
limit: Optional[int] = 50,
|
|
503
|
-
) -> Dict:
|
|
1286
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
504
1287
|
"""
|
|
505
1288
|
Consulta pedidos de compra
|
|
506
1289
|
|
|
@@ -533,6 +1316,8 @@ async def get_sienge_purchase_orders(
|
|
|
533
1316
|
"message": f"✅ Encontrados {len(orders)} pedidos de compra",
|
|
534
1317
|
"purchase_orders": orders,
|
|
535
1318
|
"count": len(orders),
|
|
1319
|
+
"request_id": result.get("request_id"),
|
|
1320
|
+
"latency_ms": result.get("latency_ms")
|
|
536
1321
|
}
|
|
537
1322
|
|
|
538
1323
|
return {
|
|
@@ -540,11 +1325,14 @@ async def get_sienge_purchase_orders(
|
|
|
540
1325
|
"message": "❌ Erro ao buscar pedidos de compra",
|
|
541
1326
|
"error": result.get("error"),
|
|
542
1327
|
"details": result.get("message"),
|
|
1328
|
+
"request_id": result.get("request_id"),
|
|
1329
|
+
"latency_ms": result.get("latency_ms")
|
|
543
1330
|
}
|
|
544
1331
|
|
|
545
1332
|
|
|
546
1333
|
@mcp.tool
|
|
547
|
-
async def get_sienge_purchase_order_items(purchase_order_id: str
|
|
1334
|
+
async def get_sienge_purchase_order_items(purchase_order_id: str,
|
|
1335
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
548
1336
|
"""
|
|
549
1337
|
Consulta itens de um pedido de compra específico
|
|
550
1338
|
|
|
@@ -574,7 +1362,8 @@ async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
|
|
|
574
1362
|
|
|
575
1363
|
|
|
576
1364
|
@mcp.tool
|
|
577
|
-
async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50
|
|
1365
|
+
async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50,
|
|
1366
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
578
1367
|
"""
|
|
579
1368
|
Consulta solicitações de compra
|
|
580
1369
|
|
|
@@ -615,7 +1404,8 @@ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None
|
|
|
615
1404
|
|
|
616
1405
|
|
|
617
1406
|
@mcp.tool
|
|
618
|
-
async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]
|
|
1407
|
+
async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]],
|
|
1408
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
619
1409
|
"""
|
|
620
1410
|
Cria nova solicitação de compra
|
|
621
1411
|
|
|
@@ -631,14 +1421,18 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
631
1421
|
"date": datetime.now().strftime("%Y-%m-%d"),
|
|
632
1422
|
}
|
|
633
1423
|
|
|
634
|
-
|
|
1424
|
+
# CORRIGIDO: Normalizar JSON payload
|
|
1425
|
+
json_data = to_camel_json(request_data)
|
|
1426
|
+
result = await make_sienge_request("POST", "/purchase-requests", json_data=json_data)
|
|
635
1427
|
|
|
636
1428
|
if result["success"]:
|
|
637
1429
|
return {
|
|
638
1430
|
"success": True,
|
|
639
1431
|
"message": "✅ Solicitação de compra criada com sucesso",
|
|
640
|
-
"request_id": result
|
|
1432
|
+
"request_id": result.get("request_id"),
|
|
1433
|
+
"purchase_request_id": result["data"].get("id"),
|
|
641
1434
|
"data": result["data"],
|
|
1435
|
+
"latency_ms": result.get("latency_ms")
|
|
642
1436
|
}
|
|
643
1437
|
|
|
644
1438
|
return {
|
|
@@ -646,6 +1440,7 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
646
1440
|
"message": "❌ Erro ao criar solicitação de compra",
|
|
647
1441
|
"error": result.get("error"),
|
|
648
1442
|
"details": result.get("message"),
|
|
1443
|
+
"request_id": result.get("request_id")
|
|
649
1444
|
}
|
|
650
1445
|
|
|
651
1446
|
|
|
@@ -653,7 +1448,8 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
653
1448
|
|
|
654
1449
|
|
|
655
1450
|
@mcp.tool
|
|
656
|
-
async def get_sienge_purchase_invoice(sequential_number: int
|
|
1451
|
+
async def get_sienge_purchase_invoice(sequential_number: int,
|
|
1452
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
657
1453
|
"""
|
|
658
1454
|
Consulta nota fiscal de compra por número sequencial
|
|
659
1455
|
|
|
@@ -674,7 +1470,8 @@ async def get_sienge_purchase_invoice(sequential_number: int) -> Dict:
|
|
|
674
1470
|
|
|
675
1471
|
|
|
676
1472
|
@mcp.tool
|
|
677
|
-
async def get_sienge_purchase_invoice_items(sequential_number: int
|
|
1473
|
+
async def get_sienge_purchase_invoice_items(sequential_number: int,
|
|
1474
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
678
1475
|
"""
|
|
679
1476
|
Consulta itens de uma nota fiscal de compra
|
|
680
1477
|
|
|
@@ -715,7 +1512,7 @@ async def create_sienge_purchase_invoice(
|
|
|
715
1512
|
issue_date: str,
|
|
716
1513
|
series: Optional[str] = None,
|
|
717
1514
|
notes: Optional[str] = None,
|
|
718
|
-
) -> Dict:
|
|
1515
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
719
1516
|
"""
|
|
720
1517
|
Cadastra uma nova nota fiscal de compra
|
|
721
1518
|
|
|
@@ -731,13 +1528,13 @@ async def create_sienge_purchase_invoice(
|
|
|
731
1528
|
notes: Observações (opcional)
|
|
732
1529
|
"""
|
|
733
1530
|
invoice_data = {
|
|
734
|
-
"
|
|
1531
|
+
"document_id": document_id,
|
|
735
1532
|
"number": number,
|
|
736
|
-
"
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
"
|
|
740
|
-
"
|
|
1533
|
+
"supplier_id": supplier_id,
|
|
1534
|
+
"company_id": company_id,
|
|
1535
|
+
"movement_type_id": movement_type_id,
|
|
1536
|
+
"movement_date": movement_date,
|
|
1537
|
+
"issue_date": issue_date,
|
|
741
1538
|
}
|
|
742
1539
|
|
|
743
1540
|
if series:
|
|
@@ -745,7 +1542,7 @@ async def create_sienge_purchase_invoice(
|
|
|
745
1542
|
if notes:
|
|
746
1543
|
invoice_data["notes"] = notes
|
|
747
1544
|
|
|
748
|
-
result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
|
|
1545
|
+
result = await make_sienge_request("POST", "/purchase-invoices", json_data=to_camel_json(invoice_data))
|
|
749
1546
|
|
|
750
1547
|
if result["success"]:
|
|
751
1548
|
return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
|
|
@@ -765,7 +1562,7 @@ async def add_items_to_purchase_invoice(
|
|
|
765
1562
|
copy_notes_purchase_orders: bool = True,
|
|
766
1563
|
copy_notes_resources: bool = False,
|
|
767
1564
|
copy_attachments_purchase_orders: bool = True,
|
|
768
|
-
) -> Dict:
|
|
1565
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
769
1566
|
"""
|
|
770
1567
|
Insere itens em uma nota fiscal a partir de entregas de pedidos de compra
|
|
771
1568
|
|
|
@@ -782,14 +1579,14 @@ async def add_items_to_purchase_invoice(
|
|
|
782
1579
|
copy_attachments_purchase_orders: Copiar anexos dos pedidos de compra
|
|
783
1580
|
"""
|
|
784
1581
|
item_data = {
|
|
785
|
-
"
|
|
786
|
-
"
|
|
787
|
-
"
|
|
788
|
-
"
|
|
1582
|
+
"deliveries_order": deliveries_order,
|
|
1583
|
+
"copy_notes_purchase_orders": copy_notes_purchase_orders,
|
|
1584
|
+
"copy_notes_resources": copy_notes_resources,
|
|
1585
|
+
"copy_attachments_purchase_orders": copy_attachments_purchase_orders,
|
|
789
1586
|
}
|
|
790
1587
|
|
|
791
1588
|
result = await make_sienge_request(
|
|
792
|
-
"POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data
|
|
1589
|
+
"POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=to_camel_json(item_data)
|
|
793
1590
|
)
|
|
794
1591
|
|
|
795
1592
|
if result["success"]:
|
|
@@ -816,7 +1613,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(
|
|
|
816
1613
|
purchase_order_item_number: Optional[int] = None,
|
|
817
1614
|
limit: Optional[int] = 100,
|
|
818
1615
|
offset: Optional[int] = 0,
|
|
819
|
-
) -> Dict:
|
|
1616
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
820
1617
|
"""
|
|
821
1618
|
Lista entregas atendidas entre pedidos de compra e notas fiscais
|
|
822
1619
|
|
|
@@ -870,7 +1667,8 @@ async def get_sienge_purchase_invoices_deliveries_attended(
|
|
|
870
1667
|
|
|
871
1668
|
|
|
872
1669
|
@mcp.tool
|
|
873
|
-
async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None
|
|
1670
|
+
async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None,
|
|
1671
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
874
1672
|
"""
|
|
875
1673
|
Consulta inventário de estoque por centro de custo
|
|
876
1674
|
|
|
@@ -907,7 +1705,8 @@ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[
|
|
|
907
1705
|
|
|
908
1706
|
|
|
909
1707
|
@mcp.tool
|
|
910
|
-
async def get_sienge_stock_reservations(limit: Optional[int] = 50
|
|
1708
|
+
async def get_sienge_stock_reservations(limit: Optional[int] = 50,
|
|
1709
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
911
1710
|
"""
|
|
912
1711
|
Lista reservas de estoque
|
|
913
1712
|
|
|
@@ -947,9 +1746,10 @@ async def get_sienge_projects(
|
|
|
947
1746
|
enterprise_type: Optional[int] = None,
|
|
948
1747
|
receivable_register: Optional[str] = None,
|
|
949
1748
|
only_buildings_enabled: Optional[bool] = False,
|
|
950
|
-
) -> Dict:
|
|
1749
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
951
1750
|
"""
|
|
952
1751
|
Busca empreendimentos/obras no Sienge
|
|
1752
|
+
CORRIGIDO: Mapeamento correto da chave de resposta
|
|
953
1753
|
|
|
954
1754
|
Args:
|
|
955
1755
|
limit: Máximo de registros (padrão: 100, máximo: 200)
|
|
@@ -959,31 +1759,39 @@ async def get_sienge_projects(
|
|
|
959
1759
|
receivable_register: Filtro de registro de recebíveis (B3, CERC)
|
|
960
1760
|
only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
|
|
961
1761
|
"""
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
params["onlyBuildingsEnabledForIntegration"] = only_buildings_enabled
|
|
972
|
-
|
|
973
|
-
result = await make_sienge_request("GET", "/enterprises", params=params)
|
|
1762
|
+
# Usar serviço interno
|
|
1763
|
+
result = await _svc_get_projects(
|
|
1764
|
+
limit=limit or 100,
|
|
1765
|
+
offset=offset or 0,
|
|
1766
|
+
company_id=company_id,
|
|
1767
|
+
enterprise_type=enterprise_type,
|
|
1768
|
+
receivable_register=receivable_register,
|
|
1769
|
+
only_buildings_enabled=only_buildings_enabled or False
|
|
1770
|
+
)
|
|
974
1771
|
|
|
975
1772
|
if result["success"]:
|
|
976
1773
|
data = result["data"]
|
|
1774
|
+
# CORREÇÃO: API retorna em "results", mas paginador espera em "enterprises"
|
|
977
1775
|
enterprises = data.get("results", []) if isinstance(data, dict) else data
|
|
978
1776
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
1777
|
+
total_count = metadata.get("count", len(enterprises))
|
|
979
1778
|
|
|
980
1779
|
return {
|
|
981
1780
|
"success": True,
|
|
982
|
-
"message": f"✅ Encontrados {len(enterprises)} empreendimentos",
|
|
983
|
-
"enterprises": enterprises,
|
|
1781
|
+
"message": f"✅ Encontrados {len(enterprises)} empreendimentos (total: {total_count})",
|
|
1782
|
+
"enterprises": enterprises, # Manter consistência para paginador
|
|
1783
|
+
"projects": enterprises, # Alias para compatibilidade
|
|
984
1784
|
"count": len(enterprises),
|
|
1785
|
+
"total_count": total_count,
|
|
985
1786
|
"metadata": metadata,
|
|
986
|
-
"
|
|
1787
|
+
"filters_applied": {
|
|
1788
|
+
"limit": limit, "offset": offset, "company_id": company_id,
|
|
1789
|
+
"enterprise_type": enterprise_type, "receivable_register": receivable_register,
|
|
1790
|
+
"only_buildings_enabled": only_buildings_enabled
|
|
1791
|
+
},
|
|
1792
|
+
"request_id": result.get("request_id"),
|
|
1793
|
+
"latency_ms": result.get("latency_ms"),
|
|
1794
|
+
"cache": result.get("cache")
|
|
987
1795
|
}
|
|
988
1796
|
|
|
989
1797
|
return {
|
|
@@ -991,11 +1799,13 @@ async def get_sienge_projects(
|
|
|
991
1799
|
"message": "❌ Erro ao buscar empreendimentos",
|
|
992
1800
|
"error": result.get("error"),
|
|
993
1801
|
"details": result.get("message"),
|
|
1802
|
+
"request_id": result.get("request_id")
|
|
994
1803
|
}
|
|
995
1804
|
|
|
996
1805
|
|
|
997
1806
|
@mcp.tool
|
|
998
|
-
async def get_sienge_enterprise_by_id(enterprise_id: int
|
|
1807
|
+
async def get_sienge_enterprise_by_id(enterprise_id: int,
|
|
1808
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
999
1809
|
"""
|
|
1000
1810
|
Busca um empreendimento específico por ID no Sienge
|
|
1001
1811
|
|
|
@@ -1016,7 +1826,8 @@ async def get_sienge_enterprise_by_id(enterprise_id: int) -> Dict:
|
|
|
1016
1826
|
|
|
1017
1827
|
|
|
1018
1828
|
@mcp.tool
|
|
1019
|
-
async def get_sienge_enterprise_groupings(enterprise_id: int
|
|
1829
|
+
async def get_sienge_enterprise_groupings(enterprise_id: int,
|
|
1830
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
1020
1831
|
"""
|
|
1021
1832
|
Busca agrupamentos de unidades de um empreendimento específico
|
|
1022
1833
|
|
|
@@ -1043,7 +1854,8 @@ async def get_sienge_enterprise_groupings(enterprise_id: int) -> Dict:
|
|
|
1043
1854
|
|
|
1044
1855
|
|
|
1045
1856
|
@mcp.tool
|
|
1046
|
-
async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0
|
|
1857
|
+
async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0,
|
|
1858
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
1047
1859
|
"""
|
|
1048
1860
|
Consulta unidades cadastradas no Sienge
|
|
1049
1861
|
|
|
@@ -1065,6 +1877,9 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0)
|
|
|
1065
1877
|
"message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
|
|
1066
1878
|
"units": units,
|
|
1067
1879
|
"count": len(units),
|
|
1880
|
+
"total_count": total_count,
|
|
1881
|
+
"request_id": result.get("request_id"),
|
|
1882
|
+
"latency_ms": result.get("latency_ms")
|
|
1068
1883
|
}
|
|
1069
1884
|
|
|
1070
1885
|
return {
|
|
@@ -1072,6 +1887,8 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0)
|
|
|
1072
1887
|
"message": "❌ Erro ao buscar unidades",
|
|
1073
1888
|
"error": result.get("error"),
|
|
1074
1889
|
"details": result.get("message"),
|
|
1890
|
+
"request_id": result.get("request_id"),
|
|
1891
|
+
"latency_ms": result.get("latency_ms")
|
|
1075
1892
|
}
|
|
1076
1893
|
|
|
1077
1894
|
|
|
@@ -1084,7 +1901,7 @@ async def get_sienge_unit_cost_tables(
|
|
|
1084
1901
|
description: Optional[str] = None,
|
|
1085
1902
|
status: Optional[str] = "Active",
|
|
1086
1903
|
integration_enabled: Optional[bool] = None,
|
|
1087
|
-
) -> Dict:
|
|
1904
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
1088
1905
|
"""
|
|
1089
1906
|
Consulta tabelas de custos unitários
|
|
1090
1907
|
|
|
@@ -1132,8 +1949,8 @@ async def search_sienge_data(
|
|
|
1132
1949
|
query: str,
|
|
1133
1950
|
entity_type: Optional[str] = None,
|
|
1134
1951
|
limit: Optional[int] = 20,
|
|
1135
|
-
filters: Optional[Dict[str, Any]] = None
|
|
1136
|
-
) -> Dict:
|
|
1952
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1953
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
1137
1954
|
"""
|
|
1138
1955
|
Busca universal no Sienge - compatível com ChatGPT/OpenAI MCP
|
|
1139
1956
|
|
|
@@ -1210,35 +2027,44 @@ async def search_sienge_data(
|
|
|
1210
2027
|
|
|
1211
2028
|
|
|
1212
2029
|
async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
|
|
1213
|
-
"""
|
|
2030
|
+
"""
|
|
2031
|
+
Função auxiliar para buscar em uma entidade específica
|
|
2032
|
+
CORRIGIDO: Usa serviços internos, nunca outras tools
|
|
2033
|
+
"""
|
|
1214
2034
|
|
|
1215
2035
|
if entity_type == "customers":
|
|
1216
|
-
result = await
|
|
2036
|
+
result = await _svc_get_customers(limit=limit, search=query)
|
|
1217
2037
|
if result["success"]:
|
|
2038
|
+
data = result["data"]
|
|
2039
|
+
customers = data.get("results", []) if isinstance(data, dict) else data
|
|
1218
2040
|
return {
|
|
1219
2041
|
"success": True,
|
|
1220
|
-
"data":
|
|
1221
|
-
"count":
|
|
2042
|
+
"data": customers,
|
|
2043
|
+
"count": len(customers),
|
|
1222
2044
|
"entity_type": "customers"
|
|
1223
2045
|
}
|
|
1224
2046
|
|
|
1225
2047
|
elif entity_type == "creditors":
|
|
1226
|
-
result = await
|
|
2048
|
+
result = await _svc_get_creditors(limit=limit, search=query)
|
|
1227
2049
|
if result["success"]:
|
|
2050
|
+
data = result["data"]
|
|
2051
|
+
creditors = data.get("results", []) if isinstance(data, dict) else data
|
|
1228
2052
|
return {
|
|
1229
2053
|
"success": True,
|
|
1230
|
-
"data":
|
|
1231
|
-
"count":
|
|
2054
|
+
"data": creditors,
|
|
2055
|
+
"count": len(creditors),
|
|
1232
2056
|
"entity_type": "creditors"
|
|
1233
2057
|
}
|
|
1234
2058
|
|
|
1235
2059
|
elif entity_type == "projects" or entity_type == "enterprises":
|
|
1236
2060
|
# Para projetos, usar filtros mais específicos se disponível
|
|
1237
2061
|
company_id = filters.get("company_id")
|
|
1238
|
-
result = await
|
|
2062
|
+
result = await _svc_get_projects(limit=limit, company_id=company_id)
|
|
1239
2063
|
if result["success"]:
|
|
2064
|
+
data = result["data"]
|
|
2065
|
+
projects = data.get("results", []) if isinstance(data, dict) else data
|
|
2066
|
+
|
|
1240
2067
|
# Filtrar por query se fornecida
|
|
1241
|
-
projects = result["enterprises"]
|
|
1242
2068
|
if query:
|
|
1243
2069
|
projects = [
|
|
1244
2070
|
p for p in projects
|
|
@@ -1257,23 +2083,27 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
|
|
|
1257
2083
|
# Para títulos, usar data padrão se não especificada
|
|
1258
2084
|
start_date = filters.get("start_date")
|
|
1259
2085
|
end_date = filters.get("end_date")
|
|
1260
|
-
result = await
|
|
2086
|
+
result = await _svc_get_bills(
|
|
1261
2087
|
start_date=start_date,
|
|
1262
2088
|
end_date=end_date,
|
|
1263
2089
|
limit=limit
|
|
1264
2090
|
)
|
|
1265
2091
|
if result["success"]:
|
|
2092
|
+
data = result["data"]
|
|
2093
|
+
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
1266
2094
|
return {
|
|
1267
2095
|
"success": True,
|
|
1268
|
-
"data":
|
|
1269
|
-
"count":
|
|
2096
|
+
"data": bills,
|
|
2097
|
+
"count": len(bills),
|
|
1270
2098
|
"entity_type": "bills"
|
|
1271
2099
|
}
|
|
1272
2100
|
|
|
1273
2101
|
elif entity_type == "purchase_orders":
|
|
1274
|
-
result = await
|
|
2102
|
+
result = await _svc_get_purchase_orders(limit=limit)
|
|
1275
2103
|
if result["success"]:
|
|
1276
|
-
|
|
2104
|
+
data = result["data"]
|
|
2105
|
+
orders = data.get("results", []) if isinstance(data, dict) else data
|
|
2106
|
+
|
|
1277
2107
|
# Filtrar por query se fornecida
|
|
1278
2108
|
if query:
|
|
1279
2109
|
orders = [
|
|
@@ -1297,7 +2127,7 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
|
|
|
1297
2127
|
|
|
1298
2128
|
|
|
1299
2129
|
@mcp.tool
|
|
1300
|
-
async def list_sienge_entities() -> Dict:
|
|
2130
|
+
async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
|
|
1301
2131
|
"""
|
|
1302
2132
|
Lista todas as entidades disponíveis no Sienge MCP para busca
|
|
1303
2133
|
|
|
@@ -1316,28 +2146,28 @@ async def list_sienge_entities() -> Dict:
|
|
|
1316
2146
|
"name": "Credores/Fornecedores",
|
|
1317
2147
|
"description": "Fornecedores e credores cadastrados",
|
|
1318
2148
|
"search_fields": ["nome", "documento"],
|
|
1319
|
-
"tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info"]
|
|
2149
|
+
"tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info", "get_sienge_suppliers"]
|
|
1320
2150
|
},
|
|
1321
2151
|
{
|
|
1322
2152
|
"type": "projects",
|
|
1323
2153
|
"name": "Empreendimentos/Obras",
|
|
1324
2154
|
"description": "Projetos e obras cadastrados",
|
|
1325
2155
|
"search_fields": ["código", "descrição", "nome"],
|
|
1326
|
-
"tools": ["get_sienge_projects", "get_sienge_enterprise_by_id"]
|
|
2156
|
+
"tools": ["get_sienge_projects", "get_sienge_enterprises", "get_sienge_enterprise_by_id"]
|
|
1327
2157
|
},
|
|
1328
2158
|
{
|
|
1329
2159
|
"type": "bills",
|
|
1330
2160
|
"name": "Títulos a Pagar",
|
|
1331
2161
|
"description": "Contas a pagar e títulos financeiros",
|
|
1332
2162
|
"search_fields": ["número", "credor", "valor"],
|
|
1333
|
-
"tools": ["get_sienge_bills"]
|
|
2163
|
+
"tools": ["get_sienge_bills", "get_sienge_accounts_payable"]
|
|
1334
2164
|
},
|
|
1335
2165
|
{
|
|
1336
2166
|
"type": "purchase_orders",
|
|
1337
2167
|
"name": "Pedidos de Compra",
|
|
1338
2168
|
"description": "Pedidos de compra e solicitações",
|
|
1339
2169
|
"search_fields": ["id", "descrição", "status"],
|
|
1340
|
-
"tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests"]
|
|
2170
|
+
"tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests", "list_sienge_purchase_requests"]
|
|
1341
2171
|
},
|
|
1342
2172
|
{
|
|
1343
2173
|
"type": "invoices",
|
|
@@ -1358,7 +2188,14 @@ async def list_sienge_entities() -> Dict:
|
|
|
1358
2188
|
"name": "Financeiro",
|
|
1359
2189
|
"description": "Contas a receber e movimentações financeiras",
|
|
1360
2190
|
"search_fields": ["período", "cliente", "valor"],
|
|
1361
|
-
"tools": ["get_sienge_accounts_receivable"]
|
|
2191
|
+
"tools": ["get_sienge_accounts_receivable", "search_sienge_financial_data", "search_sienge_finances"]
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
"type": "suppliers",
|
|
2195
|
+
"name": "Fornecedores",
|
|
2196
|
+
"description": "Fornecedores e credores cadastrados",
|
|
2197
|
+
"search_fields": ["código", "nome", "razão social"],
|
|
2198
|
+
"tools": ["get_sienge_suppliers"]
|
|
1362
2199
|
}
|
|
1363
2200
|
]
|
|
1364
2201
|
|
|
@@ -1384,8 +2221,8 @@ async def get_sienge_data_paginated(
|
|
|
1384
2221
|
page: int = 1,
|
|
1385
2222
|
page_size: int = 20,
|
|
1386
2223
|
filters: Optional[Dict[str, Any]] = None,
|
|
1387
|
-
sort_by: Optional[str] = None
|
|
1388
|
-
) -> Dict:
|
|
2224
|
+
sort_by: Optional[str] = None,
|
|
2225
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
1389
2226
|
"""
|
|
1390
2227
|
Busca dados do Sienge com paginação avançada - compatível com ChatGPT
|
|
1391
2228
|
|
|
@@ -1401,41 +2238,70 @@ async def get_sienge_data_paginated(
|
|
|
1401
2238
|
|
|
1402
2239
|
filters = filters or {}
|
|
1403
2240
|
|
|
1404
|
-
# Mapear para
|
|
2241
|
+
# CORRIGIDO: Mapear para serviços internos, não tools
|
|
1405
2242
|
if entity_type == "customers":
|
|
1406
2243
|
search = filters.get("search")
|
|
1407
2244
|
customer_type_id = filters.get("customer_type_id")
|
|
1408
|
-
result = await
|
|
2245
|
+
result = await _svc_get_customers(
|
|
1409
2246
|
limit=page_size,
|
|
1410
2247
|
offset=offset,
|
|
1411
2248
|
search=search,
|
|
1412
2249
|
customer_type_id=customer_type_id
|
|
1413
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["customers"] = items
|
|
2256
|
+
result["count"] = len(items)
|
|
2257
|
+
result["total_count"] = total
|
|
1414
2258
|
|
|
1415
2259
|
elif entity_type == "creditors":
|
|
1416
2260
|
search = filters.get("search")
|
|
1417
|
-
result = await
|
|
2261
|
+
result = await _svc_get_creditors(
|
|
1418
2262
|
limit=page_size,
|
|
1419
2263
|
offset=offset,
|
|
1420
2264
|
search=search
|
|
1421
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["creditors"] = items
|
|
2271
|
+
result["count"] = len(items)
|
|
2272
|
+
result["total_count"] = total
|
|
1422
2273
|
|
|
1423
2274
|
elif entity_type == "projects":
|
|
1424
|
-
result = await
|
|
2275
|
+
result = await _svc_get_projects(
|
|
1425
2276
|
limit=page_size,
|
|
1426
2277
|
offset=offset,
|
|
1427
2278
|
company_id=filters.get("company_id"),
|
|
1428
2279
|
enterprise_type=filters.get("enterprise_type")
|
|
1429
2280
|
)
|
|
2281
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2282
|
+
if result["success"]:
|
|
2283
|
+
data = result["data"]
|
|
2284
|
+
items, total = _extract_items_and_total(data)
|
|
2285
|
+
result["projects"] = items
|
|
2286
|
+
result["enterprises"] = items # Para compatibilidade
|
|
2287
|
+
result["count"] = len(items)
|
|
2288
|
+
result["total_count"] = total
|
|
1430
2289
|
|
|
1431
2290
|
elif entity_type == "bills":
|
|
1432
|
-
result = await
|
|
2291
|
+
result = await _svc_get_bills(
|
|
1433
2292
|
start_date=filters.get("start_date"),
|
|
1434
2293
|
end_date=filters.get("end_date"),
|
|
1435
2294
|
creditor_id=filters.get("creditor_id"),
|
|
1436
2295
|
status=filters.get("status"),
|
|
1437
2296
|
limit=page_size
|
|
1438
2297
|
)
|
|
2298
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2299
|
+
if result["success"]:
|
|
2300
|
+
data = result["data"]
|
|
2301
|
+
items, total = _extract_items_and_total(data)
|
|
2302
|
+
result["bills"] = items
|
|
2303
|
+
result["count"] = len(items)
|
|
2304
|
+
result["total_count"] = total
|
|
1439
2305
|
|
|
1440
2306
|
else:
|
|
1441
2307
|
return {
|
|
@@ -1477,8 +2343,8 @@ async def search_sienge_financial_data(
|
|
|
1477
2343
|
search_type: str = "both",
|
|
1478
2344
|
amount_min: Optional[float] = None,
|
|
1479
2345
|
amount_max: Optional[float] = None,
|
|
1480
|
-
customer_creditor_search: Optional[str] = None
|
|
1481
|
-
) -> Dict:
|
|
2346
|
+
customer_creditor_search: Optional[str] = None,
|
|
2347
|
+
_meta: Optional[Dict] = None) -> Dict:
|
|
1482
2348
|
"""
|
|
1483
2349
|
Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
|
|
1484
2350
|
|
|
@@ -1499,20 +2365,21 @@ async def search_sienge_financial_data(
|
|
|
1499
2365
|
# Buscar contas a receber
|
|
1500
2366
|
if search_type in ["receivable", "both"]:
|
|
1501
2367
|
try:
|
|
1502
|
-
|
|
2368
|
+
# CORRIGIDO: Usar serviço interno
|
|
2369
|
+
receivable_result = await _svc_get_accounts_receivable(
|
|
1503
2370
|
start_date=period_start,
|
|
1504
2371
|
end_date=period_end,
|
|
1505
2372
|
selection_type="D" # Por vencimento
|
|
1506
2373
|
)
|
|
1507
2374
|
|
|
1508
2375
|
if receivable_result["success"]:
|
|
1509
|
-
receivable_data = receivable_result
|
|
2376
|
+
receivable_data = receivable_result.get("data", [])
|
|
1510
2377
|
|
|
1511
|
-
# Aplicar filtros de valor
|
|
2378
|
+
# CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
|
|
1512
2379
|
if amount_min is not None or amount_max is not None:
|
|
1513
2380
|
filtered_data = []
|
|
1514
2381
|
for item in receivable_data:
|
|
1515
|
-
amount =
|
|
2382
|
+
amount = _parse_numeric_value(item.get("amount", 0))
|
|
1516
2383
|
if amount_min is not None and amount < amount_min:
|
|
1517
2384
|
continue
|
|
1518
2385
|
if amount_max is not None and amount > amount_max:
|
|
@@ -1545,20 +2412,22 @@ async def search_sienge_financial_data(
|
|
|
1545
2412
|
# Buscar contas a pagar
|
|
1546
2413
|
if search_type in ["payable", "both"]:
|
|
1547
2414
|
try:
|
|
1548
|
-
|
|
2415
|
+
# CORRIGIDO: Usar serviço interno
|
|
2416
|
+
payable_result = await _svc_get_bills(
|
|
1549
2417
|
start_date=period_start,
|
|
1550
2418
|
end_date=period_end,
|
|
1551
2419
|
limit=100
|
|
1552
2420
|
)
|
|
1553
2421
|
|
|
1554
2422
|
if payable_result["success"]:
|
|
1555
|
-
|
|
2423
|
+
data = payable_result["data"]
|
|
2424
|
+
payable_data = data.get("results", []) if isinstance(data, dict) else data
|
|
1556
2425
|
|
|
1557
|
-
# Aplicar filtros de valor
|
|
2426
|
+
# CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
|
|
1558
2427
|
if amount_min is not None or amount_max is not None:
|
|
1559
2428
|
filtered_data = []
|
|
1560
2429
|
for item in payable_data:
|
|
1561
|
-
amount =
|
|
2430
|
+
amount = _parse_numeric_value(item.get("amount", 0))
|
|
1562
2431
|
if amount_min is not None and amount < amount_min:
|
|
1563
2432
|
continue
|
|
1564
2433
|
if amount_max is not None and amount > amount_max:
|
|
@@ -1626,7 +2495,7 @@ async def search_sienge_financial_data(
|
|
|
1626
2495
|
|
|
1627
2496
|
|
|
1628
2497
|
@mcp.tool
|
|
1629
|
-
async def get_sienge_dashboard_summary() -> Dict:
|
|
2498
|
+
async def get_sienge_dashboard_summary(_meta: Optional[Dict] = None) -> Dict:
|
|
1630
2499
|
"""
|
|
1631
2500
|
Obtém um resumo tipo dashboard com informações gerais do Sienge
|
|
1632
2501
|
Útil para visão geral rápida do sistema
|
|
@@ -1650,23 +2519,26 @@ async def get_sienge_dashboard_summary() -> Dict:
|
|
|
1650
2519
|
|
|
1651
2520
|
# 2. Contar clientes (amostra)
|
|
1652
2521
|
try:
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
else:
|
|
1657
|
-
dashboard_data["customers_available"] = False
|
|
2522
|
+
# CORRIGIDO: Usar serviço interno
|
|
2523
|
+
customers_result = await _svc_get_customers(limit=1)
|
|
2524
|
+
dashboard_data["customers"] = {"available": customers_result["success"]}
|
|
1658
2525
|
except Exception as e:
|
|
1659
2526
|
errors.append(f"Clientes: {str(e)}")
|
|
1660
|
-
dashboard_data["
|
|
2527
|
+
dashboard_data["customers"] = {"available": False}
|
|
1661
2528
|
|
|
1662
2529
|
# 3. Contar projetos (amostra)
|
|
1663
2530
|
try:
|
|
1664
|
-
|
|
2531
|
+
# CORRIGIDO: Usar serviço interno
|
|
2532
|
+
projects_result = await _svc_get_projects(limit=5)
|
|
1665
2533
|
if projects_result["success"]:
|
|
2534
|
+
data = projects_result["data"]
|
|
2535
|
+
enterprises = data.get("results", []) if isinstance(data, dict) else data
|
|
2536
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
2537
|
+
|
|
1666
2538
|
dashboard_data["projects"] = {
|
|
1667
2539
|
"available": True,
|
|
1668
|
-
"sample_count": len(
|
|
1669
|
-
"total_count":
|
|
2540
|
+
"sample_count": len(enterprises),
|
|
2541
|
+
"total_count": metadata.get("count", "N/A")
|
|
1670
2542
|
}
|
|
1671
2543
|
else:
|
|
1672
2544
|
dashboard_data["projects"] = {"available": False}
|
|
@@ -1676,16 +2548,21 @@ async def get_sienge_dashboard_summary() -> Dict:
|
|
|
1676
2548
|
|
|
1677
2549
|
# 4. Títulos a pagar do mês atual
|
|
1678
2550
|
try:
|
|
1679
|
-
|
|
2551
|
+
# CORRIGIDO: Usar serviço interno
|
|
2552
|
+
bills_result = await _svc_get_bills(
|
|
1680
2553
|
start_date=current_month_start,
|
|
1681
2554
|
end_date=current_month_end,
|
|
1682
2555
|
limit=10
|
|
1683
2556
|
)
|
|
1684
2557
|
if bills_result["success"]:
|
|
2558
|
+
data = bills_result["data"]
|
|
2559
|
+
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
2560
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
2561
|
+
|
|
1685
2562
|
dashboard_data["monthly_bills"] = {
|
|
1686
2563
|
"available": True,
|
|
1687
|
-
"count": len(
|
|
1688
|
-
"total_count":
|
|
2564
|
+
"count": len(bills),
|
|
2565
|
+
"total_count": metadata.get("count", len(bills))
|
|
1689
2566
|
}
|
|
1690
2567
|
else:
|
|
1691
2568
|
dashboard_data["monthly_bills"] = {"available": False}
|
|
@@ -1695,11 +2572,15 @@ async def get_sienge_dashboard_summary() -> Dict:
|
|
|
1695
2572
|
|
|
1696
2573
|
# 5. Tipos de clientes
|
|
1697
2574
|
try:
|
|
1698
|
-
|
|
2575
|
+
# CORRIGIDO: Usar serviço interno
|
|
2576
|
+
customer_types_result = await _svc_get_customer_types()
|
|
1699
2577
|
if customer_types_result["success"]:
|
|
2578
|
+
data = customer_types_result["data"]
|
|
2579
|
+
customer_types = data.get("results", []) if isinstance(data, dict) else data
|
|
2580
|
+
|
|
1700
2581
|
dashboard_data["customer_types"] = {
|
|
1701
2582
|
"available": True,
|
|
1702
|
-
"count": len(
|
|
2583
|
+
"count": len(customer_types)
|
|
1703
2584
|
}
|
|
1704
2585
|
else:
|
|
1705
2586
|
dashboard_data["customer_types"] = {"available": False}
|
|
@@ -1737,6 +2618,21 @@ def add(a: int, b: int) -> int:
|
|
|
1737
2618
|
return a + b
|
|
1738
2619
|
|
|
1739
2620
|
|
|
2621
|
+
def _mask(s: str) -> str:
|
|
2622
|
+
"""Mascara dados sensíveis mantendo apenas o início e fim"""
|
|
2623
|
+
if not s:
|
|
2624
|
+
return None
|
|
2625
|
+
if len(s) == 1:
|
|
2626
|
+
return s + "*"
|
|
2627
|
+
if len(s) == 2:
|
|
2628
|
+
return s
|
|
2629
|
+
if len(s) <= 4:
|
|
2630
|
+
return s[:2] + "*" * (len(s) - 2)
|
|
2631
|
+
# Para strings > 4: usar no máximo 4 asteriscos no meio
|
|
2632
|
+
middle_asterisks = min(4, len(s) - 4)
|
|
2633
|
+
return s[:2] + "*" * middle_asterisks + s[-2:]
|
|
2634
|
+
|
|
2635
|
+
|
|
1740
2636
|
def _get_auth_info_internal() -> Dict:
|
|
1741
2637
|
"""Função interna para verificar configuração de autenticação"""
|
|
1742
2638
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
@@ -1747,7 +2643,7 @@ def _get_auth_info_internal() -> Dict:
|
|
|
1747
2643
|
"configured": True,
|
|
1748
2644
|
"base_url": SIENGE_BASE_URL,
|
|
1749
2645
|
"subdomain": SIENGE_SUBDOMAIN,
|
|
1750
|
-
"username": SIENGE_USERNAME,
|
|
2646
|
+
"username": _mask(SIENGE_USERNAME) if SIENGE_USERNAME else None,
|
|
1751
2647
|
}
|
|
1752
2648
|
else:
|
|
1753
2649
|
return {
|