sienge-ecbiesek-mcp 1.2.2__py3-none-any.whl → 1.3.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.2.2.dist-info → sienge_ecbiesek_mcp-1.3.0.dist-info}/METADATA +34 -3
- sienge_ecbiesek_mcp-1.3.0.dist-info/RECORD +11 -0
- sienge_mcp/__init__.py +1 -1
- sienge_mcp/server.py +1048 -1215
- sienge_mcp/utils/logger.py +1 -1
- sienge_ecbiesek_mcp-1.2.2.dist-info/RECORD +0 -11
- {sienge_ecbiesek_mcp-1.2.2.dist-info → sienge_ecbiesek_mcp-1.3.0.dist-info}/WHEEL +0 -0
- {sienge_ecbiesek_mcp-1.2.2.dist-info → sienge_ecbiesek_mcp-1.3.0.dist-info}/entry_points.txt +0 -0
- {sienge_ecbiesek_mcp-1.2.2.dist-info → sienge_ecbiesek_mcp-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {sienge_ecbiesek_mcp-1.2.2.dist-info → sienge_ecbiesek_mcp-1.3.0.dist-info}/top_level.txt +0 -0
sienge_mcp/server.py
CHANGED
|
@@ -2,27 +2,37 @@
|
|
|
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
|
|
12
5
|
"""
|
|
13
6
|
|
|
14
7
|
from fastmcp import FastMCP
|
|
15
8
|
import httpx
|
|
16
|
-
from typing import Dict, List, Optional, Any
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
17
10
|
import os
|
|
18
11
|
from dotenv import load_dotenv
|
|
19
|
-
from datetime import datetime
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
import time
|
|
20
14
|
import uuid
|
|
21
15
|
import asyncio
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import
|
|
25
|
-
|
|
16
|
+
|
|
17
|
+
# logger
|
|
18
|
+
from .utils.logger import get_logger
|
|
19
|
+
logger = get_logger()
|
|
20
|
+
|
|
21
|
+
# Optional: prefer tenacity for robust retries; linter will warn if not installed but code falls back
|
|
22
|
+
try:
|
|
23
|
+
from tenacity import AsyncRetrying, wait_exponential, stop_after_attempt, retry_if_exception_type # type: ignore
|
|
24
|
+
TENACITY_AVAILABLE = True
|
|
25
|
+
except Exception:
|
|
26
|
+
TENACITY_AVAILABLE = False
|
|
27
|
+
|
|
28
|
+
# Supabase client (optional)
|
|
29
|
+
try:
|
|
30
|
+
from supabase import create_client, Client
|
|
31
|
+
SUPABASE_AVAILABLE = True
|
|
32
|
+
except Exception:
|
|
33
|
+
SUPABASE_AVAILABLE = False
|
|
34
|
+
create_client = None
|
|
35
|
+
Client = None
|
|
26
36
|
|
|
27
37
|
# Carrega as variáveis de ambiente
|
|
28
38
|
load_dotenv()
|
|
@@ -37,273 +47,104 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
|
|
|
37
47
|
SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
|
|
38
48
|
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
|
|
39
49
|
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
# Configurações do Supabase
|
|
51
|
+
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
|
52
|
+
SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
|
|
53
|
+
SUPABASE_SCHEMA = "sienge_data" # Schema fixo: sienge_data
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
class SiengeAPIError(Exception):
|
|
53
57
|
"""Exceção customizada para erros da API do Sienge"""
|
|
54
|
-
pass
|
|
55
|
-
|
|
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
58
|
|
|
143
|
-
|
|
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
|
|
59
|
+
pass
|
|
188
60
|
|
|
189
61
|
|
|
190
62
|
async def make_sienge_request(
|
|
191
|
-
method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
|
|
63
|
+
method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
|
|
192
64
|
) -> Dict:
|
|
193
65
|
"""
|
|
194
66
|
Função auxiliar para fazer requisições à API do Sienge (v1)
|
|
195
67
|
Suporta tanto Bearer Token quanto Basic Auth
|
|
196
|
-
MELHORADO: Observabilidade, cache, normalização de parâmetros
|
|
197
68
|
"""
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
214
|
-
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
215
|
-
headers = {
|
|
216
|
-
"Content-Type": "application/json",
|
|
217
|
-
"Accept": "application/json",
|
|
218
|
-
"X-Request-ID": req_id
|
|
219
|
-
}
|
|
69
|
+
# Attach a request id and measure latency
|
|
70
|
+
request_id = str(uuid.uuid4())
|
|
71
|
+
start_ts = time.time()
|
|
220
72
|
|
|
221
|
-
|
|
222
|
-
auth = None
|
|
223
|
-
base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
|
|
73
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json", "X-Request-Id": request_id}
|
|
224
74
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
url = f"{base_normalized}/v1{endpoint}"
|
|
228
|
-
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
229
|
-
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
230
|
-
url = f"{base_normalized}/v1{endpoint}"
|
|
231
|
-
else:
|
|
232
|
-
return {
|
|
233
|
-
"success": False,
|
|
234
|
-
"error": "No Authentication",
|
|
235
|
-
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
236
|
-
"request_id": req_id
|
|
237
|
-
}
|
|
75
|
+
# Configurar autenticação e URL
|
|
76
|
+
auth = None
|
|
238
77
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
78
|
+
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
79
|
+
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
80
|
+
url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
|
|
81
|
+
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
82
|
+
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
83
|
+
url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/v1{endpoint}"
|
|
84
|
+
else:
|
|
85
|
+
return {
|
|
86
|
+
"success": False,
|
|
87
|
+
"error": "No Authentication",
|
|
88
|
+
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
89
|
+
"request_id": request_id,
|
|
90
|
+
}
|
|
250
91
|
|
|
251
|
-
|
|
92
|
+
async def _do_request(client: httpx.AsyncClient):
|
|
93
|
+
return await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
max_attempts = 5
|
|
97
|
+
attempts = 0
|
|
98
|
+
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
99
|
+
while True:
|
|
100
|
+
attempts += 1
|
|
252
101
|
try:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
102
|
+
response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
|
|
103
|
+
except (httpx.RequestError, httpx.TimeoutException) as exc:
|
|
104
|
+
logger.warning(f"Request error to {url}: {exc} (attempt {attempts}/{max_attempts})")
|
|
105
|
+
if attempts >= max_attempts:
|
|
106
|
+
raise
|
|
107
|
+
await asyncio.sleep(min(2 ** attempts, 60))
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Handle rate limit explicitly
|
|
111
|
+
if response.status_code == 429:
|
|
112
|
+
retry_after = response.headers.get("Retry-After")
|
|
113
|
+
try:
|
|
114
|
+
wait_seconds = int(retry_after) if retry_after is not None else min(2 ** attempts, 60)
|
|
115
|
+
except Exception:
|
|
116
|
+
wait_seconds = min(2 ** attempts, 60)
|
|
117
|
+
logger.warning(f"HTTP 429 from {url}, retrying after {wait_seconds}s (attempt {attempts}/{max_attempts})")
|
|
118
|
+
if attempts >= max_attempts:
|
|
119
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
120
|
+
return {"success": False, "error": "HTTP 429", "message": response.text, "status_code": 429, "latency_ms": latency_ms, "request_id": request_id}
|
|
121
|
+
await asyncio.sleep(wait_seconds)
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
125
|
+
|
|
126
|
+
if response.status_code in [200, 201]:
|
|
127
|
+
try:
|
|
128
|
+
return {"success": True, "data": response.json(), "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
|
|
129
|
+
except BaseException:
|
|
130
|
+
return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
|
|
131
|
+
else:
|
|
132
|
+
logger.warning(f"HTTP {response.status_code} from {url}: {response.text}")
|
|
270
133
|
return {
|
|
271
|
-
"success":
|
|
272
|
-
"
|
|
134
|
+
"success": False,
|
|
135
|
+
"error": f"HTTP {response.status_code}",
|
|
136
|
+
"message": response.text,
|
|
273
137
|
"status_code": response.status_code,
|
|
274
|
-
"
|
|
275
|
-
"
|
|
138
|
+
"latency_ms": latency_ms,
|
|
139
|
+
"request_id": request_id,
|
|
276
140
|
}
|
|
277
|
-
else:
|
|
278
|
-
return {
|
|
279
|
-
"success": False,
|
|
280
|
-
"error": f"HTTP {response.status_code}",
|
|
281
|
-
"message": response.text,
|
|
282
|
-
"status_code": response.status_code,
|
|
283
|
-
"request_id": req_id,
|
|
284
|
-
"latency_ms": round(latency * 1000, 2)
|
|
285
|
-
}
|
|
286
141
|
|
|
287
142
|
except httpx.TimeoutException:
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
}
|
|
143
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
144
|
+
return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s", "latency_ms": latency_ms, "request_id": request_id}
|
|
297
145
|
except Exception as e:
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
}
|
|
146
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
147
|
+
return {"success": False, "error": str(e), "message": f"Erro na requisição: {str(e)}", "latency_ms": latency_ms, "request_id": request_id}
|
|
307
148
|
|
|
308
149
|
|
|
309
150
|
async def make_sienge_bulk_request(
|
|
@@ -312,359 +153,94 @@ async def make_sienge_bulk_request(
|
|
|
312
153
|
"""
|
|
313
154
|
Função auxiliar para fazer requisições à API bulk-data do Sienge
|
|
314
155
|
Suporta tanto Bearer Token quanto Basic Auth
|
|
315
|
-
MELHORADO: Observabilidade e normalização de parâmetros
|
|
316
156
|
"""
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
try:
|
|
321
|
-
# Normalizar parâmetros
|
|
322
|
-
normalized_params = to_query(params) if params else None
|
|
323
|
-
|
|
324
|
-
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
325
|
-
headers = {
|
|
326
|
-
"Content-Type": "application/json",
|
|
327
|
-
"Accept": "application/json",
|
|
328
|
-
"X-Request-ID": req_id
|
|
329
|
-
}
|
|
157
|
+
# Similar to make_sienge_request but targeting bulk-data endpoints
|
|
158
|
+
request_id = str(uuid.uuid4())
|
|
159
|
+
start_ts = time.time()
|
|
330
160
|
|
|
331
|
-
|
|
332
|
-
auth = None
|
|
333
|
-
base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
|
|
161
|
+
headers = {"Content-Type": "application/json", "Accept": "application/json", "X-Request-Id": request_id}
|
|
334
162
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
163
|
+
auth = None
|
|
164
|
+
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
165
|
+
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
166
|
+
url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
|
|
167
|
+
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
168
|
+
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
169
|
+
url = f"{SIENGE_BASE_URL}/{SIENGE_SUBDOMAIN}/public/api/bulk-data/v1{endpoint}"
|
|
170
|
+
else:
|
|
171
|
+
return {
|
|
172
|
+
"success": False,
|
|
173
|
+
"error": "No Authentication",
|
|
174
|
+
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
175
|
+
"request_id": request_id,
|
|
176
|
+
}
|
|
348
177
|
|
|
349
|
-
|
|
350
|
-
|
|
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)
|
|
178
|
+
async def _do_request(client: httpx.AsyncClient):
|
|
179
|
+
return await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
|
|
360
180
|
|
|
361
|
-
|
|
181
|
+
try:
|
|
182
|
+
max_attempts = 5
|
|
183
|
+
attempts = 0
|
|
184
|
+
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
185
|
+
while True:
|
|
186
|
+
attempts += 1
|
|
362
187
|
try:
|
|
188
|
+
response = await client.request(method=method, url=url, headers=headers, params=params, json=json_data, auth=auth)
|
|
189
|
+
except (httpx.RequestError, httpx.TimeoutException) as exc:
|
|
190
|
+
logger.warning(f"Bulk request error to {url}: {exc} (attempt {attempts}/{max_attempts})")
|
|
191
|
+
if attempts >= max_attempts:
|
|
192
|
+
raise
|
|
193
|
+
await asyncio.sleep(min(2 ** attempts, 60))
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
if response.status_code == 429:
|
|
197
|
+
retry_after = response.headers.get("Retry-After")
|
|
198
|
+
try:
|
|
199
|
+
wait_seconds = int(retry_after) if retry_after is not None else min(2 ** attempts, 60)
|
|
200
|
+
except Exception:
|
|
201
|
+
wait_seconds = min(2 ** attempts, 60)
|
|
202
|
+
logger.warning(f"HTTP 429 from bulk {url}, retrying after {wait_seconds}s (attempt {attempts}/{max_attempts})")
|
|
203
|
+
if attempts >= max_attempts:
|
|
204
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
205
|
+
return {"success": False, "error": "HTTP 429", "message": response.text, "status_code": 429, "latency_ms": latency_ms, "request_id": request_id}
|
|
206
|
+
await asyncio.sleep(wait_seconds)
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
210
|
+
|
|
211
|
+
if response.status_code in [200, 201]:
|
|
212
|
+
try:
|
|
213
|
+
return {"success": True, "data": response.json(), "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
|
|
214
|
+
except BaseException:
|
|
215
|
+
return {"success": True, "data": {"message": "Success"}, "status_code": response.status_code, "latency_ms": latency_ms, "request_id": request_id}
|
|
216
|
+
else:
|
|
217
|
+
logger.warning(f"HTTP {response.status_code} from bulk {url}: {response.text}")
|
|
363
218
|
return {
|
|
364
|
-
"success":
|
|
365
|
-
"
|
|
219
|
+
"success": False,
|
|
220
|
+
"error": f"HTTP {response.status_code}",
|
|
221
|
+
"message": response.text,
|
|
366
222
|
"status_code": response.status_code,
|
|
367
|
-
"
|
|
368
|
-
"
|
|
223
|
+
"latency_ms": latency_ms,
|
|
224
|
+
"request_id": request_id,
|
|
369
225
|
}
|
|
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
|
-
}
|
|
378
|
-
else:
|
|
379
|
-
return {
|
|
380
|
-
"success": False,
|
|
381
|
-
"error": f"HTTP {response.status_code}",
|
|
382
|
-
"message": response.text,
|
|
383
|
-
"status_code": response.status_code,
|
|
384
|
-
"request_id": req_id,
|
|
385
|
-
"latency_ms": round(latency * 1000, 2)
|
|
386
|
-
}
|
|
387
226
|
|
|
388
227
|
except httpx.TimeoutException:
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
}
|
|
228
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
229
|
+
return {"success": False, "error": "Timeout", "message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s", "latency_ms": latency_ms, "request_id": request_id}
|
|
398
230
|
except Exception as e:
|
|
399
|
-
|
|
400
|
-
|
|
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)
|
|
231
|
+
latency_ms = int((time.time() - start_ts) * 1000)
|
|
232
|
+
return {"success": False, "error": str(e), "message": f"Erro na requisição bulk-data: {str(e)}", "latency_ms": latency_ms, "request_id": request_id}
|
|
657
233
|
|
|
658
234
|
|
|
659
235
|
# ============ CONEXÃO E TESTE ============
|
|
660
236
|
|
|
661
237
|
|
|
662
238
|
@mcp.tool
|
|
663
|
-
async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
|
|
664
|
-
"""Testa a conexão com a API do Sienge"""
|
|
239
|
+
async def test_sienge_connection(_meta: Optional[Dict[str, Any]] = None) -> Dict:
|
|
240
|
+
"""Testa a conexão com a API do Sienge e retorna métricas básicas"""
|
|
665
241
|
try:
|
|
666
|
-
#
|
|
667
|
-
result = await
|
|
242
|
+
# Tentar endpoint mais simples primeiro
|
|
243
|
+
result = await make_sienge_request("GET", "/customer-types")
|
|
668
244
|
|
|
669
245
|
if result["success"]:
|
|
670
246
|
auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
|
|
@@ -674,9 +250,8 @@ async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
|
|
|
674
250
|
"api_status": "Online",
|
|
675
251
|
"auth_method": auth_method,
|
|
676
252
|
"timestamp": datetime.now().isoformat(),
|
|
677
|
-
"request_id": result.get("request_id"),
|
|
678
253
|
"latency_ms": result.get("latency_ms"),
|
|
679
|
-
"
|
|
254
|
+
"request_id": result.get("request_id"),
|
|
680
255
|
}
|
|
681
256
|
else:
|
|
682
257
|
return {
|
|
@@ -685,7 +260,8 @@ async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
|
|
|
685
260
|
"error": result.get("error"),
|
|
686
261
|
"details": result.get("message"),
|
|
687
262
|
"timestamp": datetime.now().isoformat(),
|
|
688
|
-
"
|
|
263
|
+
"latency_ms": result.get("latency_ms"),
|
|
264
|
+
"request_id": result.get("request_id"),
|
|
689
265
|
}
|
|
690
266
|
except Exception as e:
|
|
691
267
|
return {
|
|
@@ -701,8 +277,12 @@ async def test_sienge_connection(_meta: Optional[Dict] = None) -> Dict:
|
|
|
701
277
|
|
|
702
278
|
@mcp.tool
|
|
703
279
|
async def get_sienge_customers(
|
|
704
|
-
limit: Optional[int] = 50,
|
|
705
|
-
|
|
280
|
+
limit: Optional[int] = 50,
|
|
281
|
+
offset: Optional[int] = 0,
|
|
282
|
+
search: Optional[str] = None,
|
|
283
|
+
customer_type_id: Optional[str] = None,
|
|
284
|
+
fetch_all: Optional[bool] = False,
|
|
285
|
+
max_records: Optional[int] = None,
|
|
706
286
|
) -> Dict:
|
|
707
287
|
"""
|
|
708
288
|
Busca clientes no Sienge com filtros
|
|
@@ -713,13 +293,44 @@ async def get_sienge_customers(
|
|
|
713
293
|
search: Buscar por nome ou documento
|
|
714
294
|
customer_type_id: Filtrar por tipo de cliente
|
|
715
295
|
"""
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
customer_type_id=customer_type_id
|
|
722
|
-
|
|
296
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
297
|
+
|
|
298
|
+
if search:
|
|
299
|
+
params["search"] = search
|
|
300
|
+
if customer_type_id:
|
|
301
|
+
params["customer_type_id"] = customer_type_id
|
|
302
|
+
|
|
303
|
+
# Basic in-memory cache for lightweight GETs
|
|
304
|
+
cache_key = f"customers:{limit}:{offset}:{search}:{customer_type_id}"
|
|
305
|
+
try:
|
|
306
|
+
cached = _simple_cache_get(cache_key)
|
|
307
|
+
if cached:
|
|
308
|
+
return cached
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
# If caller asked to fetch all, use helper to iterate pages
|
|
313
|
+
if fetch_all:
|
|
314
|
+
items = await _fetch_all_paginated("/customers", params=params, page_size=200, max_records=max_records)
|
|
315
|
+
if isinstance(items, dict) and not items.get("success", True):
|
|
316
|
+
return {"success": False, "error": items.get("error"), "message": items.get("message")}
|
|
317
|
+
|
|
318
|
+
customers = items
|
|
319
|
+
total_count = len(customers)
|
|
320
|
+
response = {
|
|
321
|
+
"success": True,
|
|
322
|
+
"message": f"✅ Encontrados {len(customers)} clientes (fetch_all)",
|
|
323
|
+
"customers": customers,
|
|
324
|
+
"count": len(customers),
|
|
325
|
+
"filters_applied": params,
|
|
326
|
+
}
|
|
327
|
+
try:
|
|
328
|
+
_simple_cache_set(cache_key, response, ttl=30)
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
return response
|
|
332
|
+
|
|
333
|
+
result = await make_sienge_request("GET", "/customers", params=params)
|
|
723
334
|
|
|
724
335
|
if result["success"]:
|
|
725
336
|
data = result["data"]
|
|
@@ -727,34 +338,31 @@ async def get_sienge_customers(
|
|
|
727
338
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
728
339
|
total_count = metadata.get("count", len(customers))
|
|
729
340
|
|
|
730
|
-
|
|
341
|
+
response = {
|
|
731
342
|
"success": True,
|
|
732
343
|
"message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
|
|
733
344
|
"customers": customers,
|
|
734
345
|
"count": len(customers),
|
|
735
|
-
"
|
|
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")
|
|
346
|
+
"filters_applied": params,
|
|
742
347
|
}
|
|
348
|
+
try:
|
|
349
|
+
_simple_cache_set(cache_key, response, ttl=30)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
return response
|
|
743
353
|
|
|
744
354
|
return {
|
|
745
355
|
"success": False,
|
|
746
356
|
"message": "❌ Erro ao buscar clientes",
|
|
747
357
|
"error": result.get("error"),
|
|
748
358
|
"details": result.get("message"),
|
|
749
|
-
"request_id": result.get("request_id")
|
|
750
359
|
}
|
|
751
360
|
|
|
752
361
|
|
|
753
362
|
@mcp.tool
|
|
754
|
-
async def get_sienge_customer_types(
|
|
363
|
+
async def get_sienge_customer_types() -> Dict:
|
|
755
364
|
"""Lista tipos de clientes disponíveis"""
|
|
756
|
-
|
|
757
|
-
result = await _svc_get_customer_types()
|
|
365
|
+
result = await make_sienge_request("GET", "/customer-types")
|
|
758
366
|
|
|
759
367
|
if result["success"]:
|
|
760
368
|
data = result["data"]
|
|
@@ -762,211 +370,23 @@ async def get_sienge_customer_types(_meta: Optional[Dict] = None) -> Dict:
|
|
|
762
370
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
763
371
|
total_count = metadata.get("count", len(customer_types))
|
|
764
372
|
|
|
765
|
-
|
|
373
|
+
response = {
|
|
766
374
|
"success": True,
|
|
767
375
|
"message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
|
|
768
376
|
"customer_types": customer_types,
|
|
769
377
|
"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")
|
|
774
378
|
}
|
|
775
|
-
|
|
776
|
-
return {
|
|
777
|
-
"success": False,
|
|
778
|
-
"message": "❌ Erro ao buscar tipos de clientes",
|
|
779
|
-
"error": result.get("error"),
|
|
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
379
|
try:
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
}
|
|
380
|
+
_simple_cache_set("customer_types", response, ttl=300)
|
|
381
|
+
except Exception:
|
|
382
|
+
pass
|
|
383
|
+
return response
|
|
963
384
|
|
|
964
385
|
return {
|
|
965
386
|
"success": False,
|
|
966
|
-
"message": "❌ Erro ao buscar
|
|
387
|
+
"message": "❌ Erro ao buscar tipos de clientes",
|
|
967
388
|
"error": result.get("error"),
|
|
968
389
|
"details": result.get("message"),
|
|
969
|
-
"request_id": result.get("request_id")
|
|
970
390
|
}
|
|
971
391
|
|
|
972
392
|
|
|
@@ -974,8 +394,13 @@ async def list_sienge_purchase_requests(limit: int = 50, status: str = None,
|
|
|
974
394
|
|
|
975
395
|
|
|
976
396
|
@mcp.tool
|
|
977
|
-
async def get_sienge_creditors(
|
|
978
|
-
|
|
397
|
+
async def get_sienge_creditors(
|
|
398
|
+
limit: Optional[int] = 50,
|
|
399
|
+
offset: Optional[int] = 0,
|
|
400
|
+
search: Optional[str] = None,
|
|
401
|
+
fetch_all: Optional[bool] = False,
|
|
402
|
+
max_records: Optional[int] = None,
|
|
403
|
+
) -> Dict:
|
|
979
404
|
"""
|
|
980
405
|
Busca credores/fornecedores
|
|
981
406
|
|
|
@@ -984,12 +409,38 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
984
409
|
offset: Pular registros (padrão: 0)
|
|
985
410
|
search: Buscar por nome
|
|
986
411
|
"""
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
412
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
413
|
+
if search:
|
|
414
|
+
params["search"] = search
|
|
415
|
+
|
|
416
|
+
cache_key = f"creditors:{limit}:{offset}:{search}:{fetch_all}:{max_records}"
|
|
417
|
+
try:
|
|
418
|
+
cached = _simple_cache_get(cache_key)
|
|
419
|
+
if cached:
|
|
420
|
+
return cached
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
# Support fetching all pages when requested
|
|
425
|
+
if fetch_all:
|
|
426
|
+
items = await _fetch_all_paginated("/creditors", params=params, page_size=200, max_records=max_records)
|
|
427
|
+
if isinstance(items, dict) and not items.get("success", True):
|
|
428
|
+
return {"success": False, "error": items.get("error"), "message": items.get("message")}
|
|
429
|
+
|
|
430
|
+
creditors = items
|
|
431
|
+
response = {
|
|
432
|
+
"success": True,
|
|
433
|
+
"message": f"✅ Encontrados {len(creditors)} credores (fetch_all)",
|
|
434
|
+
"creditors": creditors,
|
|
435
|
+
"count": len(creditors),
|
|
436
|
+
}
|
|
437
|
+
try:
|
|
438
|
+
_simple_cache_set(cache_key, response, ttl=30)
|
|
439
|
+
except Exception:
|
|
440
|
+
pass
|
|
441
|
+
return response
|
|
442
|
+
|
|
443
|
+
result = await make_sienge_request("GET", "/creditors", params=params)
|
|
993
444
|
|
|
994
445
|
if result["success"]:
|
|
995
446
|
data = result["data"]
|
|
@@ -997,38 +448,35 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
997
448
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
998
449
|
total_count = metadata.get("count", len(creditors))
|
|
999
450
|
|
|
1000
|
-
|
|
451
|
+
response = {
|
|
1001
452
|
"success": True,
|
|
1002
453
|
"message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
|
|
1003
454
|
"creditors": creditors,
|
|
1004
455
|
"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")
|
|
1010
456
|
}
|
|
457
|
+
try:
|
|
458
|
+
_simple_cache_set(cache_key, response, ttl=30)
|
|
459
|
+
except Exception:
|
|
460
|
+
pass
|
|
461
|
+
return response
|
|
1011
462
|
|
|
1012
463
|
return {
|
|
1013
464
|
"success": False,
|
|
1014
465
|
"message": "❌ Erro ao buscar credores",
|
|
1015
466
|
"error": result.get("error"),
|
|
1016
467
|
"details": result.get("message"),
|
|
1017
|
-
"request_id": result.get("request_id")
|
|
1018
468
|
}
|
|
1019
469
|
|
|
1020
470
|
|
|
1021
471
|
@mcp.tool
|
|
1022
|
-
async def get_sienge_creditor_bank_info(creditor_id: str
|
|
1023
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
472
|
+
async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
|
|
1024
473
|
"""
|
|
1025
474
|
Consulta informações bancárias de um credor
|
|
1026
475
|
|
|
1027
476
|
Args:
|
|
1028
477
|
creditor_id: ID do credor (obrigatório)
|
|
1029
478
|
"""
|
|
1030
|
-
|
|
1031
|
-
result = await _svc_get_creditor_bank_info(creditor_id=creditor_id)
|
|
479
|
+
result = await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
|
|
1032
480
|
|
|
1033
481
|
if result["success"]:
|
|
1034
482
|
return {
|
|
@@ -1036,8 +484,6 @@ async def get_sienge_creditor_bank_info(creditor_id: str,
|
|
|
1036
484
|
"message": f"✅ Informações bancárias do credor {creditor_id}",
|
|
1037
485
|
"creditor_id": creditor_id,
|
|
1038
486
|
"bank_info": result["data"],
|
|
1039
|
-
"request_id": result.get("request_id"),
|
|
1040
|
-
"latency_ms": result.get("latency_ms")
|
|
1041
487
|
}
|
|
1042
488
|
|
|
1043
489
|
return {
|
|
@@ -1045,7 +491,6 @@ async def get_sienge_creditor_bank_info(creditor_id: str,
|
|
|
1045
491
|
"message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
|
|
1046
492
|
"error": result.get("error"),
|
|
1047
493
|
"details": result.get("message"),
|
|
1048
|
-
"request_id": result.get("request_id")
|
|
1049
494
|
}
|
|
1050
495
|
|
|
1051
496
|
|
|
@@ -1066,10 +511,9 @@ async def get_sienge_accounts_receivable(
|
|
|
1066
511
|
origins_ids: Optional[List[str]] = None,
|
|
1067
512
|
bearers_id_in: Optional[List[int]] = None,
|
|
1068
513
|
bearers_id_not_in: Optional[List[int]] = None,
|
|
1069
|
-
|
|
514
|
+
) -> Dict:
|
|
1070
515
|
"""
|
|
1071
516
|
Consulta parcelas do contas a receber via API bulk-data
|
|
1072
|
-
MELHORADO: Suporte a polling assíncrono para requests 202
|
|
1073
517
|
|
|
1074
518
|
Args:
|
|
1075
519
|
start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
|
|
@@ -1085,121 +529,90 @@ async def get_sienge_accounts_receivable(
|
|
|
1085
529
|
bearers_id_in: Filtrar parcelas com códigos de portador específicos
|
|
1086
530
|
bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
|
|
1087
531
|
"""
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
532
|
+
params = {"startDate": start_date, "endDate": end_date, "selectionType": selection_type}
|
|
533
|
+
|
|
534
|
+
if company_id:
|
|
535
|
+
params["companyId"] = company_id
|
|
536
|
+
if cost_centers_id:
|
|
537
|
+
params["costCentersId"] = cost_centers_id
|
|
538
|
+
if correction_indexer_id:
|
|
539
|
+
params["correctionIndexerId"] = correction_indexer_id
|
|
540
|
+
if correction_date:
|
|
541
|
+
params["correctionDate"] = correction_date
|
|
542
|
+
if change_start_date:
|
|
543
|
+
params["changeStartDate"] = change_start_date
|
|
544
|
+
if completed_bills:
|
|
545
|
+
params["completedBills"] = completed_bills
|
|
546
|
+
if origins_ids:
|
|
547
|
+
params["originsIds"] = origins_ids
|
|
548
|
+
if bearers_id_in:
|
|
549
|
+
params["bearersIdIn"] = bearers_id_in
|
|
550
|
+
if bearers_id_not_in:
|
|
551
|
+
params["bearersIdNotIn"] = bearers_id_not_in
|
|
552
|
+
|
|
553
|
+
result = await make_sienge_bulk_request("GET", "/income", params=params)
|
|
1103
554
|
|
|
1104
555
|
if result["success"]:
|
|
1105
|
-
|
|
1106
|
-
income_data =
|
|
1107
|
-
|
|
1108
|
-
|
|
556
|
+
data = result["data"]
|
|
557
|
+
income_data = data.get("data", []) if isinstance(data, dict) else data
|
|
558
|
+
|
|
559
|
+
return {
|
|
1109
560
|
"success": True,
|
|
1110
561
|
"message": f"✅ Encontradas {len(income_data)} parcelas a receber",
|
|
1111
562
|
"income_data": income_data,
|
|
1112
563
|
"count": len(income_data),
|
|
1113
564
|
"period": f"{start_date} a {end_date}",
|
|
1114
565
|
"selection_type": selection_type,
|
|
1115
|
-
"
|
|
1116
|
-
"latency_ms": result.get("latency_ms")
|
|
566
|
+
"filters": params,
|
|
1117
567
|
}
|
|
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
|
|
1132
568
|
|
|
1133
569
|
return {
|
|
1134
570
|
"success": False,
|
|
1135
571
|
"message": "❌ Erro ao buscar parcelas a receber",
|
|
1136
572
|
"error": result.get("error"),
|
|
1137
573
|
"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
|
|
1143
574
|
}
|
|
1144
575
|
|
|
1145
576
|
|
|
1146
577
|
@mcp.tool
|
|
1147
578
|
async def get_sienge_accounts_receivable_by_bills(
|
|
1148
|
-
bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None
|
|
1149
|
-
|
|
579
|
+
bills_ids: List[int], correction_indexer_id: Optional[int] = None, correction_date: Optional[str] = None
|
|
580
|
+
) -> Dict:
|
|
1150
581
|
"""
|
|
1151
582
|
Consulta parcelas dos títulos informados via API bulk-data
|
|
1152
|
-
MELHORADO: Suporte a polling assíncrono para requests 202
|
|
1153
583
|
|
|
1154
584
|
Args:
|
|
1155
585
|
bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
|
|
1156
586
|
correction_indexer_id: Código do indexador de correção
|
|
1157
587
|
correction_date: Data para correção do indexador (YYYY-MM-DD)
|
|
1158
588
|
"""
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
589
|
+
params = {"billsIds": bills_ids}
|
|
590
|
+
|
|
591
|
+
if correction_indexer_id:
|
|
592
|
+
params["correctionIndexerId"] = correction_indexer_id
|
|
593
|
+
if correction_date:
|
|
594
|
+
params["correctionDate"] = correction_date
|
|
595
|
+
|
|
596
|
+
result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
|
|
1165
597
|
|
|
1166
598
|
if result["success"]:
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
599
|
+
data = result["data"]
|
|
600
|
+
income_data = data.get("data", []) if isinstance(data, dict) else data
|
|
601
|
+
|
|
602
|
+
return {
|
|
1170
603
|
"success": True,
|
|
1171
604
|
"message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
|
|
1172
605
|
"income_data": income_data,
|
|
1173
606
|
"count": len(income_data),
|
|
1174
607
|
"bills_consulted": bills_ids,
|
|
1175
|
-
"
|
|
1176
|
-
"latency_ms": result.get("latency_ms")
|
|
608
|
+
"filters": params,
|
|
1177
609
|
}
|
|
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
|
|
1192
610
|
|
|
1193
611
|
return {
|
|
1194
612
|
"success": False,
|
|
1195
613
|
"message": "❌ Erro ao buscar parcelas dos títulos informados",
|
|
1196
614
|
"error": result.get("error"),
|
|
1197
615
|
"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
|
|
1203
616
|
}
|
|
1204
617
|
|
|
1205
618
|
|
|
@@ -1210,7 +623,7 @@ async def get_sienge_bills(
|
|
|
1210
623
|
creditor_id: Optional[str] = None,
|
|
1211
624
|
status: Optional[str] = None,
|
|
1212
625
|
limit: Optional[int] = 50,
|
|
1213
|
-
|
|
626
|
+
) -> Dict:
|
|
1214
627
|
"""
|
|
1215
628
|
Consulta títulos a pagar (contas a pagar) - REQUER startDate obrigatório
|
|
1216
629
|
|
|
@@ -1221,14 +634,26 @@ async def get_sienge_bills(
|
|
|
1221
634
|
status: Status do título (ex: open, paid, cancelled)
|
|
1222
635
|
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
1223
636
|
"""
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
637
|
+
from datetime import datetime, timedelta
|
|
638
|
+
|
|
639
|
+
# Se start_date não fornecido, usar últimos 30 dias
|
|
640
|
+
if not start_date:
|
|
641
|
+
start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
642
|
+
|
|
643
|
+
# Se end_date não fornecido, usar hoje
|
|
644
|
+
if not end_date:
|
|
645
|
+
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
646
|
+
|
|
647
|
+
# Parâmetros obrigatórios
|
|
648
|
+
params = {"startDate": start_date, "endDate": end_date, "limit": min(limit or 50, 200)} # OBRIGATÓRIO pela API
|
|
649
|
+
|
|
650
|
+
# Parâmetros opcionais
|
|
651
|
+
if creditor_id:
|
|
652
|
+
params["creditor_id"] = creditor_id
|
|
653
|
+
if status:
|
|
654
|
+
params["status"] = status
|
|
655
|
+
|
|
656
|
+
result = await make_sienge_request("GET", "/bills", params=params)
|
|
1232
657
|
|
|
1233
658
|
if result["success"]:
|
|
1234
659
|
data = result["data"]
|
|
@@ -1236,33 +661,14 @@ async def get_sienge_bills(
|
|
|
1236
661
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
1237
662
|
total_count = metadata.get("count", len(bills))
|
|
1238
663
|
|
|
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
|
-
|
|
1252
664
|
return {
|
|
1253
665
|
"success": True,
|
|
1254
|
-
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {
|
|
666
|
+
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {start_date} a {end_date}",
|
|
1255
667
|
"bills": bills,
|
|
1256
668
|
"count": len(bills),
|
|
1257
669
|
"total_count": total_count,
|
|
1258
|
-
"period": {"start_date":
|
|
1259
|
-
"
|
|
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")
|
|
670
|
+
"period": {"start_date": start_date, "end_date": end_date},
|
|
671
|
+
"filters": params,
|
|
1266
672
|
}
|
|
1267
673
|
|
|
1268
674
|
return {
|
|
@@ -1270,7 +676,6 @@ async def get_sienge_bills(
|
|
|
1270
676
|
"message": "❌ Erro ao buscar títulos a pagar",
|
|
1271
677
|
"error": result.get("error"),
|
|
1272
678
|
"details": result.get("message"),
|
|
1273
|
-
"request_id": result.get("request_id")
|
|
1274
679
|
}
|
|
1275
680
|
|
|
1276
681
|
|
|
@@ -1283,7 +688,7 @@ async def get_sienge_purchase_orders(
|
|
|
1283
688
|
status: Optional[str] = None,
|
|
1284
689
|
date_from: Optional[str] = None,
|
|
1285
690
|
limit: Optional[int] = 50,
|
|
1286
|
-
|
|
691
|
+
) -> Dict:
|
|
1287
692
|
"""
|
|
1288
693
|
Consulta pedidos de compra
|
|
1289
694
|
|
|
@@ -1316,8 +721,6 @@ async def get_sienge_purchase_orders(
|
|
|
1316
721
|
"message": f"✅ Encontrados {len(orders)} pedidos de compra",
|
|
1317
722
|
"purchase_orders": orders,
|
|
1318
723
|
"count": len(orders),
|
|
1319
|
-
"request_id": result.get("request_id"),
|
|
1320
|
-
"latency_ms": result.get("latency_ms")
|
|
1321
724
|
}
|
|
1322
725
|
|
|
1323
726
|
return {
|
|
@@ -1325,14 +728,11 @@ async def get_sienge_purchase_orders(
|
|
|
1325
728
|
"message": "❌ Erro ao buscar pedidos de compra",
|
|
1326
729
|
"error": result.get("error"),
|
|
1327
730
|
"details": result.get("message"),
|
|
1328
|
-
"request_id": result.get("request_id"),
|
|
1329
|
-
"latency_ms": result.get("latency_ms")
|
|
1330
731
|
}
|
|
1331
732
|
|
|
1332
733
|
|
|
1333
734
|
@mcp.tool
|
|
1334
|
-
async def get_sienge_purchase_order_items(purchase_order_id: str
|
|
1335
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
735
|
+
async def get_sienge_purchase_order_items(purchase_order_id: str) -> Dict:
|
|
1336
736
|
"""
|
|
1337
737
|
Consulta itens de um pedido de compra específico
|
|
1338
738
|
|
|
@@ -1362,8 +762,7 @@ async def get_sienge_purchase_order_items(purchase_order_id: str,
|
|
|
1362
762
|
|
|
1363
763
|
|
|
1364
764
|
@mcp.tool
|
|
1365
|
-
async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50
|
|
1366
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
765
|
+
async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None, limit: Optional[int] = 50) -> Dict:
|
|
1367
766
|
"""
|
|
1368
767
|
Consulta solicitações de compra
|
|
1369
768
|
|
|
@@ -1404,8 +803,7 @@ async def get_sienge_purchase_requests(purchase_request_id: Optional[str] = None
|
|
|
1404
803
|
|
|
1405
804
|
|
|
1406
805
|
@mcp.tool
|
|
1407
|
-
async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]
|
|
1408
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
806
|
+
async def create_sienge_purchase_request(description: str, project_id: str, items: List[Dict[str, Any]]) -> Dict:
|
|
1409
807
|
"""
|
|
1410
808
|
Cria nova solicitação de compra
|
|
1411
809
|
|
|
@@ -1421,18 +819,14 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
1421
819
|
"date": datetime.now().strftime("%Y-%m-%d"),
|
|
1422
820
|
}
|
|
1423
821
|
|
|
1424
|
-
|
|
1425
|
-
json_data = to_camel_json(request_data)
|
|
1426
|
-
result = await make_sienge_request("POST", "/purchase-requests", json_data=json_data)
|
|
822
|
+
result = await make_sienge_request("POST", "/purchase-requests", json_data=request_data)
|
|
1427
823
|
|
|
1428
824
|
if result["success"]:
|
|
1429
825
|
return {
|
|
1430
826
|
"success": True,
|
|
1431
827
|
"message": "✅ Solicitação de compra criada com sucesso",
|
|
1432
|
-
"request_id": result.get("
|
|
1433
|
-
"purchase_request_id": result["data"].get("id"),
|
|
828
|
+
"request_id": result["data"].get("id"),
|
|
1434
829
|
"data": result["data"],
|
|
1435
|
-
"latency_ms": result.get("latency_ms")
|
|
1436
830
|
}
|
|
1437
831
|
|
|
1438
832
|
return {
|
|
@@ -1440,7 +834,6 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
1440
834
|
"message": "❌ Erro ao criar solicitação de compra",
|
|
1441
835
|
"error": result.get("error"),
|
|
1442
836
|
"details": result.get("message"),
|
|
1443
|
-
"request_id": result.get("request_id")
|
|
1444
837
|
}
|
|
1445
838
|
|
|
1446
839
|
|
|
@@ -1448,8 +841,7 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
1448
841
|
|
|
1449
842
|
|
|
1450
843
|
@mcp.tool
|
|
1451
|
-
async def get_sienge_purchase_invoice(sequential_number: int
|
|
1452
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
844
|
+
async def get_sienge_purchase_invoice(sequential_number: int) -> Dict:
|
|
1453
845
|
"""
|
|
1454
846
|
Consulta nota fiscal de compra por número sequencial
|
|
1455
847
|
|
|
@@ -1470,8 +862,7 @@ async def get_sienge_purchase_invoice(sequential_number: int,
|
|
|
1470
862
|
|
|
1471
863
|
|
|
1472
864
|
@mcp.tool
|
|
1473
|
-
async def get_sienge_purchase_invoice_items(sequential_number: int
|
|
1474
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
865
|
+
async def get_sienge_purchase_invoice_items(sequential_number: int) -> Dict:
|
|
1475
866
|
"""
|
|
1476
867
|
Consulta itens de uma nota fiscal de compra
|
|
1477
868
|
|
|
@@ -1512,7 +903,7 @@ async def create_sienge_purchase_invoice(
|
|
|
1512
903
|
issue_date: str,
|
|
1513
904
|
series: Optional[str] = None,
|
|
1514
905
|
notes: Optional[str] = None,
|
|
1515
|
-
|
|
906
|
+
) -> Dict:
|
|
1516
907
|
"""
|
|
1517
908
|
Cadastra uma nova nota fiscal de compra
|
|
1518
909
|
|
|
@@ -1528,13 +919,13 @@ async def create_sienge_purchase_invoice(
|
|
|
1528
919
|
notes: Observações (opcional)
|
|
1529
920
|
"""
|
|
1530
921
|
invoice_data = {
|
|
1531
|
-
"
|
|
922
|
+
"documentId": document_id,
|
|
1532
923
|
"number": number,
|
|
1533
|
-
"
|
|
1534
|
-
"
|
|
1535
|
-
"
|
|
1536
|
-
"
|
|
1537
|
-
"
|
|
924
|
+
"supplierId": supplier_id,
|
|
925
|
+
"companyId": company_id,
|
|
926
|
+
"movementTypeId": movement_type_id,
|
|
927
|
+
"movementDate": movement_date,
|
|
928
|
+
"issueDate": issue_date,
|
|
1538
929
|
}
|
|
1539
930
|
|
|
1540
931
|
if series:
|
|
@@ -1542,7 +933,7 @@ async def create_sienge_purchase_invoice(
|
|
|
1542
933
|
if notes:
|
|
1543
934
|
invoice_data["notes"] = notes
|
|
1544
935
|
|
|
1545
|
-
result = await make_sienge_request("POST", "/purchase-invoices", json_data=
|
|
936
|
+
result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
|
|
1546
937
|
|
|
1547
938
|
if result["success"]:
|
|
1548
939
|
return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
|
|
@@ -1562,7 +953,7 @@ async def add_items_to_purchase_invoice(
|
|
|
1562
953
|
copy_notes_purchase_orders: bool = True,
|
|
1563
954
|
copy_notes_resources: bool = False,
|
|
1564
955
|
copy_attachments_purchase_orders: bool = True,
|
|
1565
|
-
|
|
956
|
+
) -> Dict:
|
|
1566
957
|
"""
|
|
1567
958
|
Insere itens em uma nota fiscal a partir de entregas de pedidos de compra
|
|
1568
959
|
|
|
@@ -1579,14 +970,14 @@ async def add_items_to_purchase_invoice(
|
|
|
1579
970
|
copy_attachments_purchase_orders: Copiar anexos dos pedidos de compra
|
|
1580
971
|
"""
|
|
1581
972
|
item_data = {
|
|
1582
|
-
"
|
|
1583
|
-
"
|
|
1584
|
-
"
|
|
1585
|
-
"
|
|
973
|
+
"deliveriesOrder": deliveries_order,
|
|
974
|
+
"copyNotesPurchaseOrders": copy_notes_purchase_orders,
|
|
975
|
+
"copyNotesResources": copy_notes_resources,
|
|
976
|
+
"copyAttachmentsPurchaseOrders": copy_attachments_purchase_orders,
|
|
1586
977
|
}
|
|
1587
978
|
|
|
1588
979
|
result = await make_sienge_request(
|
|
1589
|
-
"POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=
|
|
980
|
+
"POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data
|
|
1590
981
|
)
|
|
1591
982
|
|
|
1592
983
|
if result["success"]:
|
|
@@ -1613,7 +1004,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(
|
|
|
1613
1004
|
purchase_order_item_number: Optional[int] = None,
|
|
1614
1005
|
limit: Optional[int] = 100,
|
|
1615
1006
|
offset: Optional[int] = 0,
|
|
1616
|
-
|
|
1007
|
+
) -> Dict:
|
|
1617
1008
|
"""
|
|
1618
1009
|
Lista entregas atendidas entre pedidos de compra e notas fiscais
|
|
1619
1010
|
|
|
@@ -1667,8 +1058,7 @@ async def get_sienge_purchase_invoices_deliveries_attended(
|
|
|
1667
1058
|
|
|
1668
1059
|
|
|
1669
1060
|
@mcp.tool
|
|
1670
|
-
async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None
|
|
1671
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
1061
|
+
async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[str] = None) -> Dict:
|
|
1672
1062
|
"""
|
|
1673
1063
|
Consulta inventário de estoque por centro de custo
|
|
1674
1064
|
|
|
@@ -1705,8 +1095,7 @@ async def get_sienge_stock_inventory(cost_center_id: str, resource_id: Optional[
|
|
|
1705
1095
|
|
|
1706
1096
|
|
|
1707
1097
|
@mcp.tool
|
|
1708
|
-
async def get_sienge_stock_reservations(limit: Optional[int] = 50
|
|
1709
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
1098
|
+
async def get_sienge_stock_reservations(limit: Optional[int] = 50) -> Dict:
|
|
1710
1099
|
"""
|
|
1711
1100
|
Lista reservas de estoque
|
|
1712
1101
|
|
|
@@ -1746,10 +1135,9 @@ async def get_sienge_projects(
|
|
|
1746
1135
|
enterprise_type: Optional[int] = None,
|
|
1747
1136
|
receivable_register: Optional[str] = None,
|
|
1748
1137
|
only_buildings_enabled: Optional[bool] = False,
|
|
1749
|
-
|
|
1138
|
+
) -> Dict:
|
|
1750
1139
|
"""
|
|
1751
1140
|
Busca empreendimentos/obras no Sienge
|
|
1752
|
-
CORRIGIDO: Mapeamento correto da chave de resposta
|
|
1753
1141
|
|
|
1754
1142
|
Args:
|
|
1755
1143
|
limit: Máximo de registros (padrão: 100, máximo: 200)
|
|
@@ -1759,39 +1147,31 @@ async def get_sienge_projects(
|
|
|
1759
1147
|
receivable_register: Filtro de registro de recebíveis (B3, CERC)
|
|
1760
1148
|
only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
|
|
1761
1149
|
"""
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1150
|
+
params = {"limit": min(limit or 100, 200), "offset": offset or 0}
|
|
1151
|
+
|
|
1152
|
+
if company_id:
|
|
1153
|
+
params["companyId"] = company_id
|
|
1154
|
+
if enterprise_type:
|
|
1155
|
+
params["type"] = enterprise_type
|
|
1156
|
+
if receivable_register:
|
|
1157
|
+
params["receivableRegister"] = receivable_register
|
|
1158
|
+
if only_buildings_enabled:
|
|
1159
|
+
params["onlyBuildingsEnabledForIntegration"] = only_buildings_enabled
|
|
1160
|
+
|
|
1161
|
+
result = await make_sienge_request("GET", "/enterprises", params=params)
|
|
1771
1162
|
|
|
1772
1163
|
if result["success"]:
|
|
1773
1164
|
data = result["data"]
|
|
1774
|
-
# CORREÇÃO: API retorna em "results", mas paginador espera em "enterprises"
|
|
1775
1165
|
enterprises = data.get("results", []) if isinstance(data, dict) else data
|
|
1776
1166
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
1777
|
-
total_count = metadata.get("count", len(enterprises))
|
|
1778
1167
|
|
|
1779
1168
|
return {
|
|
1780
1169
|
"success": True,
|
|
1781
|
-
"message": f"✅ Encontrados {len(enterprises)} empreendimentos
|
|
1782
|
-
"enterprises": enterprises,
|
|
1783
|
-
"projects": enterprises, # Alias para compatibilidade
|
|
1170
|
+
"message": f"✅ Encontrados {len(enterprises)} empreendimentos",
|
|
1171
|
+
"enterprises": enterprises,
|
|
1784
1172
|
"count": len(enterprises),
|
|
1785
|
-
"total_count": total_count,
|
|
1786
1173
|
"metadata": metadata,
|
|
1787
|
-
"
|
|
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")
|
|
1174
|
+
"filters": params,
|
|
1795
1175
|
}
|
|
1796
1176
|
|
|
1797
1177
|
return {
|
|
@@ -1799,13 +1179,11 @@ async def get_sienge_projects(
|
|
|
1799
1179
|
"message": "❌ Erro ao buscar empreendimentos",
|
|
1800
1180
|
"error": result.get("error"),
|
|
1801
1181
|
"details": result.get("message"),
|
|
1802
|
-
"request_id": result.get("request_id")
|
|
1803
1182
|
}
|
|
1804
1183
|
|
|
1805
1184
|
|
|
1806
1185
|
@mcp.tool
|
|
1807
|
-
async def get_sienge_enterprise_by_id(enterprise_id: int
|
|
1808
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
1186
|
+
async def get_sienge_enterprise_by_id(enterprise_id: int) -> Dict:
|
|
1809
1187
|
"""
|
|
1810
1188
|
Busca um empreendimento específico por ID no Sienge
|
|
1811
1189
|
|
|
@@ -1826,8 +1204,7 @@ async def get_sienge_enterprise_by_id(enterprise_id: int,
|
|
|
1826
1204
|
|
|
1827
1205
|
|
|
1828
1206
|
@mcp.tool
|
|
1829
|
-
async def get_sienge_enterprise_groupings(enterprise_id: int
|
|
1830
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
1207
|
+
async def get_sienge_enterprise_groupings(enterprise_id: int) -> Dict:
|
|
1831
1208
|
"""
|
|
1832
1209
|
Busca agrupamentos de unidades de um empreendimento específico
|
|
1833
1210
|
|
|
@@ -1854,8 +1231,7 @@ async def get_sienge_enterprise_groupings(enterprise_id: int,
|
|
|
1854
1231
|
|
|
1855
1232
|
|
|
1856
1233
|
@mcp.tool
|
|
1857
|
-
async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0
|
|
1858
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
1234
|
+
async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0) -> Dict:
|
|
1859
1235
|
"""
|
|
1860
1236
|
Consulta unidades cadastradas no Sienge
|
|
1861
1237
|
|
|
@@ -1877,9 +1253,6 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0,
|
|
|
1877
1253
|
"message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
|
|
1878
1254
|
"units": units,
|
|
1879
1255
|
"count": len(units),
|
|
1880
|
-
"total_count": total_count,
|
|
1881
|
-
"request_id": result.get("request_id"),
|
|
1882
|
-
"latency_ms": result.get("latency_ms")
|
|
1883
1256
|
}
|
|
1884
1257
|
|
|
1885
1258
|
return {
|
|
@@ -1887,8 +1260,6 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0,
|
|
|
1887
1260
|
"message": "❌ Erro ao buscar unidades",
|
|
1888
1261
|
"error": result.get("error"),
|
|
1889
1262
|
"details": result.get("message"),
|
|
1890
|
-
"request_id": result.get("request_id"),
|
|
1891
|
-
"latency_ms": result.get("latency_ms")
|
|
1892
1263
|
}
|
|
1893
1264
|
|
|
1894
1265
|
|
|
@@ -1901,7 +1272,7 @@ async def get_sienge_unit_cost_tables(
|
|
|
1901
1272
|
description: Optional[str] = None,
|
|
1902
1273
|
status: Optional[str] = "Active",
|
|
1903
1274
|
integration_enabled: Optional[bool] = None,
|
|
1904
|
-
|
|
1275
|
+
) -> Dict:
|
|
1905
1276
|
"""
|
|
1906
1277
|
Consulta tabelas de custos unitários
|
|
1907
1278
|
|
|
@@ -1949,8 +1320,8 @@ async def search_sienge_data(
|
|
|
1949
1320
|
query: str,
|
|
1950
1321
|
entity_type: Optional[str] = None,
|
|
1951
1322
|
limit: Optional[int] = 20,
|
|
1952
|
-
filters: Optional[Dict[str, Any]] = None
|
|
1953
|
-
|
|
1323
|
+
filters: Optional[Dict[str, Any]] = None
|
|
1324
|
+
) -> Dict:
|
|
1954
1325
|
"""
|
|
1955
1326
|
Busca universal no Sienge - compatível com ChatGPT/OpenAI MCP
|
|
1956
1327
|
|
|
@@ -2027,44 +1398,35 @@ async def search_sienge_data(
|
|
|
2027
1398
|
|
|
2028
1399
|
|
|
2029
1400
|
async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
|
|
2030
|
-
"""
|
|
2031
|
-
Função auxiliar para buscar em uma entidade específica
|
|
2032
|
-
CORRIGIDO: Usa serviços internos, nunca outras tools
|
|
2033
|
-
"""
|
|
1401
|
+
"""Função auxiliar para buscar em uma entidade específica"""
|
|
2034
1402
|
|
|
2035
1403
|
if entity_type == "customers":
|
|
2036
|
-
result = await
|
|
1404
|
+
result = await get_sienge_customers(limit=limit, search=query)
|
|
2037
1405
|
if result["success"]:
|
|
2038
|
-
data = result["data"]
|
|
2039
|
-
customers = data.get("results", []) if isinstance(data, dict) else data
|
|
2040
1406
|
return {
|
|
2041
1407
|
"success": True,
|
|
2042
|
-
"data": customers,
|
|
2043
|
-
"count":
|
|
1408
|
+
"data": result["customers"],
|
|
1409
|
+
"count": result["count"],
|
|
2044
1410
|
"entity_type": "customers"
|
|
2045
1411
|
}
|
|
2046
1412
|
|
|
2047
1413
|
elif entity_type == "creditors":
|
|
2048
|
-
result = await
|
|
1414
|
+
result = await get_sienge_creditors(limit=limit, search=query)
|
|
2049
1415
|
if result["success"]:
|
|
2050
|
-
data = result["data"]
|
|
2051
|
-
creditors = data.get("results", []) if isinstance(data, dict) else data
|
|
2052
1416
|
return {
|
|
2053
1417
|
"success": True,
|
|
2054
|
-
"data": creditors,
|
|
2055
|
-
"count":
|
|
1418
|
+
"data": result["creditors"],
|
|
1419
|
+
"count": result["count"],
|
|
2056
1420
|
"entity_type": "creditors"
|
|
2057
1421
|
}
|
|
2058
1422
|
|
|
2059
1423
|
elif entity_type == "projects" or entity_type == "enterprises":
|
|
2060
1424
|
# Para projetos, usar filtros mais específicos se disponível
|
|
2061
1425
|
company_id = filters.get("company_id")
|
|
2062
|
-
result = await
|
|
1426
|
+
result = await get_sienge_projects(limit=limit, company_id=company_id)
|
|
2063
1427
|
if result["success"]:
|
|
2064
|
-
data = result["data"]
|
|
2065
|
-
projects = data.get("results", []) if isinstance(data, dict) else data
|
|
2066
|
-
|
|
2067
1428
|
# Filtrar por query se fornecida
|
|
1429
|
+
projects = result["enterprises"]
|
|
2068
1430
|
if query:
|
|
2069
1431
|
projects = [
|
|
2070
1432
|
p for p in projects
|
|
@@ -2083,27 +1445,23 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
|
|
|
2083
1445
|
# Para títulos, usar data padrão se não especificada
|
|
2084
1446
|
start_date = filters.get("start_date")
|
|
2085
1447
|
end_date = filters.get("end_date")
|
|
2086
|
-
result = await
|
|
1448
|
+
result = await get_sienge_bills(
|
|
2087
1449
|
start_date=start_date,
|
|
2088
1450
|
end_date=end_date,
|
|
2089
1451
|
limit=limit
|
|
2090
1452
|
)
|
|
2091
1453
|
if result["success"]:
|
|
2092
|
-
data = result["data"]
|
|
2093
|
-
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
2094
1454
|
return {
|
|
2095
1455
|
"success": True,
|
|
2096
|
-
"data": bills,
|
|
2097
|
-
"count":
|
|
1456
|
+
"data": result["bills"],
|
|
1457
|
+
"count": result["count"],
|
|
2098
1458
|
"entity_type": "bills"
|
|
2099
1459
|
}
|
|
2100
1460
|
|
|
2101
1461
|
elif entity_type == "purchase_orders":
|
|
2102
|
-
result = await
|
|
1462
|
+
result = await get_sienge_purchase_orders(limit=limit)
|
|
2103
1463
|
if result["success"]:
|
|
2104
|
-
|
|
2105
|
-
orders = data.get("results", []) if isinstance(data, dict) else data
|
|
2106
|
-
|
|
1464
|
+
orders = result["purchase_orders"]
|
|
2107
1465
|
# Filtrar por query se fornecida
|
|
2108
1466
|
if query:
|
|
2109
1467
|
orders = [
|
|
@@ -2127,7 +1485,7 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
|
|
|
2127
1485
|
|
|
2128
1486
|
|
|
2129
1487
|
@mcp.tool
|
|
2130
|
-
async def list_sienge_entities(
|
|
1488
|
+
async def list_sienge_entities() -> Dict:
|
|
2131
1489
|
"""
|
|
2132
1490
|
Lista todas as entidades disponíveis no Sienge MCP para busca
|
|
2133
1491
|
|
|
@@ -2146,28 +1504,28 @@ async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
|
|
|
2146
1504
|
"name": "Credores/Fornecedores",
|
|
2147
1505
|
"description": "Fornecedores e credores cadastrados",
|
|
2148
1506
|
"search_fields": ["nome", "documento"],
|
|
2149
|
-
"tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info"
|
|
1507
|
+
"tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info"]
|
|
2150
1508
|
},
|
|
2151
1509
|
{
|
|
2152
1510
|
"type": "projects",
|
|
2153
1511
|
"name": "Empreendimentos/Obras",
|
|
2154
1512
|
"description": "Projetos e obras cadastrados",
|
|
2155
1513
|
"search_fields": ["código", "descrição", "nome"],
|
|
2156
|
-
"tools": ["get_sienge_projects", "
|
|
1514
|
+
"tools": ["get_sienge_projects", "get_sienge_enterprise_by_id"]
|
|
2157
1515
|
},
|
|
2158
1516
|
{
|
|
2159
1517
|
"type": "bills",
|
|
2160
1518
|
"name": "Títulos a Pagar",
|
|
2161
1519
|
"description": "Contas a pagar e títulos financeiros",
|
|
2162
1520
|
"search_fields": ["número", "credor", "valor"],
|
|
2163
|
-
"tools": ["get_sienge_bills"
|
|
1521
|
+
"tools": ["get_sienge_bills"]
|
|
2164
1522
|
},
|
|
2165
1523
|
{
|
|
2166
1524
|
"type": "purchase_orders",
|
|
2167
1525
|
"name": "Pedidos de Compra",
|
|
2168
1526
|
"description": "Pedidos de compra e solicitações",
|
|
2169
1527
|
"search_fields": ["id", "descrição", "status"],
|
|
2170
|
-
"tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests"
|
|
1528
|
+
"tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests"]
|
|
2171
1529
|
},
|
|
2172
1530
|
{
|
|
2173
1531
|
"type": "invoices",
|
|
@@ -2188,14 +1546,7 @@ async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
|
|
|
2188
1546
|
"name": "Financeiro",
|
|
2189
1547
|
"description": "Contas a receber e movimentações financeiras",
|
|
2190
1548
|
"search_fields": ["período", "cliente", "valor"],
|
|
2191
|
-
"tools": ["get_sienge_accounts_receivable"
|
|
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"]
|
|
1549
|
+
"tools": ["get_sienge_accounts_receivable"]
|
|
2199
1550
|
}
|
|
2200
1551
|
]
|
|
2201
1552
|
|
|
@@ -2215,93 +1566,54 @@ async def list_sienge_entities(_meta: Optional[Dict] = None) -> Dict:
|
|
|
2215
1566
|
# ============ PAGINATION E NAVEGAÇÃO ============
|
|
2216
1567
|
|
|
2217
1568
|
|
|
2218
|
-
|
|
2219
|
-
async def get_sienge_data_paginated(
|
|
1569
|
+
async def _get_data_paginated_internal(
|
|
2220
1570
|
entity_type: str,
|
|
2221
1571
|
page: int = 1,
|
|
2222
1572
|
page_size: int = 20,
|
|
2223
1573
|
filters: Optional[Dict[str, Any]] = None,
|
|
2224
|
-
sort_by: Optional[str] = None
|
|
2225
|
-
|
|
2226
|
-
"""
|
|
2227
|
-
Busca dados do Sienge com paginação avançada - compatível com ChatGPT
|
|
2228
|
-
|
|
2229
|
-
Args:
|
|
2230
|
-
entity_type: Tipo de entidade (customers, creditors, projects, bills, etc.)
|
|
2231
|
-
page: Número da página (começando em 1)
|
|
2232
|
-
page_size: Registros por página (máximo 50)
|
|
2233
|
-
filters: Filtros específicos da entidade
|
|
2234
|
-
sort_by: Campo para ordenação (se suportado)
|
|
2235
|
-
"""
|
|
1574
|
+
sort_by: Optional[str] = None
|
|
1575
|
+
) -> Dict:
|
|
1576
|
+
"""Função interna para paginação (sem decorador @mcp.tool)"""
|
|
2236
1577
|
page_size = min(page_size, 50)
|
|
2237
1578
|
offset = (page - 1) * page_size
|
|
2238
1579
|
|
|
2239
1580
|
filters = filters or {}
|
|
2240
1581
|
|
|
2241
|
-
#
|
|
1582
|
+
# Mapear para os tools existentes com offset
|
|
2242
1583
|
if entity_type == "customers":
|
|
2243
1584
|
search = filters.get("search")
|
|
2244
1585
|
customer_type_id = filters.get("customer_type_id")
|
|
2245
|
-
result = await
|
|
1586
|
+
result = await get_sienge_customers(
|
|
2246
1587
|
limit=page_size,
|
|
2247
1588
|
offset=offset,
|
|
2248
1589
|
search=search,
|
|
2249
1590
|
customer_type_id=customer_type_id
|
|
2250
1591
|
)
|
|
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
|
|
2258
1592
|
|
|
2259
1593
|
elif entity_type == "creditors":
|
|
2260
1594
|
search = filters.get("search")
|
|
2261
|
-
result = await
|
|
1595
|
+
result = await get_sienge_creditors(
|
|
2262
1596
|
limit=page_size,
|
|
2263
1597
|
offset=offset,
|
|
2264
1598
|
search=search
|
|
2265
1599
|
)
|
|
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
|
|
2273
1600
|
|
|
2274
1601
|
elif entity_type == "projects":
|
|
2275
|
-
result = await
|
|
1602
|
+
result = await get_sienge_projects(
|
|
2276
1603
|
limit=page_size,
|
|
2277
1604
|
offset=offset,
|
|
2278
1605
|
company_id=filters.get("company_id"),
|
|
2279
1606
|
enterprise_type=filters.get("enterprise_type")
|
|
2280
1607
|
)
|
|
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
|
|
2289
1608
|
|
|
2290
1609
|
elif entity_type == "bills":
|
|
2291
|
-
result = await
|
|
1610
|
+
result = await get_sienge_bills(
|
|
2292
1611
|
start_date=filters.get("start_date"),
|
|
2293
1612
|
end_date=filters.get("end_date"),
|
|
2294
1613
|
creditor_id=filters.get("creditor_id"),
|
|
2295
1614
|
status=filters.get("status"),
|
|
2296
1615
|
limit=page_size
|
|
2297
1616
|
)
|
|
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
|
|
2305
1617
|
|
|
2306
1618
|
else:
|
|
2307
1619
|
return {
|
|
@@ -2336,26 +1648,42 @@ async def get_sienge_data_paginated(
|
|
|
2336
1648
|
return result
|
|
2337
1649
|
|
|
2338
1650
|
|
|
2339
|
-
@mcp.tool
|
|
2340
|
-
async def
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
_meta: Optional[Dict] = None) -> Dict:
|
|
1651
|
+
@mcp.tool
|
|
1652
|
+
async def get_sienge_data_paginated(
|
|
1653
|
+
entity_type: str,
|
|
1654
|
+
page: int = 1,
|
|
1655
|
+
page_size: int = 20,
|
|
1656
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1657
|
+
sort_by: Optional[str] = None
|
|
1658
|
+
) -> Dict:
|
|
2348
1659
|
"""
|
|
2349
|
-
Busca
|
|
1660
|
+
Busca dados do Sienge com paginação avançada - compatível com ChatGPT
|
|
2350
1661
|
|
|
2351
1662
|
Args:
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
customer_creditor_search: Buscar por nome de cliente/credor (opcional)
|
|
1663
|
+
entity_type: Tipo de entidade (customers, creditors, projects, bills, etc.)
|
|
1664
|
+
page: Número da página (começando em 1)
|
|
1665
|
+
page_size: Registros por página (máximo 50)
|
|
1666
|
+
filters: Filtros específicos da entidade
|
|
1667
|
+
sort_by: Campo para ordenação (se suportado)
|
|
2358
1668
|
"""
|
|
1669
|
+
return await _get_data_paginated_internal(
|
|
1670
|
+
entity_type=entity_type,
|
|
1671
|
+
page=page,
|
|
1672
|
+
page_size=page_size,
|
|
1673
|
+
filters=filters,
|
|
1674
|
+
sort_by=sort_by
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
async def _search_financial_data_internal(
|
|
1679
|
+
period_start: str,
|
|
1680
|
+
period_end: str,
|
|
1681
|
+
search_type: str = "both",
|
|
1682
|
+
amount_min: Optional[float] = None,
|
|
1683
|
+
amount_max: Optional[float] = None,
|
|
1684
|
+
customer_creditor_search: Optional[str] = None
|
|
1685
|
+
) -> Dict:
|
|
1686
|
+
"""Função interna para busca financeira (sem decorador @mcp.tool)"""
|
|
2359
1687
|
|
|
2360
1688
|
financial_results = {
|
|
2361
1689
|
"receivable": {"success": False, "data": [], "count": 0, "error": None},
|
|
@@ -2365,21 +1693,20 @@ async def search_sienge_financial_data(
|
|
|
2365
1693
|
# Buscar contas a receber
|
|
2366
1694
|
if search_type in ["receivable", "both"]:
|
|
2367
1695
|
try:
|
|
2368
|
-
|
|
2369
|
-
receivable_result = await _svc_get_accounts_receivable(
|
|
1696
|
+
receivable_result = await get_sienge_accounts_receivable(
|
|
2370
1697
|
start_date=period_start,
|
|
2371
1698
|
end_date=period_end,
|
|
2372
1699
|
selection_type="D" # Por vencimento
|
|
2373
1700
|
)
|
|
2374
1701
|
|
|
2375
1702
|
if receivable_result["success"]:
|
|
2376
|
-
receivable_data = receivable_result
|
|
1703
|
+
receivable_data = receivable_result["income_data"]
|
|
2377
1704
|
|
|
2378
|
-
#
|
|
1705
|
+
# Aplicar filtros de valor se especificados
|
|
2379
1706
|
if amount_min is not None or amount_max is not None:
|
|
2380
1707
|
filtered_data = []
|
|
2381
1708
|
for item in receivable_data:
|
|
2382
|
-
amount =
|
|
1709
|
+
amount = float(item.get("amount", 0) or 0)
|
|
2383
1710
|
if amount_min is not None and amount < amount_min:
|
|
2384
1711
|
continue
|
|
2385
1712
|
if amount_max is not None and amount > amount_max:
|
|
@@ -2412,22 +1739,20 @@ async def search_sienge_financial_data(
|
|
|
2412
1739
|
# Buscar contas a pagar
|
|
2413
1740
|
if search_type in ["payable", "both"]:
|
|
2414
1741
|
try:
|
|
2415
|
-
|
|
2416
|
-
payable_result = await _svc_get_bills(
|
|
1742
|
+
payable_result = await get_sienge_bills(
|
|
2417
1743
|
start_date=period_start,
|
|
2418
1744
|
end_date=period_end,
|
|
2419
1745
|
limit=100
|
|
2420
1746
|
)
|
|
2421
1747
|
|
|
2422
1748
|
if payable_result["success"]:
|
|
2423
|
-
|
|
2424
|
-
payable_data = data.get("results", []) if isinstance(data, dict) else data
|
|
1749
|
+
payable_data = payable_result["bills"]
|
|
2425
1750
|
|
|
2426
|
-
#
|
|
1751
|
+
# Aplicar filtros de valor se especificados
|
|
2427
1752
|
if amount_min is not None or amount_max is not None:
|
|
2428
1753
|
filtered_data = []
|
|
2429
1754
|
for item in payable_data:
|
|
2430
|
-
amount =
|
|
1755
|
+
amount = float(item.get("amount", 0) or 0)
|
|
2431
1756
|
if amount_min is not None and amount < amount_min:
|
|
2432
1757
|
continue
|
|
2433
1758
|
if amount_max is not None and amount > amount_max:
|
|
@@ -2494,50 +1819,39 @@ async def search_sienge_financial_data(
|
|
|
2494
1819
|
}
|
|
2495
1820
|
|
|
2496
1821
|
|
|
2497
|
-
async def _svc_test_connection() -> Dict:
|
|
2498
|
-
"""Serviço interno: testar conexão com a API do Sienge"""
|
|
2499
|
-
try:
|
|
2500
|
-
# Usar serviço interno
|
|
2501
|
-
result = await _svc_get_customer_types()
|
|
2502
|
-
|
|
2503
|
-
if result["success"]:
|
|
2504
|
-
auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
|
|
2505
|
-
return {
|
|
2506
|
-
"success": True,
|
|
2507
|
-
"message": "✅ Conexão com API do Sienge estabelecida com sucesso!",
|
|
2508
|
-
"api_status": "Online",
|
|
2509
|
-
"auth_method": auth_method,
|
|
2510
|
-
"timestamp": datetime.now().isoformat(),
|
|
2511
|
-
"request_id": result.get("request_id"),
|
|
2512
|
-
"latency_ms": result.get("latency_ms"),
|
|
2513
|
-
"cache": result.get("cache")
|
|
2514
|
-
}
|
|
2515
|
-
else:
|
|
2516
|
-
return {
|
|
2517
|
-
"success": False,
|
|
2518
|
-
"message": "❌ Falha ao conectar com API do Sienge",
|
|
2519
|
-
"error": result.get("error"),
|
|
2520
|
-
"details": result.get("message"),
|
|
2521
|
-
"timestamp": datetime.now().isoformat(),
|
|
2522
|
-
"request_id": result.get("request_id")
|
|
2523
|
-
}
|
|
2524
|
-
except Exception as e:
|
|
2525
|
-
return {
|
|
2526
|
-
"success": False,
|
|
2527
|
-
"message": "❌ Erro ao testar conexão",
|
|
2528
|
-
"error": str(e),
|
|
2529
|
-
"timestamp": datetime.now().isoformat(),
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
1822
|
@mcp.tool
|
|
2534
|
-
async def
|
|
2535
|
-
|
|
2536
|
-
|
|
1823
|
+
async def search_sienge_financial_data(
|
|
1824
|
+
period_start: str,
|
|
1825
|
+
period_end: str,
|
|
1826
|
+
search_type: str = "both",
|
|
1827
|
+
amount_min: Optional[float] = None,
|
|
1828
|
+
amount_max: Optional[float] = None,
|
|
1829
|
+
customer_creditor_search: Optional[str] = None
|
|
1830
|
+
) -> Dict:
|
|
1831
|
+
"""
|
|
1832
|
+
Busca avançada em dados financeiros do Sienge - Contas a Pagar e Receber
|
|
1833
|
+
|
|
1834
|
+
Args:
|
|
1835
|
+
period_start: Data inicial do período (YYYY-MM-DD)
|
|
1836
|
+
period_end: Data final do período (YYYY-MM-DD)
|
|
1837
|
+
search_type: Tipo de busca ("receivable", "payable", "both")
|
|
1838
|
+
amount_min: Valor mínimo (opcional)
|
|
1839
|
+
amount_max: Valor máximo (opcional)
|
|
1840
|
+
customer_creditor_search: Buscar por nome de cliente/credor (opcional)
|
|
1841
|
+
"""
|
|
1842
|
+
return await _search_financial_data_internal(
|
|
1843
|
+
period_start=period_start,
|
|
1844
|
+
period_end=period_end,
|
|
1845
|
+
search_type=search_type,
|
|
1846
|
+
amount_min=amount_min,
|
|
1847
|
+
amount_max=amount_max,
|
|
1848
|
+
customer_creditor_search=customer_creditor_search
|
|
1849
|
+
)
|
|
2537
1850
|
|
|
2538
1851
|
|
|
2539
|
-
async def
|
|
2540
|
-
"""
|
|
1852
|
+
async def _get_dashboard_summary_internal() -> Dict:
|
|
1853
|
+
"""Função interna para dashboard (sem decorador @mcp.tool)"""
|
|
1854
|
+
|
|
2541
1855
|
# Data atual e períodos
|
|
2542
1856
|
today = datetime.now()
|
|
2543
1857
|
current_month_start = today.replace(day=1).strftime("%Y-%m-%d")
|
|
@@ -2548,7 +1862,7 @@ async def _svc_get_dashboard_summary() -> Dict:
|
|
|
2548
1862
|
|
|
2549
1863
|
# 1. Testar conexão
|
|
2550
1864
|
try:
|
|
2551
|
-
connection_test = await
|
|
1865
|
+
connection_test = await test_sienge_connection()
|
|
2552
1866
|
dashboard_data["connection"] = connection_test
|
|
2553
1867
|
except Exception as e:
|
|
2554
1868
|
errors.append(f"Teste de conexão: {str(e)}")
|
|
@@ -2556,24 +1870,23 @@ async def _svc_get_dashboard_summary() -> Dict:
|
|
|
2556
1870
|
|
|
2557
1871
|
# 2. Contar clientes (amostra)
|
|
2558
1872
|
try:
|
|
2559
|
-
customers_result = await
|
|
2560
|
-
|
|
1873
|
+
customers_result = await get_sienge_customers(limit=1)
|
|
1874
|
+
if customers_result["success"]:
|
|
1875
|
+
dashboard_data["customers_available"] = True
|
|
1876
|
+
else:
|
|
1877
|
+
dashboard_data["customers_available"] = False
|
|
2561
1878
|
except Exception as e:
|
|
2562
1879
|
errors.append(f"Clientes: {str(e)}")
|
|
2563
1880
|
dashboard_data["customers_available"] = False
|
|
2564
1881
|
|
|
2565
1882
|
# 3. Contar projetos (amostra)
|
|
2566
1883
|
try:
|
|
2567
|
-
projects_result = await
|
|
1884
|
+
projects_result = await get_sienge_projects(limit=5)
|
|
2568
1885
|
if projects_result["success"]:
|
|
2569
|
-
data = projects_result["data"]
|
|
2570
|
-
enterprises = data.get("results", []) if isinstance(data, dict) else data
|
|
2571
|
-
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
2572
|
-
|
|
2573
1886
|
dashboard_data["projects"] = {
|
|
2574
1887
|
"available": True,
|
|
2575
|
-
"sample_count": len(enterprises),
|
|
2576
|
-
"total_count": metadata.get("count", "N/A")
|
|
1888
|
+
"sample_count": len(projects_result["enterprises"]),
|
|
1889
|
+
"total_count": projects_result.get("metadata", {}).get("count", "N/A")
|
|
2577
1890
|
}
|
|
2578
1891
|
else:
|
|
2579
1892
|
dashboard_data["projects"] = {"available": False}
|
|
@@ -2583,20 +1896,16 @@ async def _svc_get_dashboard_summary() -> Dict:
|
|
|
2583
1896
|
|
|
2584
1897
|
# 4. Títulos a pagar do mês atual
|
|
2585
1898
|
try:
|
|
2586
|
-
bills_result = await
|
|
1899
|
+
bills_result = await get_sienge_bills(
|
|
2587
1900
|
start_date=current_month_start,
|
|
2588
1901
|
end_date=current_month_end,
|
|
2589
1902
|
limit=10
|
|
2590
1903
|
)
|
|
2591
1904
|
if bills_result["success"]:
|
|
2592
|
-
data = bills_result["data"]
|
|
2593
|
-
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
2594
|
-
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
2595
|
-
|
|
2596
1905
|
dashboard_data["monthly_bills"] = {
|
|
2597
1906
|
"available": True,
|
|
2598
|
-
"count": len(bills),
|
|
2599
|
-
"total_count":
|
|
1907
|
+
"count": len(bills_result["bills"]),
|
|
1908
|
+
"total_count": bills_result.get("total_count", len(bills_result["bills"]))
|
|
2600
1909
|
}
|
|
2601
1910
|
else:
|
|
2602
1911
|
dashboard_data["monthly_bills"] = {"available": False}
|
|
@@ -2606,14 +1915,11 @@ async def _svc_get_dashboard_summary() -> Dict:
|
|
|
2606
1915
|
|
|
2607
1916
|
# 5. Tipos de clientes
|
|
2608
1917
|
try:
|
|
2609
|
-
customer_types_result = await
|
|
1918
|
+
customer_types_result = await get_sienge_customer_types()
|
|
2610
1919
|
if customer_types_result["success"]:
|
|
2611
|
-
data = customer_types_result["data"]
|
|
2612
|
-
customer_types = data.get("results", []) if isinstance(data, dict) else data
|
|
2613
|
-
|
|
2614
1920
|
dashboard_data["customer_types"] = {
|
|
2615
1921
|
"available": True,
|
|
2616
|
-
"count": len(customer_types)
|
|
1922
|
+
"count": len(customer_types_result["customer_types"])
|
|
2617
1923
|
}
|
|
2618
1924
|
else:
|
|
2619
1925
|
dashboard_data["customer_types"] = {"available": False}
|
|
@@ -2643,12 +1949,298 @@ async def _svc_get_dashboard_summary() -> Dict:
|
|
|
2643
1949
|
|
|
2644
1950
|
|
|
2645
1951
|
@mcp.tool
|
|
2646
|
-
async def get_sienge_dashboard_summary(
|
|
1952
|
+
async def get_sienge_dashboard_summary() -> Dict:
|
|
2647
1953
|
"""
|
|
2648
1954
|
Obtém um resumo tipo dashboard com informações gerais do Sienge
|
|
2649
1955
|
Útil para visão geral rápida do sistema
|
|
2650
1956
|
"""
|
|
2651
|
-
return await
|
|
1957
|
+
return await _get_dashboard_summary_internal()
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
# ============ SUPABASE QUERY TOOLS ============
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
@mcp.tool
|
|
1964
|
+
async def query_supabase_database(
|
|
1965
|
+
table_name: str,
|
|
1966
|
+
columns: Optional[str] = "*",
|
|
1967
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
1968
|
+
limit: Optional[int] = 100,
|
|
1969
|
+
order_by: Optional[str] = None,
|
|
1970
|
+
search_term: Optional[str] = None,
|
|
1971
|
+
search_columns: Optional[List[str]] = None
|
|
1972
|
+
) -> Dict:
|
|
1973
|
+
"""
|
|
1974
|
+
Executa queries no banco de dados Supabase para buscar dados das tabelas do Sienge
|
|
1975
|
+
|
|
1976
|
+
Args:
|
|
1977
|
+
table_name: Nome da tabela (customers, creditors, enterprises, purchase_invoices, stock_inventories, accounts_receivable, installment_payments, income_installments)
|
|
1978
|
+
columns: Colunas a retornar (padrão: "*")
|
|
1979
|
+
filters: Filtros WHERE como dict {"campo": "valor"}
|
|
1980
|
+
limit: Limite de registros (padrão: 100, máximo: 1000)
|
|
1981
|
+
order_by: Campo para ordenação (ex: "name", "created_at desc")
|
|
1982
|
+
search_term: Termo de busca para busca textual
|
|
1983
|
+
search_columns: Colunas onde fazer busca textual (se não especificado, usa campos de texto principais)
|
|
1984
|
+
|
|
1985
|
+
Nota: As queries são executadas no schema 'sienge_data' (fixo)
|
|
1986
|
+
"""
|
|
1987
|
+
# Validação de parâmetros
|
|
1988
|
+
if not table_name or not isinstance(table_name, str):
|
|
1989
|
+
return {
|
|
1990
|
+
"success": False,
|
|
1991
|
+
"message": "❌ Nome da tabela é obrigatório e deve ser uma string",
|
|
1992
|
+
"error": "INVALID_TABLE_NAME"
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
if limit is not None and (not isinstance(limit, int) or limit <= 0):
|
|
1996
|
+
return {
|
|
1997
|
+
"success": False,
|
|
1998
|
+
"message": "❌ Limite deve ser um número inteiro positivo",
|
|
1999
|
+
"error": "INVALID_LIMIT"
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
if limit and limit > 1000:
|
|
2003
|
+
limit = 1000 # Aplicar limite máximo
|
|
2004
|
+
|
|
2005
|
+
return await _query_supabase_internal(
|
|
2006
|
+
table_name=table_name,
|
|
2007
|
+
columns=columns,
|
|
2008
|
+
filters=filters,
|
|
2009
|
+
limit=limit,
|
|
2010
|
+
order_by=order_by,
|
|
2011
|
+
search_term=search_term,
|
|
2012
|
+
search_columns=search_columns
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
@mcp.tool
|
|
2017
|
+
async def get_supabase_table_info(table_name: Optional[str] = None) -> Dict:
|
|
2018
|
+
"""
|
|
2019
|
+
Obtém informações sobre as tabelas disponíveis no Supabase ou detalhes de uma tabela específica
|
|
2020
|
+
|
|
2021
|
+
Args:
|
|
2022
|
+
table_name: Nome da tabela para obter detalhes (opcional)
|
|
2023
|
+
|
|
2024
|
+
Nota: As tabelas estão no schema 'sienge_data' (fixo)
|
|
2025
|
+
"""
|
|
2026
|
+
if not SUPABASE_AVAILABLE:
|
|
2027
|
+
return {
|
|
2028
|
+
"success": False,
|
|
2029
|
+
"message": "❌ Cliente Supabase não disponível",
|
|
2030
|
+
"error": "SUPABASE_NOT_AVAILABLE"
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
client = _get_supabase_client()
|
|
2034
|
+
if not client:
|
|
2035
|
+
return {
|
|
2036
|
+
"success": False,
|
|
2037
|
+
"message": "❌ Cliente Supabase não configurado",
|
|
2038
|
+
"error": "SUPABASE_NOT_CONFIGURED"
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
# Informações das tabelas disponíveis
|
|
2042
|
+
tables_info = {
|
|
2043
|
+
"customers": {
|
|
2044
|
+
"name": "Clientes",
|
|
2045
|
+
"description": "Clientes cadastrados no Sienge",
|
|
2046
|
+
"columns": ["id", "name", "document", "email", "phone", "customer_type_id", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2047
|
+
"search_fields": ["name", "document", "email"],
|
|
2048
|
+
"indexes": ["document", "name (trigram)", "updated_at"]
|
|
2049
|
+
},
|
|
2050
|
+
"creditors": {
|
|
2051
|
+
"name": "Credores/Fornecedores",
|
|
2052
|
+
"description": "Fornecedores e credores cadastrados",
|
|
2053
|
+
"columns": ["id", "name", "document", "bank_info", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2054
|
+
"search_fields": ["name", "document"],
|
|
2055
|
+
"indexes": ["document", "name (trigram)", "updated_at"]
|
|
2056
|
+
},
|
|
2057
|
+
"enterprises": {
|
|
2058
|
+
"name": "Empreendimentos/Obras",
|
|
2059
|
+
"description": "Projetos e obras cadastrados",
|
|
2060
|
+
"columns": ["id", "code", "name", "description", "company_id", "type", "metadata", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2061
|
+
"search_fields": ["name", "description", "code"],
|
|
2062
|
+
"indexes": ["name (trigram)", "company_id", "updated_at"]
|
|
2063
|
+
},
|
|
2064
|
+
"purchase_invoices": {
|
|
2065
|
+
"name": "Notas Fiscais de Compra",
|
|
2066
|
+
"description": "Notas fiscais de compra",
|
|
2067
|
+
"columns": ["id", "sequential_number", "supplier_id", "company_id", "movement_date", "issue_date", "series", "notes", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2068
|
+
"search_fields": ["sequential_number", "notes"],
|
|
2069
|
+
"indexes": ["supplier_id", "sequential_number", "updated_at"]
|
|
2070
|
+
},
|
|
2071
|
+
"installment_payments": {
|
|
2072
|
+
"name": "Pagamentos de Parcelas",
|
|
2073
|
+
"description": "Pagamentos efetuados para parcelas",
|
|
2074
|
+
"columns": [
|
|
2075
|
+
"id", "installment_uid", "amount", "payment_date", "method", "raw",
|
|
2076
|
+
"updated_at", "last_synced_at", "created_at"
|
|
2077
|
+
],
|
|
2078
|
+
"search_fields": ["installment_uid"],
|
|
2079
|
+
"indexes": ["payment_date", "installment_uid", "updated_at"]
|
|
2080
|
+
},
|
|
2081
|
+
"income_installments": {
|
|
2082
|
+
"name": "Parcelas de Receita",
|
|
2083
|
+
"description": "Parcelas de contas a receber (busca apenas por valores numéricos)",
|
|
2084
|
+
"columns": [
|
|
2085
|
+
"id", "bill_id", "customer_id", "amount", "due_date", "status", "raw",
|
|
2086
|
+
"updated_at", "last_synced_at", "created_at"
|
|
2087
|
+
],
|
|
2088
|
+
"search_fields": ["bill_id (numérico)", "customer_id (numérico)", "amount (numérico)"],
|
|
2089
|
+
"indexes": ["due_date", "status", "updated_at"],
|
|
2090
|
+
"search_note": "Para buscar nesta tabela, use valores numéricos (ex: '123' para bill_id)"
|
|
2091
|
+
},
|
|
2092
|
+
"stock_inventories": {
|
|
2093
|
+
"name": "Inventário de Estoque",
|
|
2094
|
+
"description": "Inventário e movimentações de estoque",
|
|
2095
|
+
"columns": ["id", "cost_center_id", "resource_id", "inventory", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2096
|
+
"search_fields": ["cost_center_id", "resource_id"],
|
|
2097
|
+
"indexes": ["cost_center_id", "resource_id"]
|
|
2098
|
+
},
|
|
2099
|
+
"accounts_receivable": {
|
|
2100
|
+
"name": "Contas a Receber",
|
|
2101
|
+
"description": "Contas a receber e movimentações financeiras",
|
|
2102
|
+
"columns": ["id", "bill_id", "customer_id", "amount", "due_date", "payment_date", "raw", "updated_at", "last_synced_at", "created_at"],
|
|
2103
|
+
"search_fields": ["bill_id", "customer_id"],
|
|
2104
|
+
"indexes": ["customer_id", "due_date", "updated_at"]
|
|
2105
|
+
},
|
|
2106
|
+
"sync_meta": {
|
|
2107
|
+
"name": "Metadados de Sincronização",
|
|
2108
|
+
"description": "Controle de sincronização entre Sienge e Supabase",
|
|
2109
|
+
"columns": ["id", "entity_name", "last_synced_at", "last_record_count", "notes", "created_at"],
|
|
2110
|
+
"search_fields": ["entity_name"],
|
|
2111
|
+
"indexes": ["entity_name"]
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if table_name:
|
|
2116
|
+
if table_name in tables_info:
|
|
2117
|
+
return {
|
|
2118
|
+
"success": True,
|
|
2119
|
+
"message": f"✅ Informações da tabela '{table_name}'",
|
|
2120
|
+
"table_info": tables_info[table_name],
|
|
2121
|
+
"table_name": table_name
|
|
2122
|
+
}
|
|
2123
|
+
else:
|
|
2124
|
+
return {
|
|
2125
|
+
"success": False,
|
|
2126
|
+
"message": f"❌ Tabela '{table_name}' não encontrada",
|
|
2127
|
+
"error": "TABLE_NOT_FOUND",
|
|
2128
|
+
"available_tables": list(tables_info.keys())
|
|
2129
|
+
}
|
|
2130
|
+
else:
|
|
2131
|
+
return {
|
|
2132
|
+
"success": True,
|
|
2133
|
+
"message": f"✅ {len(tables_info)} tabelas disponíveis no Supabase",
|
|
2134
|
+
"schema": SUPABASE_SCHEMA,
|
|
2135
|
+
"tables": tables_info,
|
|
2136
|
+
"usage_examples": {
|
|
2137
|
+
"query_customers": "query_supabase_database('customers', search_term='João')",
|
|
2138
|
+
"query_bills_by_date": "query_supabase_database('bills', filters={'due_date': '2024-01-01'})",
|
|
2139
|
+
"query_enterprises": "query_supabase_database('enterprises', columns='id,name,description', limit=50)"
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
@mcp.tool
|
|
2145
|
+
async def search_supabase_data(
|
|
2146
|
+
search_term: str,
|
|
2147
|
+
table_names: Optional[List[str]] = None,
|
|
2148
|
+
limit_per_table: Optional[int] = 20
|
|
2149
|
+
) -> Dict:
|
|
2150
|
+
"""
|
|
2151
|
+
Busca universal em múltiplas tabelas do Supabase
|
|
2152
|
+
|
|
2153
|
+
Args:
|
|
2154
|
+
search_term: Termo de busca
|
|
2155
|
+
table_names: Lista de tabelas para buscar (se não especificado, busca em todas)
|
|
2156
|
+
limit_per_table: Limite de resultados por tabela (padrão: 20)
|
|
2157
|
+
"""
|
|
2158
|
+
# Validação de parâmetros
|
|
2159
|
+
if not search_term or not isinstance(search_term, str):
|
|
2160
|
+
return {
|
|
2161
|
+
"success": False,
|
|
2162
|
+
"message": "❌ Termo de busca é obrigatório e deve ser uma string",
|
|
2163
|
+
"error": "INVALID_SEARCH_TERM"
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
if limit_per_table is not None and (not isinstance(limit_per_table, int) or limit_per_table <= 0):
|
|
2167
|
+
return {
|
|
2168
|
+
"success": False,
|
|
2169
|
+
"message": "❌ Limite por tabela deve ser um número inteiro positivo",
|
|
2170
|
+
"error": "INVALID_LIMIT"
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
# Validar e processar table_names
|
|
2174
|
+
if table_names is not None:
|
|
2175
|
+
if not isinstance(table_names, list):
|
|
2176
|
+
return {
|
|
2177
|
+
"success": False,
|
|
2178
|
+
"message": "❌ table_names deve ser uma lista de strings",
|
|
2179
|
+
"error": "INVALID_TABLE_NAMES"
|
|
2180
|
+
}
|
|
2181
|
+
# Filtrar apenas tabelas válidas
|
|
2182
|
+
valid_tables = ["customers", "creditors", "enterprises", "purchase_invoices",
|
|
2183
|
+
"stock_inventories", "accounts_receivable", "sync_meta",
|
|
2184
|
+
"installment_payments", "income_installments"]
|
|
2185
|
+
table_names = [t for t in table_names if t in valid_tables]
|
|
2186
|
+
if not table_names:
|
|
2187
|
+
return {
|
|
2188
|
+
"success": False,
|
|
2189
|
+
"message": "❌ Nenhuma tabela válida especificada",
|
|
2190
|
+
"error": "NO_VALID_TABLES",
|
|
2191
|
+
"valid_tables": valid_tables
|
|
2192
|
+
}
|
|
2193
|
+
else:
|
|
2194
|
+
table_names = ["customers", "creditors", "enterprises", "installment_payments", "income_installments"]
|
|
2195
|
+
|
|
2196
|
+
results = {}
|
|
2197
|
+
total_found = 0
|
|
2198
|
+
|
|
2199
|
+
for table_name in table_names:
|
|
2200
|
+
try:
|
|
2201
|
+
# Chamar a função interna diretamente
|
|
2202
|
+
result = await _query_supabase_internal(
|
|
2203
|
+
table_name=table_name,
|
|
2204
|
+
search_term=search_term,
|
|
2205
|
+
limit=limit_per_table or 20
|
|
2206
|
+
)
|
|
2207
|
+
|
|
2208
|
+
if result["success"]:
|
|
2209
|
+
results[table_name] = {
|
|
2210
|
+
"count": result["count"],
|
|
2211
|
+
"data": result["data"][:5] if result["count"] > 5 else result["data"], # Limitar preview
|
|
2212
|
+
"has_more": result["count"] > 5
|
|
2213
|
+
}
|
|
2214
|
+
total_found += result["count"]
|
|
2215
|
+
else:
|
|
2216
|
+
results[table_name] = {
|
|
2217
|
+
"error": result.get("error"),
|
|
2218
|
+
"count": 0
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
except Exception as e:
|
|
2222
|
+
results[table_name] = {
|
|
2223
|
+
"error": str(e),
|
|
2224
|
+
"count": 0
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
if total_found > 0:
|
|
2228
|
+
return {
|
|
2229
|
+
"success": True,
|
|
2230
|
+
"message": f"✅ Busca '{search_term}' encontrou {total_found} registros em {len([t for t in results.values() if t.get('count', 0) > 0])} tabelas",
|
|
2231
|
+
"search_term": search_term,
|
|
2232
|
+
"total_found": total_found,
|
|
2233
|
+
"results_by_table": results,
|
|
2234
|
+
"suggestion": "Use query_supabase_database() para buscar especificamente em uma tabela e obter mais resultados"
|
|
2235
|
+
}
|
|
2236
|
+
else:
|
|
2237
|
+
return {
|
|
2238
|
+
"success": False,
|
|
2239
|
+
"message": f"❌ Nenhum resultado encontrado para '{search_term}'",
|
|
2240
|
+
"search_term": search_term,
|
|
2241
|
+
"searched_tables": table_names,
|
|
2242
|
+
"results_by_table": results
|
|
2243
|
+
}
|
|
2652
2244
|
|
|
2653
2245
|
|
|
2654
2246
|
# ============ UTILITÁRIOS ============
|
|
@@ -2660,21 +2252,6 @@ def add(a: int, b: int) -> int:
|
|
|
2660
2252
|
return a + b
|
|
2661
2253
|
|
|
2662
2254
|
|
|
2663
|
-
def _mask(s: str) -> str:
|
|
2664
|
-
"""Mascara dados sensíveis mantendo apenas o início e fim"""
|
|
2665
|
-
if not s:
|
|
2666
|
-
return None
|
|
2667
|
-
if len(s) == 1:
|
|
2668
|
-
return s + "*"
|
|
2669
|
-
if len(s) == 2:
|
|
2670
|
-
return s
|
|
2671
|
-
if len(s) <= 4:
|
|
2672
|
-
return s[:2] + "*" * (len(s) - 2)
|
|
2673
|
-
# Para strings > 4: usar no máximo 4 asteriscos no meio
|
|
2674
|
-
middle_asterisks = min(4, len(s) - 4)
|
|
2675
|
-
return s[:2] + "*" * middle_asterisks + s[-2:]
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
2255
|
def _get_auth_info_internal() -> Dict:
|
|
2679
2256
|
"""Função interna para verificar configuração de autenticação"""
|
|
2680
2257
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
@@ -2685,7 +2262,7 @@ def _get_auth_info_internal() -> Dict:
|
|
|
2685
2262
|
"configured": True,
|
|
2686
2263
|
"base_url": SIENGE_BASE_URL,
|
|
2687
2264
|
"subdomain": SIENGE_SUBDOMAIN,
|
|
2688
|
-
"username":
|
|
2265
|
+
"username": SIENGE_USERNAME,
|
|
2689
2266
|
}
|
|
2690
2267
|
else:
|
|
2691
2268
|
return {
|
|
@@ -2695,6 +2272,262 @@ def _get_auth_info_internal() -> Dict:
|
|
|
2695
2272
|
}
|
|
2696
2273
|
|
|
2697
2274
|
|
|
2275
|
+
def _get_supabase_client() -> Optional[Client]:
|
|
2276
|
+
"""Função interna para obter cliente do Supabase"""
|
|
2277
|
+
if not SUPABASE_AVAILABLE:
|
|
2278
|
+
return None
|
|
2279
|
+
if not SUPABASE_URL or not SUPABASE_SERVICE_ROLE_KEY:
|
|
2280
|
+
return None
|
|
2281
|
+
try:
|
|
2282
|
+
client = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
|
|
2283
|
+
return client
|
|
2284
|
+
except Exception as e:
|
|
2285
|
+
logger.warning(f"Erro ao criar cliente Supabase: {e}")
|
|
2286
|
+
return None
|
|
2287
|
+
|
|
2288
|
+
|
|
2289
|
+
async def _query_supabase_internal(
|
|
2290
|
+
table_name: str,
|
|
2291
|
+
columns: Optional[str] = "*",
|
|
2292
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
2293
|
+
limit: Optional[int] = 100,
|
|
2294
|
+
order_by: Optional[str] = None,
|
|
2295
|
+
search_term: Optional[str] = None,
|
|
2296
|
+
search_columns: Optional[List[str]] = None
|
|
2297
|
+
) -> Dict:
|
|
2298
|
+
"""Função interna para query no Supabase (sem decorador @mcp.tool)"""
|
|
2299
|
+
|
|
2300
|
+
if not SUPABASE_AVAILABLE:
|
|
2301
|
+
return {
|
|
2302
|
+
"success": False,
|
|
2303
|
+
"message": "❌ Cliente Supabase não disponível. Instale: pip install supabase",
|
|
2304
|
+
"error": "SUPABASE_NOT_AVAILABLE"
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
client = _get_supabase_client()
|
|
2308
|
+
if not client:
|
|
2309
|
+
return {
|
|
2310
|
+
"success": False,
|
|
2311
|
+
"message": "❌ Cliente Supabase não configurado. Configure SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY",
|
|
2312
|
+
"error": "SUPABASE_NOT_CONFIGURED"
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
# Validar tabela
|
|
2316
|
+
valid_tables = [
|
|
2317
|
+
"customers", "creditors", "enterprises", "purchase_invoices",
|
|
2318
|
+
"stock_inventories", "accounts_receivable", "sync_meta",
|
|
2319
|
+
"installment_payments", "income_installments"
|
|
2320
|
+
]
|
|
2321
|
+
|
|
2322
|
+
if table_name not in valid_tables:
|
|
2323
|
+
return {
|
|
2324
|
+
"success": False,
|
|
2325
|
+
"message": f"❌ Tabela '{table_name}' não é válida",
|
|
2326
|
+
"error": "INVALID_TABLE",
|
|
2327
|
+
"valid_tables": valid_tables
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
try:
|
|
2331
|
+
# Construir query sempre usando schema sienge_data
|
|
2332
|
+
schema_client = client.schema(SUPABASE_SCHEMA)
|
|
2333
|
+
query = schema_client.table(table_name).select(columns)
|
|
2334
|
+
|
|
2335
|
+
# Aplicar filtros
|
|
2336
|
+
if filters:
|
|
2337
|
+
for field, value in filters.items():
|
|
2338
|
+
if isinstance(value, str) and "%" in value:
|
|
2339
|
+
# Busca com LIKE
|
|
2340
|
+
query = query.like(field, value)
|
|
2341
|
+
elif isinstance(value, list):
|
|
2342
|
+
# Busca com IN
|
|
2343
|
+
query = query.in_(field, value)
|
|
2344
|
+
else:
|
|
2345
|
+
# Busca exata
|
|
2346
|
+
query = query.eq(field, value)
|
|
2347
|
+
|
|
2348
|
+
# Aplicar busca textual se especificada
|
|
2349
|
+
if search_term and search_columns:
|
|
2350
|
+
# Para busca textual, usar OR entre as colunas
|
|
2351
|
+
search_conditions = []
|
|
2352
|
+
for col in search_columns:
|
|
2353
|
+
search_conditions.append(f"{col}.ilike.%{search_term}%")
|
|
2354
|
+
if search_conditions:
|
|
2355
|
+
query = query.or_(",".join(search_conditions))
|
|
2356
|
+
elif search_term:
|
|
2357
|
+
# Busca padrão baseada na tabela
|
|
2358
|
+
default_search_columns = {
|
|
2359
|
+
"customers": ["name", "document", "email"],
|
|
2360
|
+
"creditors": ["name", "document"],
|
|
2361
|
+
"enterprises": ["name", "description", "code"],
|
|
2362
|
+
"purchase_invoices": ["sequential_number", "notes"],
|
|
2363
|
+
"stock_inventories": ["cost_center_id", "resource_id"],
|
|
2364
|
+
"accounts_receivable": ["bill_id", "customer_id"],
|
|
2365
|
+
"installment_payments": ["installment_uid"],
|
|
2366
|
+
"income_installments": [] # Campos numéricos - sem busca textual
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
search_cols = default_search_columns.get(table_name, ["name"])
|
|
2370
|
+
|
|
2371
|
+
# Se não há colunas de texto para buscar, tentar busca numérica
|
|
2372
|
+
if not search_cols:
|
|
2373
|
+
# Para tabelas com campos numéricos, tentar converter search_term para número
|
|
2374
|
+
try:
|
|
2375
|
+
search_num = int(search_term)
|
|
2376
|
+
# Buscar em campos numéricos comuns
|
|
2377
|
+
numeric_conditions = []
|
|
2378
|
+
if table_name == "income_installments":
|
|
2379
|
+
numeric_conditions = [
|
|
2380
|
+
f"bill_id.eq.{search_num}",
|
|
2381
|
+
f"customer_id.eq.{search_num}",
|
|
2382
|
+
f"amount.eq.{search_num}"
|
|
2383
|
+
]
|
|
2384
|
+
elif table_name == "installment_payments":
|
|
2385
|
+
numeric_conditions = [
|
|
2386
|
+
f"installment_uid.eq.{search_num}",
|
|
2387
|
+
f"amount.eq.{search_num}"
|
|
2388
|
+
]
|
|
2389
|
+
|
|
2390
|
+
if numeric_conditions:
|
|
2391
|
+
query = query.or_(",".join(numeric_conditions))
|
|
2392
|
+
except ValueError:
|
|
2393
|
+
# Se não é número, não fazer busca
|
|
2394
|
+
pass
|
|
2395
|
+
else:
|
|
2396
|
+
# Busca textual normal
|
|
2397
|
+
search_conditions = [f"{col}.ilike.%{search_term}%" for col in search_cols]
|
|
2398
|
+
if search_conditions:
|
|
2399
|
+
query = query.or_(",".join(search_conditions))
|
|
2400
|
+
|
|
2401
|
+
# Aplicar ordenação
|
|
2402
|
+
if order_by:
|
|
2403
|
+
if " desc" in order_by.lower():
|
|
2404
|
+
field = order_by.replace(" desc", "").replace(" DESC", "")
|
|
2405
|
+
query = query.order(field, desc=True)
|
|
2406
|
+
else:
|
|
2407
|
+
field = order_by.replace(" asc", "").replace(" ASC", "")
|
|
2408
|
+
query = query.order(field)
|
|
2409
|
+
|
|
2410
|
+
# Aplicar limite
|
|
2411
|
+
limit = min(limit or 100, 1000)
|
|
2412
|
+
query = query.limit(limit)
|
|
2413
|
+
|
|
2414
|
+
# Executar query
|
|
2415
|
+
result = query.execute()
|
|
2416
|
+
|
|
2417
|
+
if hasattr(result, 'data'):
|
|
2418
|
+
data = result.data
|
|
2419
|
+
else:
|
|
2420
|
+
data = result
|
|
2421
|
+
|
|
2422
|
+
return {
|
|
2423
|
+
"success": True,
|
|
2424
|
+
"message": f"✅ Query executada com sucesso na tabela '{table_name}'",
|
|
2425
|
+
"table_name": table_name,
|
|
2426
|
+
"data": data,
|
|
2427
|
+
"count": len(data) if isinstance(data, list) else 1,
|
|
2428
|
+
"query_info": {
|
|
2429
|
+
"columns": columns,
|
|
2430
|
+
"filters": filters,
|
|
2431
|
+
"limit": limit,
|
|
2432
|
+
"order_by": order_by,
|
|
2433
|
+
"search_term": search_term,
|
|
2434
|
+
"search_columns": search_columns
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
except Exception as e:
|
|
2439
|
+
logger.error(f"Erro na query Supabase: {e}")
|
|
2440
|
+
return {
|
|
2441
|
+
"success": False,
|
|
2442
|
+
"message": f"❌ Erro ao executar query na tabela '{table_name}'",
|
|
2443
|
+
"error": str(e),
|
|
2444
|
+
"table_name": table_name
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
|
|
2448
|
+
# ============ SIMPLE ASYNC CACHE (in-memory, process-local) ============
|
|
2449
|
+
# Lightweight helper to improve hit-rate on repeated test queries
|
|
2450
|
+
_SIMPLE_CACHE: Dict[str, Dict[str, Any]] = {}
|
|
2451
|
+
|
|
2452
|
+
def _simple_cache_set(key: str, value: Dict[str, Any], ttl: int = 60) -> None:
|
|
2453
|
+
expire_at = int(time.time()) + int(ttl)
|
|
2454
|
+
_SIMPLE_CACHE[key] = {"value": value, "expire_at": expire_at}
|
|
2455
|
+
|
|
2456
|
+
def _simple_cache_get(key: str) -> Optional[Dict[str, Any]]:
|
|
2457
|
+
item = _SIMPLE_CACHE.get(key)
|
|
2458
|
+
if not item:
|
|
2459
|
+
return None
|
|
2460
|
+
if int(time.time()) > item.get("expire_at", 0):
|
|
2461
|
+
try:
|
|
2462
|
+
del _SIMPLE_CACHE[key]
|
|
2463
|
+
except KeyError:
|
|
2464
|
+
pass
|
|
2465
|
+
return None
|
|
2466
|
+
return item.get("value")
|
|
2467
|
+
|
|
2468
|
+
|
|
2469
|
+
async def _fetch_all_paginated(
|
|
2470
|
+
endpoint: str,
|
|
2471
|
+
params: Optional[Dict[str, Any]] = None,
|
|
2472
|
+
page_size: int = 200,
|
|
2473
|
+
max_records: Optional[int] = None,
|
|
2474
|
+
results_key: str = "results",
|
|
2475
|
+
use_bulk: bool = False,
|
|
2476
|
+
) -> List[Dict[str, Any]]:
|
|
2477
|
+
"""
|
|
2478
|
+
Helper to fetch all pages from a paginated endpoint that uses limit/offset.
|
|
2479
|
+
|
|
2480
|
+
- endpoint: API endpoint path (starting with /)
|
|
2481
|
+
- params: base params (function will add/override limit and offset)
|
|
2482
|
+
- page_size: maximum per request (API typically allows up to 200)
|
|
2483
|
+
- max_records: optional soft limit to stop early
|
|
2484
|
+
- results_key: key in the JSON response where the array is located (default: 'results')
|
|
2485
|
+
- use_bulk: if True expect bulk-data style response where items may be under 'data'
|
|
2486
|
+
"""
|
|
2487
|
+
params = dict(params or {})
|
|
2488
|
+
all_items: List[Dict[str, Any]] = []
|
|
2489
|
+
offset = int(params.get("offset", 0) or 0)
|
|
2490
|
+
page_size = min(int(page_size), 200)
|
|
2491
|
+
|
|
2492
|
+
while True:
|
|
2493
|
+
params["limit"] = page_size
|
|
2494
|
+
params["offset"] = offset
|
|
2495
|
+
|
|
2496
|
+
# choose the correct requester
|
|
2497
|
+
requester = make_sienge_bulk_request if use_bulk else make_sienge_request
|
|
2498
|
+
result = await requester("GET", endpoint, params=params)
|
|
2499
|
+
|
|
2500
|
+
if not result.get("success"):
|
|
2501
|
+
# stop and raise or return whatever accumulated
|
|
2502
|
+
return {"success": False, "error": result.get("error"), "message": result.get("message")}
|
|
2503
|
+
|
|
2504
|
+
data = result.get("data")
|
|
2505
|
+
|
|
2506
|
+
if use_bulk:
|
|
2507
|
+
items = data.get("data", []) if isinstance(data, dict) else data
|
|
2508
|
+
else:
|
|
2509
|
+
items = data.get(results_key, []) if isinstance(data, dict) else data
|
|
2510
|
+
|
|
2511
|
+
if not isinstance(items, list):
|
|
2512
|
+
# if API returned single object or unexpected structure, append and stop
|
|
2513
|
+
all_items.append(items)
|
|
2514
|
+
break
|
|
2515
|
+
|
|
2516
|
+
all_items.extend(items)
|
|
2517
|
+
|
|
2518
|
+
# enforce max_records if provided
|
|
2519
|
+
if max_records and len(all_items) >= int(max_records):
|
|
2520
|
+
return all_items[: int(max_records)]
|
|
2521
|
+
|
|
2522
|
+
# if fewer items returned than page_size, we've reached the end
|
|
2523
|
+
if len(items) < page_size:
|
|
2524
|
+
break
|
|
2525
|
+
|
|
2526
|
+
offset += len(items) if len(items) > 0 else page_size
|
|
2527
|
+
|
|
2528
|
+
return all_items
|
|
2529
|
+
|
|
2530
|
+
|
|
2698
2531
|
@mcp.tool
|
|
2699
2532
|
def get_auth_info() -> Dict:
|
|
2700
2533
|
"""Retorna informações sobre a configuração de autenticação"""
|
|
@@ -2733,4 +2566,4 @@ def main():
|
|
|
2733
2566
|
|
|
2734
2567
|
|
|
2735
2568
|
if __name__ == "__main__":
|
|
2736
|
-
main()
|
|
2569
|
+
main()
|