sienge-ecbiesek-mcp 1.1.5__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sienge-ecbiesek-mcp might be problematic. Click here for more details.
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/METADATA +2 -2
- sienge_ecbiesek_mcp-1.2.0.dist-info/RECORD +11 -0
- sienge_mcp/server.py +1067 -186
- sienge_mcp/server2.py +2679 -0
- sienge_ecbiesek_mcp-1.1.5.dist-info/RECORD +0 -10
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/WHEEL +0 -0
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/entry_points.txt +0 -0
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {sienge_ecbiesek_mcp-1.1.5.dist-info → sienge_ecbiesek_mcp-1.2.0.dist-info}/top_level.txt +0 -0
sienge_mcp/server.py
CHANGED
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
"""
|
|
3
3
|
SIENGE MCP COMPLETO - FastMCP com Autenticação Flexível
|
|
4
4
|
Suporta Bearer Token e Basic Auth
|
|
5
|
+
CORREÇÕES IMPLEMENTADAS:
|
|
6
|
+
1. Separação entre camada MCP e camada de serviços
|
|
7
|
+
2. Alias compatíveis com checklist
|
|
8
|
+
3. Normalização de parâmetros (camelCase + arrays)
|
|
9
|
+
4. Bulk-data assíncrono com polling
|
|
10
|
+
5. Observabilidade mínima (X-Request-ID, cache, logs)
|
|
11
|
+
6. Ajustes de compatibilidade pontuais
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
14
|
from fastmcp import FastMCP
|
|
8
15
|
import httpx
|
|
9
|
-
from typing import Dict, List, Optional, Any
|
|
16
|
+
from typing import Dict, List, Optional, Any, Union
|
|
10
17
|
import os
|
|
11
18
|
from dotenv import load_dotenv
|
|
12
|
-
from datetime import datetime
|
|
19
|
+
from datetime import datetime, timedelta
|
|
20
|
+
import uuid
|
|
21
|
+
import asyncio
|
|
22
|
+
import json
|
|
23
|
+
import time
|
|
24
|
+
import logging
|
|
25
|
+
from functools import wraps
|
|
13
26
|
|
|
14
27
|
# Carrega as variáveis de ambiente
|
|
15
28
|
load_dotenv()
|
|
@@ -24,61 +37,273 @@ SIENGE_PASSWORD = os.getenv("SIENGE_PASSWORD", "")
|
|
|
24
37
|
SIENGE_API_KEY = os.getenv("SIENGE_API_KEY", "")
|
|
25
38
|
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30"))
|
|
26
39
|
|
|
40
|
+
# Cache simples em memória
|
|
41
|
+
_cache = {}
|
|
42
|
+
CACHE_TTL = 300 # 5 minutos
|
|
43
|
+
|
|
44
|
+
# Configurar logging estruturado
|
|
45
|
+
logging.basicConfig(
|
|
46
|
+
level=logging.INFO,
|
|
47
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
48
|
+
)
|
|
49
|
+
logger = logging.getLogger("sienge-mcp")
|
|
50
|
+
|
|
27
51
|
|
|
28
52
|
class SiengeAPIError(Exception):
|
|
29
53
|
"""Exceção customizada para erros da API do Sienge"""
|
|
30
|
-
|
|
31
54
|
pass
|
|
32
55
|
|
|
33
56
|
|
|
57
|
+
# ============ HELPERS DE NORMALIZAÇÃO E OBSERVABILIDADE ============
|
|
58
|
+
|
|
59
|
+
def _camel(s: str) -> str:
|
|
60
|
+
"""Converte snake_case para camelCase"""
|
|
61
|
+
if '_' not in s:
|
|
62
|
+
return s
|
|
63
|
+
parts = s.split('_')
|
|
64
|
+
return parts[0] + ''.join(x.capitalize() for x in parts[1:])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def to_query(params: dict) -> dict:
|
|
68
|
+
"""
|
|
69
|
+
Converte parâmetros para query string normalizada:
|
|
70
|
+
- snake_case → camelCase
|
|
71
|
+
- listas/tuplas → CSV string
|
|
72
|
+
- booleanos → 'true'/'false' (minúsculo)
|
|
73
|
+
- remove valores None
|
|
74
|
+
"""
|
|
75
|
+
if not params:
|
|
76
|
+
return {}
|
|
77
|
+
|
|
78
|
+
out = {}
|
|
79
|
+
for k, v in params.items():
|
|
80
|
+
if v is None:
|
|
81
|
+
continue
|
|
82
|
+
key = _camel(k)
|
|
83
|
+
if isinstance(v, (list, tuple)):
|
|
84
|
+
out[key] = ','.join(map(str, v))
|
|
85
|
+
elif isinstance(v, bool):
|
|
86
|
+
out[key] = 'true' if v else 'false'
|
|
87
|
+
else:
|
|
88
|
+
out[key] = v
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def to_camel_json(obj: Any) -> Any:
|
|
93
|
+
"""
|
|
94
|
+
Normaliza payload JSON recursivamente:
|
|
95
|
+
- snake_case → camelCase nas chaves
|
|
96
|
+
- remove valores None
|
|
97
|
+
- mantém estrutura de listas e objetos aninhados
|
|
98
|
+
"""
|
|
99
|
+
if isinstance(obj, dict):
|
|
100
|
+
return {_camel(k): to_camel_json(v) for k, v in obj.items() if v is not None}
|
|
101
|
+
elif isinstance(obj, list):
|
|
102
|
+
return [to_camel_json(x) for x in obj]
|
|
103
|
+
else:
|
|
104
|
+
return obj
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _normalize_url(base_url: str, subdomain: str) -> str:
|
|
108
|
+
"""Normaliza URL evitando //public/api quando subdomain está vazio"""
|
|
109
|
+
if not subdomain or subdomain.strip() == "":
|
|
110
|
+
return f"{base_url.rstrip('/')}/public/api"
|
|
111
|
+
return f"{base_url.rstrip('/')}/{subdomain.strip()}/public/api"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _parse_numeric_value(value: Any) -> float:
|
|
115
|
+
"""Sanitiza valores numéricos, lidando com vírgulas decimais"""
|
|
116
|
+
if value is None:
|
|
117
|
+
return 0.0
|
|
118
|
+
|
|
119
|
+
if isinstance(value, (int, float)):
|
|
120
|
+
return float(value)
|
|
121
|
+
|
|
122
|
+
# Se for string, tentar converter
|
|
123
|
+
str_value = str(value).strip()
|
|
124
|
+
if not str_value:
|
|
125
|
+
return 0.0
|
|
126
|
+
|
|
127
|
+
# Trocar vírgula por ponto decimal
|
|
128
|
+
str_value = str_value.replace(',', '.')
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
return float(str_value)
|
|
132
|
+
except (ValueError, TypeError):
|
|
133
|
+
logger.warning(f"Não foi possível converter '{value}' para número")
|
|
134
|
+
return 0.0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _get_cache_key(endpoint: str, params: dict = None) -> str:
|
|
138
|
+
"""Gera chave de cache baseada no endpoint e parâmetros"""
|
|
139
|
+
cache_params = json.dumps(params or {}, sort_keys=True)
|
|
140
|
+
return f"{endpoint}:{hash(cache_params)}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _set_cache(key: str, data: Any) -> None:
|
|
144
|
+
"""Armazena dados no cache com TTL"""
|
|
145
|
+
_cache[key] = {
|
|
146
|
+
"data": data,
|
|
147
|
+
"timestamp": time.time(),
|
|
148
|
+
"ttl": CACHE_TTL
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _get_cache(key: str) -> Optional[Dict]:
|
|
153
|
+
"""Recupera dados do cache se ainda válidos - CORRIGIDO: mantém shape original"""
|
|
154
|
+
if key not in _cache:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
cached = _cache[key]
|
|
158
|
+
if time.time() - cached["timestamp"] > cached["ttl"]:
|
|
159
|
+
del _cache[key] # Remove cache expirado
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
# cached["data"] já é o "result" completo salvo em _set_cache
|
|
163
|
+
result = dict(cached["data"]) # cópia rasa
|
|
164
|
+
result["cache"] = {
|
|
165
|
+
"hit": True,
|
|
166
|
+
"ttl_s": cached["ttl"] - (time.time() - cached["timestamp"])
|
|
167
|
+
}
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _log_request(method: str, endpoint: str, status_code: int, latency: float, request_id: str) -> None:
|
|
172
|
+
"""Log estruturado das requisições"""
|
|
173
|
+
logger.info(
|
|
174
|
+
f"HTTP {method} {endpoint} - Status: {status_code} - "
|
|
175
|
+
f"Latency: {latency:.3f}s - RequestID: {request_id}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _extract_items_and_total(resp_data: Any) -> tuple:
|
|
180
|
+
"""
|
|
181
|
+
Extrai items e total count de resposta padronizada da API Sienge
|
|
182
|
+
Retorna: (items_list, total_count)
|
|
183
|
+
"""
|
|
184
|
+
items = resp_data.get("results", []) if isinstance(resp_data, dict) else (resp_data or [])
|
|
185
|
+
meta = resp_data.get("resultSetMetadata", {}) if isinstance(resp_data, dict) else {}
|
|
186
|
+
total = meta.get("count", len(items))
|
|
187
|
+
return items, total
|
|
188
|
+
|
|
189
|
+
|
|
34
190
|
async def make_sienge_request(
|
|
35
|
-
method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None
|
|
191
|
+
method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None, use_cache: bool = True
|
|
36
192
|
) -> Dict:
|
|
37
193
|
"""
|
|
38
194
|
Função auxiliar para fazer requisições à API do Sienge (v1)
|
|
39
195
|
Suporta tanto Bearer Token quanto Basic Auth
|
|
196
|
+
MELHORADO: Observabilidade, cache, normalização de parâmetros
|
|
40
197
|
"""
|
|
198
|
+
start_time = time.time()
|
|
199
|
+
req_id = str(uuid.uuid4())
|
|
200
|
+
|
|
41
201
|
try:
|
|
202
|
+
# Normalizar parâmetros
|
|
203
|
+
normalized_params = to_query(params) if params else None
|
|
204
|
+
|
|
205
|
+
# Verificar cache para operações GET
|
|
206
|
+
cache_key = None
|
|
207
|
+
if method.upper() == "GET" and use_cache and endpoint in ["/customer-types", "/creditors", "/customers"]:
|
|
208
|
+
cache_key = _get_cache_key(endpoint, normalized_params)
|
|
209
|
+
cached_result = _get_cache(cache_key)
|
|
210
|
+
if cached_result:
|
|
211
|
+
cached_result["request_id"] = req_id
|
|
212
|
+
return cached_result
|
|
213
|
+
|
|
42
214
|
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
43
|
-
headers = {
|
|
215
|
+
headers = {
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
"Accept": "application/json",
|
|
218
|
+
"X-Request-ID": req_id
|
|
219
|
+
}
|
|
44
220
|
|
|
45
|
-
# Configurar autenticação e URL
|
|
221
|
+
# Configurar autenticação e URL (corrigindo URLs duplas)
|
|
46
222
|
auth = None
|
|
223
|
+
base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
|
|
47
224
|
|
|
48
225
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
49
|
-
# Bearer Token (Recomendado)
|
|
50
226
|
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
51
|
-
url = f"{
|
|
227
|
+
url = f"{base_normalized}/v1{endpoint}"
|
|
52
228
|
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
53
|
-
# Basic Auth usando httpx.BasicAuth
|
|
54
229
|
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
55
|
-
url = f"{
|
|
230
|
+
url = f"{base_normalized}/v1{endpoint}"
|
|
56
231
|
else:
|
|
57
232
|
return {
|
|
58
233
|
"success": False,
|
|
59
234
|
"error": "No Authentication",
|
|
60
235
|
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
236
|
+
"request_id": req_id
|
|
61
237
|
}
|
|
62
238
|
|
|
63
|
-
response = await client.request(
|
|
239
|
+
response = await client.request(
|
|
240
|
+
method=method,
|
|
241
|
+
url=url,
|
|
242
|
+
headers=headers,
|
|
243
|
+
params=normalized_params,
|
|
244
|
+
json=json_data,
|
|
245
|
+
auth=auth
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
latency = time.time() - start_time
|
|
249
|
+
_log_request(method, endpoint, response.status_code, latency, req_id)
|
|
64
250
|
|
|
65
251
|
if response.status_code in [200, 201]:
|
|
66
252
|
try:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
253
|
+
data = response.json()
|
|
254
|
+
result = {
|
|
255
|
+
"success": True,
|
|
256
|
+
"data": data,
|
|
257
|
+
"status_code": response.status_code,
|
|
258
|
+
"request_id": req_id,
|
|
259
|
+
"latency_ms": round(latency * 1000, 2)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Armazenar no cache se aplicável
|
|
263
|
+
if cache_key and method.upper() == "GET":
|
|
264
|
+
_set_cache(cache_key, result)
|
|
265
|
+
result["cache"] = {"hit": False, "ttl_s": CACHE_TTL}
|
|
266
|
+
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
except Exception:
|
|
270
|
+
return {
|
|
271
|
+
"success": True,
|
|
272
|
+
"data": {"message": "Success"},
|
|
273
|
+
"status_code": response.status_code,
|
|
274
|
+
"request_id": req_id,
|
|
275
|
+
"latency_ms": round(latency * 1000, 2)
|
|
276
|
+
}
|
|
70
277
|
else:
|
|
71
278
|
return {
|
|
72
279
|
"success": False,
|
|
73
280
|
"error": f"HTTP {response.status_code}",
|
|
74
281
|
"message": response.text,
|
|
75
282
|
"status_code": response.status_code,
|
|
283
|
+
"request_id": req_id,
|
|
284
|
+
"latency_ms": round(latency * 1000, 2)
|
|
76
285
|
}
|
|
77
286
|
|
|
78
287
|
except httpx.TimeoutException:
|
|
79
|
-
|
|
288
|
+
latency = time.time() - start_time
|
|
289
|
+
_log_request(method, endpoint, 408, latency, req_id)
|
|
290
|
+
return {
|
|
291
|
+
"success": False,
|
|
292
|
+
"error": "Timeout",
|
|
293
|
+
"message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s",
|
|
294
|
+
"request_id": req_id,
|
|
295
|
+
"latency_ms": round(latency * 1000, 2)
|
|
296
|
+
}
|
|
80
297
|
except Exception as e:
|
|
81
|
-
|
|
298
|
+
latency = time.time() - start_time
|
|
299
|
+
_log_request(method, endpoint, 500, latency, req_id)
|
|
300
|
+
return {
|
|
301
|
+
"success": False,
|
|
302
|
+
"error": str(e),
|
|
303
|
+
"message": f"Erro na requisição: {str(e)}",
|
|
304
|
+
"request_id": req_id,
|
|
305
|
+
"latency_ms": round(latency * 1000, 2)
|
|
306
|
+
}
|
|
82
307
|
|
|
83
308
|
|
|
84
309
|
async def make_sienge_bulk_request(
|
|
@@ -87,48 +312,348 @@ async def make_sienge_bulk_request(
|
|
|
87
312
|
"""
|
|
88
313
|
Função auxiliar para fazer requisições à API bulk-data do Sienge
|
|
89
314
|
Suporta tanto Bearer Token quanto Basic Auth
|
|
315
|
+
MELHORADO: Observabilidade e normalização de parâmetros
|
|
90
316
|
"""
|
|
317
|
+
start_time = time.time()
|
|
318
|
+
req_id = str(uuid.uuid4())
|
|
319
|
+
|
|
91
320
|
try:
|
|
321
|
+
# Normalizar parâmetros
|
|
322
|
+
normalized_params = to_query(params) if params else None
|
|
323
|
+
|
|
92
324
|
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
|
93
|
-
headers = {
|
|
325
|
+
headers = {
|
|
326
|
+
"Content-Type": "application/json",
|
|
327
|
+
"Accept": "application/json",
|
|
328
|
+
"X-Request-ID": req_id
|
|
329
|
+
}
|
|
94
330
|
|
|
95
|
-
# Configurar autenticação e URL para bulk-data
|
|
331
|
+
# Configurar autenticação e URL para bulk-data (corrigindo URLs duplas)
|
|
96
332
|
auth = None
|
|
333
|
+
base_normalized = _normalize_url(SIENGE_BASE_URL, SIENGE_SUBDOMAIN)
|
|
97
334
|
|
|
98
335
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
99
|
-
# Bearer Token (Recomendado)
|
|
100
336
|
headers["Authorization"] = f"Bearer {SIENGE_API_KEY}"
|
|
101
|
-
url = f"{
|
|
337
|
+
url = f"{base_normalized}/bulk-data/v1{endpoint}"
|
|
102
338
|
elif SIENGE_USERNAME and SIENGE_PASSWORD:
|
|
103
|
-
# Basic Auth usando httpx.BasicAuth
|
|
104
339
|
auth = httpx.BasicAuth(SIENGE_USERNAME, SIENGE_PASSWORD)
|
|
105
|
-
url = f"{
|
|
340
|
+
url = f"{base_normalized}/bulk-data/v1{endpoint}"
|
|
106
341
|
else:
|
|
107
342
|
return {
|
|
108
343
|
"success": False,
|
|
109
344
|
"error": "No Authentication",
|
|
110
345
|
"message": "Configure SIENGE_API_KEY ou SIENGE_USERNAME/PASSWORD no .env",
|
|
346
|
+
"request_id": req_id
|
|
111
347
|
}
|
|
112
348
|
|
|
113
|
-
response = await client.request(
|
|
349
|
+
response = await client.request(
|
|
350
|
+
method=method,
|
|
351
|
+
url=url,
|
|
352
|
+
headers=headers,
|
|
353
|
+
params=normalized_params,
|
|
354
|
+
json=json_data,
|
|
355
|
+
auth=auth
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
latency = time.time() - start_time
|
|
359
|
+
_log_request(method, f"bulk-data{endpoint}", response.status_code, latency, req_id)
|
|
114
360
|
|
|
115
|
-
if response.status_code in [200, 201]:
|
|
361
|
+
if response.status_code in [200, 201, 202]:
|
|
116
362
|
try:
|
|
117
|
-
return {
|
|
118
|
-
|
|
119
|
-
|
|
363
|
+
return {
|
|
364
|
+
"success": True,
|
|
365
|
+
"data": response.json(),
|
|
366
|
+
"status_code": response.status_code,
|
|
367
|
+
"request_id": req_id,
|
|
368
|
+
"latency_ms": round(latency * 1000, 2)
|
|
369
|
+
}
|
|
370
|
+
except Exception:
|
|
371
|
+
return {
|
|
372
|
+
"success": True,
|
|
373
|
+
"data": {"message": "Success"},
|
|
374
|
+
"status_code": response.status_code,
|
|
375
|
+
"request_id": req_id,
|
|
376
|
+
"latency_ms": round(latency * 1000, 2)
|
|
377
|
+
}
|
|
120
378
|
else:
|
|
121
379
|
return {
|
|
122
380
|
"success": False,
|
|
123
381
|
"error": f"HTTP {response.status_code}",
|
|
124
382
|
"message": response.text,
|
|
125
383
|
"status_code": response.status_code,
|
|
384
|
+
"request_id": req_id,
|
|
385
|
+
"latency_ms": round(latency * 1000, 2)
|
|
126
386
|
}
|
|
127
387
|
|
|
128
388
|
except httpx.TimeoutException:
|
|
129
|
-
|
|
389
|
+
latency = time.time() - start_time
|
|
390
|
+
_log_request(method, f"bulk-data{endpoint}", 408, latency, req_id)
|
|
391
|
+
return {
|
|
392
|
+
"success": False,
|
|
393
|
+
"error": "Timeout",
|
|
394
|
+
"message": f"A requisição excedeu o tempo limite de {REQUEST_TIMEOUT}s",
|
|
395
|
+
"request_id": req_id,
|
|
396
|
+
"latency_ms": round(latency * 1000, 2)
|
|
397
|
+
}
|
|
130
398
|
except Exception as e:
|
|
131
|
-
|
|
399
|
+
latency = time.time() - start_time
|
|
400
|
+
_log_request(method, f"bulk-data{endpoint}", 500, latency, req_id)
|
|
401
|
+
return {
|
|
402
|
+
"success": False,
|
|
403
|
+
"error": str(e),
|
|
404
|
+
"message": f"Erro na requisição bulk-data: {str(e)}",
|
|
405
|
+
"request_id": req_id,
|
|
406
|
+
"latency_ms": round(latency * 1000, 2)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
async def _fetch_bulk_with_polling(method: str, endpoint: str, params: Optional[Dict] = None, json_data: Optional[Dict] = None) -> Dict:
|
|
411
|
+
"""
|
|
412
|
+
Faz requisição bulk com polling automático para requests assíncronos (202)
|
|
413
|
+
"""
|
|
414
|
+
correlation_id = str(uuid.uuid4())
|
|
415
|
+
|
|
416
|
+
# Fazer requisição inicial
|
|
417
|
+
result = await make_sienge_bulk_request(method, endpoint, params, json_data)
|
|
418
|
+
|
|
419
|
+
# Se não foi 202 ou não tem identifier, retornar resultado direto
|
|
420
|
+
if result.get("status_code") != 202:
|
|
421
|
+
return result
|
|
422
|
+
|
|
423
|
+
data = result.get("data", {})
|
|
424
|
+
if not isinstance(data, dict) or not data.get("identifier"):
|
|
425
|
+
return result
|
|
426
|
+
|
|
427
|
+
# Processar requisição assíncrona com polling
|
|
428
|
+
identifier = data["identifier"]
|
|
429
|
+
request_id = result.get("request_id")
|
|
430
|
+
|
|
431
|
+
logger.info(f"Iniciando polling para bulk request - Identifier: {identifier} - RequestID: {request_id}")
|
|
432
|
+
|
|
433
|
+
max_attempts = 30 # Máximo 5 minutos (30 * 10s)
|
|
434
|
+
attempt = 0
|
|
435
|
+
backoff_delay = 2 # Começar com 2 segundos
|
|
436
|
+
|
|
437
|
+
while attempt < max_attempts:
|
|
438
|
+
attempt += 1
|
|
439
|
+
await asyncio.sleep(backoff_delay)
|
|
440
|
+
|
|
441
|
+
# Verificar status do processamento
|
|
442
|
+
status_result = await make_sienge_bulk_request("GET", f"/async/{identifier}")
|
|
443
|
+
|
|
444
|
+
if not status_result["success"]:
|
|
445
|
+
logger.error(f"Erro ao verificar status do bulk request {identifier}: {status_result.get('error')}")
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
status_data = status_result.get("data", {})
|
|
449
|
+
status = status_data.get("status", "unknown")
|
|
450
|
+
|
|
451
|
+
logger.info(f"Polling attempt {attempt} - Status: {status} - Identifier: {identifier}")
|
|
452
|
+
|
|
453
|
+
if status == "completed":
|
|
454
|
+
# Buscar resultados finais
|
|
455
|
+
all_chunks = []
|
|
456
|
+
chunk_count = status_data.get("chunk_count", 1)
|
|
457
|
+
|
|
458
|
+
chunks_downloaded = 0
|
|
459
|
+
for chunk_num in range(chunk_count):
|
|
460
|
+
try:
|
|
461
|
+
# CORRIGIDO: endpoint aninhado sob /async
|
|
462
|
+
chunk_result = await make_sienge_bulk_request("GET", f"/async/{identifier}/result/{chunk_num}")
|
|
463
|
+
if chunk_result["success"]:
|
|
464
|
+
chunk_data = chunk_result.get("data", {}).get("data", [])
|
|
465
|
+
if isinstance(chunk_data, list):
|
|
466
|
+
all_chunks.extend(chunk_data)
|
|
467
|
+
chunks_downloaded += 1
|
|
468
|
+
except Exception as e:
|
|
469
|
+
logger.warning(f"Erro ao buscar chunk {chunk_num}: {e}")
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
"success": True,
|
|
473
|
+
"data": all_chunks,
|
|
474
|
+
"async_identifier": identifier,
|
|
475
|
+
"correlation_id": correlation_id,
|
|
476
|
+
"chunks_downloaded": chunks_downloaded,
|
|
477
|
+
"rows_returned": len(all_chunks),
|
|
478
|
+
"polling_attempts": attempt,
|
|
479
|
+
"request_id": request_id
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
elif status == "failed" or status == "error":
|
|
483
|
+
return {
|
|
484
|
+
"success": False,
|
|
485
|
+
"error": "Bulk processing failed",
|
|
486
|
+
"message": status_data.get("error_message", "Processamento bulk falhou"),
|
|
487
|
+
"async_identifier": identifier,
|
|
488
|
+
"correlation_id": correlation_id,
|
|
489
|
+
"polling_attempts": attempt,
|
|
490
|
+
"request_id": request_id
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# Aumentar delay progressivamente (backoff exponencial limitado)
|
|
494
|
+
backoff_delay = min(backoff_delay * 1.5, 10) # Máximo 10 segundos
|
|
495
|
+
|
|
496
|
+
# Timeout do polling
|
|
497
|
+
return {
|
|
498
|
+
"success": False,
|
|
499
|
+
"error": "Polling timeout",
|
|
500
|
+
"message": f"Processamento bulk não completou em {max_attempts} tentativas",
|
|
501
|
+
"async_identifier": identifier,
|
|
502
|
+
"correlation_id": correlation_id,
|
|
503
|
+
"polling_attempts": attempt,
|
|
504
|
+
"request_id": request_id
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# ============ CAMADA DE SERVIÇOS (FUNÇÕES INTERNAS) ============
|
|
509
|
+
|
|
510
|
+
async def _svc_get_customer_types() -> Dict:
|
|
511
|
+
"""Serviço interno: buscar tipos de clientes"""
|
|
512
|
+
return await make_sienge_request("GET", "/customer-types", use_cache=True)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
async def _svc_get_customers(*, limit: int = 50, offset: int = 0, search: str = None, customer_type_id: str = None) -> Dict:
|
|
516
|
+
"""Serviço interno: buscar clientes"""
|
|
517
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
518
|
+
if search:
|
|
519
|
+
params["search"] = search
|
|
520
|
+
if customer_type_id:
|
|
521
|
+
params["customer_type_id"] = customer_type_id
|
|
522
|
+
|
|
523
|
+
return await make_sienge_request("GET", "/customers", params=params)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
async def _svc_get_creditors(*, limit: int = 50, offset: int = 0, search: str = None) -> Dict:
|
|
527
|
+
"""Serviço interno: buscar credores/fornecedores"""
|
|
528
|
+
params = {"limit": min(limit or 50, 200), "offset": offset or 0}
|
|
529
|
+
if search:
|
|
530
|
+
params["search"] = search
|
|
531
|
+
|
|
532
|
+
return await make_sienge_request("GET", "/creditors", params=params)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
async def _svc_get_creditor_bank_info(*, creditor_id: str) -> Dict:
|
|
536
|
+
"""Serviço interno: informações bancárias de credor"""
|
|
537
|
+
return await make_sienge_request("GET", f"/creditors/{creditor_id}/bank-informations")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
async def _svc_get_projects(*, limit: int = 100, offset: int = 0, company_id: int = None,
|
|
541
|
+
enterprise_type: int = None, receivable_register: str = None,
|
|
542
|
+
only_buildings_enabled: bool = False) -> Dict:
|
|
543
|
+
"""Serviço interno: buscar empreendimentos/projetos"""
|
|
544
|
+
params = {"limit": min(limit or 100, 200), "offset": offset or 0}
|
|
545
|
+
|
|
546
|
+
if company_id:
|
|
547
|
+
params["company_id"] = company_id
|
|
548
|
+
if enterprise_type:
|
|
549
|
+
params["type"] = enterprise_type
|
|
550
|
+
if receivable_register:
|
|
551
|
+
params["receivable_register"] = receivable_register
|
|
552
|
+
if only_buildings_enabled:
|
|
553
|
+
params["only_buildings_enabled_for_integration"] = only_buildings_enabled
|
|
554
|
+
|
|
555
|
+
return await make_sienge_request("GET", "/enterprises", params=params)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
async def _svc_get_bills(*, start_date: str = None, end_date: str = None, creditor_id: str = None,
|
|
559
|
+
status: str = None, limit: int = 50) -> Dict:
|
|
560
|
+
"""Serviço interno: buscar títulos a pagar"""
|
|
561
|
+
# Se start_date não fornecido, usar últimos 30 dias
|
|
562
|
+
if not start_date:
|
|
563
|
+
start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
564
|
+
|
|
565
|
+
# Se end_date não fornecido, usar hoje
|
|
566
|
+
if not end_date:
|
|
567
|
+
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
568
|
+
|
|
569
|
+
params = {"start_date": start_date, "end_date": end_date, "limit": min(limit or 50, 200)}
|
|
570
|
+
|
|
571
|
+
if creditor_id:
|
|
572
|
+
params["creditor_id"] = creditor_id
|
|
573
|
+
if status:
|
|
574
|
+
params["status"] = status
|
|
575
|
+
|
|
576
|
+
return await make_sienge_request("GET", "/bills", params=params)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
async def _svc_get_accounts_receivable(*, start_date: str, end_date: str, selection_type: str = "D",
|
|
580
|
+
company_id: int = None, cost_centers_id: List[int] = None,
|
|
581
|
+
correction_indexer_id: int = None, correction_date: str = None,
|
|
582
|
+
change_start_date: str = None, completed_bills: str = None,
|
|
583
|
+
origins_ids: List[str] = None, bearers_id_in: List[int] = None,
|
|
584
|
+
bearers_id_not_in: List[int] = None) -> Dict:
|
|
585
|
+
"""Serviço interno: buscar contas a receber via bulk-data"""
|
|
586
|
+
params = {"start_date": start_date, "end_date": end_date, "selection_type": selection_type}
|
|
587
|
+
|
|
588
|
+
if company_id:
|
|
589
|
+
params["company_id"] = company_id
|
|
590
|
+
if cost_centers_id:
|
|
591
|
+
params["cost_centers_id"] = cost_centers_id
|
|
592
|
+
if correction_indexer_id:
|
|
593
|
+
params["correction_indexer_id"] = correction_indexer_id
|
|
594
|
+
if correction_date:
|
|
595
|
+
params["correction_date"] = correction_date
|
|
596
|
+
if change_start_date:
|
|
597
|
+
params["change_start_date"] = change_start_date
|
|
598
|
+
if completed_bills:
|
|
599
|
+
params["completed_bills"] = completed_bills
|
|
600
|
+
if origins_ids:
|
|
601
|
+
params["origins_ids"] = origins_ids
|
|
602
|
+
if bearers_id_in:
|
|
603
|
+
params["bearers_id_in"] = bearers_id_in
|
|
604
|
+
if bearers_id_not_in:
|
|
605
|
+
params["bearers_id_not_in"] = bearers_id_not_in
|
|
606
|
+
|
|
607
|
+
return await _fetch_bulk_with_polling("GET", "/income", params=params)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
async def _svc_get_accounts_receivable_by_bills(*, bills_ids: List[int], correction_indexer_id: int = None,
|
|
611
|
+
correction_date: str = None) -> Dict:
|
|
612
|
+
"""Serviço interno: buscar contas a receber por títulos específicos"""
|
|
613
|
+
params = {"bills_ids": bills_ids}
|
|
614
|
+
|
|
615
|
+
if correction_indexer_id:
|
|
616
|
+
params["correction_indexer_id"] = correction_indexer_id
|
|
617
|
+
if correction_date:
|
|
618
|
+
params["correction_date"] = correction_date
|
|
619
|
+
|
|
620
|
+
return await _fetch_bulk_with_polling("GET", "/income/by-bills", params=params)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
async def _svc_get_purchase_orders(*, purchase_order_id: str = None, status: str = None,
|
|
624
|
+
date_from: str = None, limit: int = 50) -> Dict:
|
|
625
|
+
"""Serviço interno: buscar pedidos de compra"""
|
|
626
|
+
if purchase_order_id:
|
|
627
|
+
return await make_sienge_request("GET", f"/purchase-orders/{purchase_order_id}")
|
|
628
|
+
|
|
629
|
+
params = {"limit": min(limit or 50, 200)}
|
|
630
|
+
if status:
|
|
631
|
+
params["status"] = status
|
|
632
|
+
if date_from:
|
|
633
|
+
params["date_from"] = date_from
|
|
634
|
+
|
|
635
|
+
return await make_sienge_request("GET", "/purchase-orders", params=params)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
async def _svc_get_purchase_requests(*, purchase_request_id: str = None, limit: int = 50, status: str = None) -> Dict:
|
|
639
|
+
"""Serviço interno: buscar solicitações de compra"""
|
|
640
|
+
if purchase_request_id:
|
|
641
|
+
return await make_sienge_request("GET", f"/purchase-requests/{purchase_request_id}")
|
|
642
|
+
|
|
643
|
+
params = {"limit": min(limit or 50, 200)}
|
|
644
|
+
if status:
|
|
645
|
+
params["status"] = status
|
|
646
|
+
|
|
647
|
+
return await make_sienge_request("GET", "/purchase-requests", params=params)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
async def _svc_get_purchase_invoices(*, limit: int = 50, date_from: str = None) -> Dict:
|
|
651
|
+
"""Serviço interno: listar notas fiscais de compra"""
|
|
652
|
+
params = {"limit": min(limit or 50, 200)}
|
|
653
|
+
if date_from:
|
|
654
|
+
params["date_from"] = date_from
|
|
655
|
+
|
|
656
|
+
return await make_sienge_request("GET", "/purchase-invoices", params=params)
|
|
132
657
|
|
|
133
658
|
|
|
134
659
|
# ============ CONEXÃO E TESTE ============
|
|
@@ -138,8 +663,8 @@ async def make_sienge_bulk_request(
|
|
|
138
663
|
async def test_sienge_connection() -> Dict:
|
|
139
664
|
"""Testa a conexão com a API do Sienge"""
|
|
140
665
|
try:
|
|
141
|
-
#
|
|
142
|
-
result = await
|
|
666
|
+
# Usar serviço interno
|
|
667
|
+
result = await _svc_get_customer_types()
|
|
143
668
|
|
|
144
669
|
if result["success"]:
|
|
145
670
|
auth_method = "Bearer Token" if SIENGE_API_KEY else "Basic Auth"
|
|
@@ -149,6 +674,9 @@ async def test_sienge_connection() -> Dict:
|
|
|
149
674
|
"api_status": "Online",
|
|
150
675
|
"auth_method": auth_method,
|
|
151
676
|
"timestamp": datetime.now().isoformat(),
|
|
677
|
+
"request_id": result.get("request_id"),
|
|
678
|
+
"latency_ms": result.get("latency_ms"),
|
|
679
|
+
"cache": result.get("cache")
|
|
152
680
|
}
|
|
153
681
|
else:
|
|
154
682
|
return {
|
|
@@ -157,6 +685,7 @@ async def test_sienge_connection() -> Dict:
|
|
|
157
685
|
"error": result.get("error"),
|
|
158
686
|
"details": result.get("message"),
|
|
159
687
|
"timestamp": datetime.now().isoformat(),
|
|
688
|
+
"request_id": result.get("request_id")
|
|
160
689
|
}
|
|
161
690
|
except Exception as e:
|
|
162
691
|
return {
|
|
@@ -183,14 +712,13 @@ async def get_sienge_customers(
|
|
|
183
712
|
search: Buscar por nome ou documento
|
|
184
713
|
customer_type_id: Filtrar por tipo de cliente
|
|
185
714
|
"""
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
result = await make_sienge_request("GET", "/customers", params=params)
|
|
715
|
+
# Usar serviço interno
|
|
716
|
+
result = await _svc_get_customers(
|
|
717
|
+
limit=limit or 50,
|
|
718
|
+
offset=offset or 0,
|
|
719
|
+
search=search,
|
|
720
|
+
customer_type_id=customer_type_id
|
|
721
|
+
)
|
|
194
722
|
|
|
195
723
|
if result["success"]:
|
|
196
724
|
data = result["data"]
|
|
@@ -203,7 +731,13 @@ async def get_sienge_customers(
|
|
|
203
731
|
"message": f"✅ Encontrados {len(customers)} clientes (total: {total_count})",
|
|
204
732
|
"customers": customers,
|
|
205
733
|
"count": len(customers),
|
|
206
|
-
"
|
|
734
|
+
"total_count": total_count,
|
|
735
|
+
"filters_applied": {
|
|
736
|
+
"limit": limit, "offset": offset, "search": search, "customer_type_id": customer_type_id
|
|
737
|
+
},
|
|
738
|
+
"request_id": result.get("request_id"),
|
|
739
|
+
"latency_ms": result.get("latency_ms"),
|
|
740
|
+
"cache": result.get("cache")
|
|
207
741
|
}
|
|
208
742
|
|
|
209
743
|
return {
|
|
@@ -211,13 +745,15 @@ async def get_sienge_customers(
|
|
|
211
745
|
"message": "❌ Erro ao buscar clientes",
|
|
212
746
|
"error": result.get("error"),
|
|
213
747
|
"details": result.get("message"),
|
|
748
|
+
"request_id": result.get("request_id")
|
|
214
749
|
}
|
|
215
750
|
|
|
216
751
|
|
|
217
752
|
@mcp.tool
|
|
218
753
|
async def get_sienge_customer_types() -> Dict:
|
|
219
754
|
"""Lista tipos de clientes disponíveis"""
|
|
220
|
-
|
|
755
|
+
# Usar serviço interno
|
|
756
|
+
result = await _svc_get_customer_types()
|
|
221
757
|
|
|
222
758
|
if result["success"]:
|
|
223
759
|
data = result["data"]
|
|
@@ -230,6 +766,10 @@ async def get_sienge_customer_types() -> Dict:
|
|
|
230
766
|
"message": f"✅ Encontrados {len(customer_types)} tipos de clientes (total: {total_count})",
|
|
231
767
|
"customer_types": customer_types,
|
|
232
768
|
"count": len(customer_types),
|
|
769
|
+
"total_count": total_count,
|
|
770
|
+
"request_id": result.get("request_id"),
|
|
771
|
+
"latency_ms": result.get("latency_ms"),
|
|
772
|
+
"cache": result.get("cache")
|
|
233
773
|
}
|
|
234
774
|
|
|
235
775
|
return {
|
|
@@ -237,6 +777,193 @@ async def get_sienge_customer_types() -> Dict:
|
|
|
237
777
|
"message": "❌ Erro ao buscar tipos de clientes",
|
|
238
778
|
"error": result.get("error"),
|
|
239
779
|
"details": result.get("message"),
|
|
780
|
+
"request_id": result.get("request_id")
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
# ============ ALIAS COMPATÍVEIS COM CHECKLIST ============
|
|
785
|
+
|
|
786
|
+
@mcp.tool
|
|
787
|
+
async def get_sienge_enterprises(
|
|
788
|
+
limit: int = 100, offset: int = 0, company_id: int = None, enterprise_type: int = None
|
|
789
|
+
) -> Dict:
|
|
790
|
+
"""
|
|
791
|
+
ALIAS: get_sienge_projects → get_sienge_enterprises
|
|
792
|
+
Busca empreendimentos/obras (compatibilidade com checklist)
|
|
793
|
+
"""
|
|
794
|
+
return await get_sienge_projects(
|
|
795
|
+
limit=limit,
|
|
796
|
+
offset=offset,
|
|
797
|
+
company_id=company_id,
|
|
798
|
+
enterprise_type=enterprise_type
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
@mcp.tool
|
|
803
|
+
async def get_sienge_suppliers(
|
|
804
|
+
limit: int = 50,
|
|
805
|
+
offset: int = 0,
|
|
806
|
+
search: str = None
|
|
807
|
+
) -> Dict:
|
|
808
|
+
"""
|
|
809
|
+
ALIAS: get_sienge_creditors → get_sienge_suppliers
|
|
810
|
+
Busca fornecedores (compatibilidade com checklist)
|
|
811
|
+
"""
|
|
812
|
+
return await get_sienge_creditors(limit=limit, offset=offset, search=search)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@mcp.tool
|
|
816
|
+
async def search_sienge_finances(
|
|
817
|
+
period_start: str,
|
|
818
|
+
period_end: str,
|
|
819
|
+
account_type: Optional[str] = None,
|
|
820
|
+
cost_center: Optional[str] = None, # ignorado por enquanto (não suportado na API atual)
|
|
821
|
+
amount_filter: Optional[str] = None,
|
|
822
|
+
customer_creditor: Optional[str] = None
|
|
823
|
+
) -> Dict:
|
|
824
|
+
"""
|
|
825
|
+
ALIAS: search_sienge_financial_data → search_sienge_finances
|
|
826
|
+
- account_type: receivable | payable | both
|
|
827
|
+
- amount_filter: "100..500", ">=1000", "<=500", ">100", "<200", "=750"
|
|
828
|
+
- customer_creditor: termo de busca (cliente/credor)
|
|
829
|
+
"""
|
|
830
|
+
# 1) mapear tipo
|
|
831
|
+
search_type = (account_type or "both").lower()
|
|
832
|
+
if search_type not in {"receivable", "payable", "both"}:
|
|
833
|
+
search_type = "both"
|
|
834
|
+
|
|
835
|
+
# 2) parse de faixa de valores
|
|
836
|
+
amount_min = amount_max = None
|
|
837
|
+
if amount_filter:
|
|
838
|
+
s = amount_filter.replace(" ", "")
|
|
839
|
+
try:
|
|
840
|
+
if ".." in s:
|
|
841
|
+
lo, hi = s.split("..", 1)
|
|
842
|
+
amount_min = float(lo) if lo else None
|
|
843
|
+
amount_max = float(hi) if hi else None
|
|
844
|
+
elif s.startswith(">="):
|
|
845
|
+
amount_min = float(s[2:])
|
|
846
|
+
elif s.startswith("<="):
|
|
847
|
+
amount_max = float(s[2:])
|
|
848
|
+
elif s.startswith(">"):
|
|
849
|
+
# >x → min = x (estrito não suportado; aproximamos)
|
|
850
|
+
amount_min = float(s[1:])
|
|
851
|
+
elif s.startswith("<"):
|
|
852
|
+
amount_max = float(s[1:])
|
|
853
|
+
elif s.startswith("="):
|
|
854
|
+
v = float(s[1:])
|
|
855
|
+
amount_min = v
|
|
856
|
+
amount_max = v
|
|
857
|
+
else:
|
|
858
|
+
# número puro → min
|
|
859
|
+
amount_min = float(s)
|
|
860
|
+
except ValueError:
|
|
861
|
+
# filtro inválido → ignora silenciosamente
|
|
862
|
+
amount_min = amount_max = None
|
|
863
|
+
|
|
864
|
+
return await search_sienge_financial_data(
|
|
865
|
+
period_start=period_start,
|
|
866
|
+
period_end=period_end,
|
|
867
|
+
search_type=search_type,
|
|
868
|
+
amount_min=amount_min,
|
|
869
|
+
amount_max=amount_max,
|
|
870
|
+
customer_creditor_search=customer_creditor
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
@mcp.tool
|
|
875
|
+
async def get_sienge_accounts_payable(
|
|
876
|
+
start_date: str = None, end_date: str = None, creditor_id: str = None,
|
|
877
|
+
status: str = None, limit: int = 50
|
|
878
|
+
) -> Dict:
|
|
879
|
+
"""
|
|
880
|
+
ALIAS: get_sienge_bills → get_sienge_accounts_payable
|
|
881
|
+
Busca contas a pagar (compatibilidade com checklist)
|
|
882
|
+
"""
|
|
883
|
+
return await get_sienge_bills(
|
|
884
|
+
start_date=start_date,
|
|
885
|
+
end_date=end_date,
|
|
886
|
+
creditor_id=creditor_id,
|
|
887
|
+
status=status,
|
|
888
|
+
limit=limit
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@mcp.tool
|
|
893
|
+
async def list_sienge_purchase_invoices(limit: int = 50, date_from: str = None) -> Dict:
|
|
894
|
+
"""
|
|
895
|
+
Lista notas fiscais de compra (versão list/plural esperada pelo checklist)
|
|
896
|
+
|
|
897
|
+
Args:
|
|
898
|
+
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
899
|
+
date_from: Data inicial (YYYY-MM-DD)
|
|
900
|
+
"""
|
|
901
|
+
# Usar serviço interno
|
|
902
|
+
result = await _svc_get_purchase_invoices(limit=limit, date_from=date_from)
|
|
903
|
+
|
|
904
|
+
if result["success"]:
|
|
905
|
+
data = result["data"]
|
|
906
|
+
invoices = data.get("results", []) if isinstance(data, dict) else data
|
|
907
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
908
|
+
total_count = metadata.get("count", len(invoices))
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
"success": True,
|
|
912
|
+
"message": f"✅ Encontradas {len(invoices)} notas fiscais de compra (total: {total_count})",
|
|
913
|
+
"purchase_invoices": invoices,
|
|
914
|
+
"count": len(invoices),
|
|
915
|
+
"total_count": total_count,
|
|
916
|
+
"filters_applied": {"limit": limit, "date_from": date_from},
|
|
917
|
+
"request_id": result.get("request_id"),
|
|
918
|
+
"latency_ms": result.get("latency_ms"),
|
|
919
|
+
"cache": result.get("cache")
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
"success": False,
|
|
924
|
+
"message": "❌ Erro ao buscar notas fiscais de compra",
|
|
925
|
+
"error": result.get("error"),
|
|
926
|
+
"details": result.get("message"),
|
|
927
|
+
"request_id": result.get("request_id")
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
@mcp.tool
|
|
932
|
+
async def list_sienge_purchase_requests(limit: int = 50, status: str = None) -> Dict:
|
|
933
|
+
"""
|
|
934
|
+
Lista solicitações de compra (versão list/plural esperada pelo checklist)
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
938
|
+
status: Status da solicitação
|
|
939
|
+
"""
|
|
940
|
+
# Usar serviço interno
|
|
941
|
+
result = await _svc_get_purchase_requests(limit=limit, status=status)
|
|
942
|
+
|
|
943
|
+
if result["success"]:
|
|
944
|
+
data = result["data"]
|
|
945
|
+
requests = data.get("results", []) if isinstance(data, dict) else data
|
|
946
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
947
|
+
total_count = metadata.get("count", len(requests))
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
"success": True,
|
|
951
|
+
"message": f"✅ Encontradas {len(requests)} solicitações de compra (total: {total_count})",
|
|
952
|
+
"purchase_requests": requests,
|
|
953
|
+
"count": len(requests),
|
|
954
|
+
"total_count": total_count,
|
|
955
|
+
"filters_applied": {"limit": limit, "status": status},
|
|
956
|
+
"request_id": result.get("request_id"),
|
|
957
|
+
"latency_ms": result.get("latency_ms"),
|
|
958
|
+
"cache": result.get("cache")
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
"success": False,
|
|
963
|
+
"message": "❌ Erro ao buscar solicitações de compra",
|
|
964
|
+
"error": result.get("error"),
|
|
965
|
+
"details": result.get("message"),
|
|
966
|
+
"request_id": result.get("request_id")
|
|
240
967
|
}
|
|
241
968
|
|
|
242
969
|
|
|
@@ -253,11 +980,12 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
253
980
|
offset: Pular registros (padrão: 0)
|
|
254
981
|
search: Buscar por nome
|
|
255
982
|
"""
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
983
|
+
# Usar serviço interno
|
|
984
|
+
result = await _svc_get_creditors(
|
|
985
|
+
limit=limit or 50,
|
|
986
|
+
offset=offset or 0,
|
|
987
|
+
search=search
|
|
988
|
+
)
|
|
261
989
|
|
|
262
990
|
if result["success"]:
|
|
263
991
|
data = result["data"]
|
|
@@ -270,6 +998,11 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
270
998
|
"message": f"✅ Encontrados {len(creditors)} credores (total: {total_count})",
|
|
271
999
|
"creditors": creditors,
|
|
272
1000
|
"count": len(creditors),
|
|
1001
|
+
"total_count": total_count,
|
|
1002
|
+
"filters_applied": {"limit": limit, "offset": offset, "search": search},
|
|
1003
|
+
"request_id": result.get("request_id"),
|
|
1004
|
+
"latency_ms": result.get("latency_ms"),
|
|
1005
|
+
"cache": result.get("cache")
|
|
273
1006
|
}
|
|
274
1007
|
|
|
275
1008
|
return {
|
|
@@ -277,6 +1010,7 @@ async def get_sienge_creditors(limit: Optional[int] = 50, offset: Optional[int]
|
|
|
277
1010
|
"message": "❌ Erro ao buscar credores",
|
|
278
1011
|
"error": result.get("error"),
|
|
279
1012
|
"details": result.get("message"),
|
|
1013
|
+
"request_id": result.get("request_id")
|
|
280
1014
|
}
|
|
281
1015
|
|
|
282
1016
|
|
|
@@ -288,7 +1022,8 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
|
|
|
288
1022
|
Args:
|
|
289
1023
|
creditor_id: ID do credor (obrigatório)
|
|
290
1024
|
"""
|
|
291
|
-
|
|
1025
|
+
# Usar serviço interno
|
|
1026
|
+
result = await _svc_get_creditor_bank_info(creditor_id=creditor_id)
|
|
292
1027
|
|
|
293
1028
|
if result["success"]:
|
|
294
1029
|
return {
|
|
@@ -296,6 +1031,8 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
|
|
|
296
1031
|
"message": f"✅ Informações bancárias do credor {creditor_id}",
|
|
297
1032
|
"creditor_id": creditor_id,
|
|
298
1033
|
"bank_info": result["data"],
|
|
1034
|
+
"request_id": result.get("request_id"),
|
|
1035
|
+
"latency_ms": result.get("latency_ms")
|
|
299
1036
|
}
|
|
300
1037
|
|
|
301
1038
|
return {
|
|
@@ -303,6 +1040,7 @@ async def get_sienge_creditor_bank_info(creditor_id: str) -> Dict:
|
|
|
303
1040
|
"message": f"❌ Erro ao buscar info bancária do credor {creditor_id}",
|
|
304
1041
|
"error": result.get("error"),
|
|
305
1042
|
"details": result.get("message"),
|
|
1043
|
+
"request_id": result.get("request_id")
|
|
306
1044
|
}
|
|
307
1045
|
|
|
308
1046
|
|
|
@@ -326,6 +1064,7 @@ async def get_sienge_accounts_receivable(
|
|
|
326
1064
|
) -> Dict:
|
|
327
1065
|
"""
|
|
328
1066
|
Consulta parcelas do contas a receber via API bulk-data
|
|
1067
|
+
MELHORADO: Suporte a polling assíncrono para requests 202
|
|
329
1068
|
|
|
330
1069
|
Args:
|
|
331
1070
|
start_date: Data de início do período (YYYY-MM-DD) - OBRIGATÓRIO
|
|
@@ -341,48 +1080,61 @@ async def get_sienge_accounts_receivable(
|
|
|
341
1080
|
bearers_id_in: Filtrar parcelas com códigos de portador específicos
|
|
342
1081
|
bearers_id_not_in: Filtrar parcelas excluindo códigos de portador específicos
|
|
343
1082
|
"""
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
params["originsIds"] = origins_ids
|
|
360
|
-
if bearers_id_in:
|
|
361
|
-
params["bearersIdIn"] = bearers_id_in
|
|
362
|
-
if bearers_id_not_in:
|
|
363
|
-
params["bearersIdNotIn"] = bearers_id_not_in
|
|
364
|
-
|
|
365
|
-
result = await make_sienge_bulk_request("GET", "/income", params=params)
|
|
1083
|
+
# Usar serviço interno com polling assíncrono
|
|
1084
|
+
result = await _svc_get_accounts_receivable(
|
|
1085
|
+
start_date=start_date,
|
|
1086
|
+
end_date=end_date,
|
|
1087
|
+
selection_type=selection_type,
|
|
1088
|
+
company_id=company_id,
|
|
1089
|
+
cost_centers_id=cost_centers_id,
|
|
1090
|
+
correction_indexer_id=correction_indexer_id,
|
|
1091
|
+
correction_date=correction_date,
|
|
1092
|
+
change_start_date=change_start_date,
|
|
1093
|
+
completed_bills=completed_bills,
|
|
1094
|
+
origins_ids=origins_ids,
|
|
1095
|
+
bearers_id_in=bearers_id_in,
|
|
1096
|
+
bearers_id_not_in=bearers_id_not_in
|
|
1097
|
+
)
|
|
366
1098
|
|
|
367
1099
|
if result["success"]:
|
|
368
|
-
|
|
369
|
-
income_data =
|
|
370
|
-
|
|
371
|
-
|
|
1100
|
+
# Para requests normais (200) e assíncronos processados
|
|
1101
|
+
income_data = result.get("data", [])
|
|
1102
|
+
|
|
1103
|
+
response = {
|
|
372
1104
|
"success": True,
|
|
373
1105
|
"message": f"✅ Encontradas {len(income_data)} parcelas a receber",
|
|
374
1106
|
"income_data": income_data,
|
|
375
1107
|
"count": len(income_data),
|
|
376
1108
|
"period": f"{start_date} a {end_date}",
|
|
377
1109
|
"selection_type": selection_type,
|
|
378
|
-
"
|
|
1110
|
+
"request_id": result.get("request_id"),
|
|
1111
|
+
"latency_ms": result.get("latency_ms")
|
|
379
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
# Se foi processamento assíncrono, incluir informações extras
|
|
1115
|
+
if result.get("async_identifier"):
|
|
1116
|
+
response.update({
|
|
1117
|
+
"async_processing": {
|
|
1118
|
+
"identifier": result.get("async_identifier"),
|
|
1119
|
+
"correlation_id": result.get("correlation_id"),
|
|
1120
|
+
"chunks_downloaded": result.get("chunks_downloaded"),
|
|
1121
|
+
"rows_returned": result.get("rows_returned"),
|
|
1122
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1123
|
+
}
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
return response
|
|
380
1127
|
|
|
381
1128
|
return {
|
|
382
1129
|
"success": False,
|
|
383
1130
|
"message": "❌ Erro ao buscar parcelas a receber",
|
|
384
1131
|
"error": result.get("error"),
|
|
385
1132
|
"details": result.get("message"),
|
|
1133
|
+
"request_id": result.get("request_id"),
|
|
1134
|
+
"async_info": {
|
|
1135
|
+
"identifier": result.get("async_identifier"),
|
|
1136
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1137
|
+
} if result.get("async_identifier") else None
|
|
386
1138
|
}
|
|
387
1139
|
|
|
388
1140
|
|
|
@@ -392,39 +1144,57 @@ async def get_sienge_accounts_receivable_by_bills(
|
|
|
392
1144
|
) -> Dict:
|
|
393
1145
|
"""
|
|
394
1146
|
Consulta parcelas dos títulos informados via API bulk-data
|
|
1147
|
+
MELHORADO: Suporte a polling assíncrono para requests 202
|
|
395
1148
|
|
|
396
1149
|
Args:
|
|
397
1150
|
bills_ids: Lista de códigos dos títulos - OBRIGATÓRIO
|
|
398
1151
|
correction_indexer_id: Código do indexador de correção
|
|
399
1152
|
correction_date: Data para correção do indexador (YYYY-MM-DD)
|
|
400
1153
|
"""
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
result = await make_sienge_bulk_request("GET", "/income/by-bills", params=params)
|
|
1154
|
+
# Usar serviço interno com polling assíncrono
|
|
1155
|
+
result = await _svc_get_accounts_receivable_by_bills(
|
|
1156
|
+
bills_ids=bills_ids,
|
|
1157
|
+
correction_indexer_id=correction_indexer_id,
|
|
1158
|
+
correction_date=correction_date
|
|
1159
|
+
)
|
|
409
1160
|
|
|
410
1161
|
if result["success"]:
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
return {
|
|
1162
|
+
income_data = result.get("data", [])
|
|
1163
|
+
|
|
1164
|
+
response = {
|
|
415
1165
|
"success": True,
|
|
416
1166
|
"message": f"✅ Encontradas {len(income_data)} parcelas dos títulos informados",
|
|
417
1167
|
"income_data": income_data,
|
|
418
1168
|
"count": len(income_data),
|
|
419
1169
|
"bills_consulted": bills_ids,
|
|
420
|
-
"
|
|
1170
|
+
"request_id": result.get("request_id"),
|
|
1171
|
+
"latency_ms": result.get("latency_ms")
|
|
421
1172
|
}
|
|
1173
|
+
|
|
1174
|
+
# Se foi processamento assíncrono, incluir informações extras
|
|
1175
|
+
if result.get("async_identifier"):
|
|
1176
|
+
response.update({
|
|
1177
|
+
"async_processing": {
|
|
1178
|
+
"identifier": result.get("async_identifier"),
|
|
1179
|
+
"correlation_id": result.get("correlation_id"),
|
|
1180
|
+
"chunks_downloaded": result.get("chunks_downloaded"),
|
|
1181
|
+
"rows_returned": result.get("rows_returned"),
|
|
1182
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1183
|
+
}
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
return response
|
|
422
1187
|
|
|
423
1188
|
return {
|
|
424
1189
|
"success": False,
|
|
425
1190
|
"message": "❌ Erro ao buscar parcelas dos títulos informados",
|
|
426
1191
|
"error": result.get("error"),
|
|
427
1192
|
"details": result.get("message"),
|
|
1193
|
+
"request_id": result.get("request_id"),
|
|
1194
|
+
"async_info": {
|
|
1195
|
+
"identifier": result.get("async_identifier"),
|
|
1196
|
+
"polling_attempts": result.get("polling_attempts")
|
|
1197
|
+
} if result.get("async_identifier") else None
|
|
428
1198
|
}
|
|
429
1199
|
|
|
430
1200
|
|
|
@@ -446,26 +1216,14 @@ async def get_sienge_bills(
|
|
|
446
1216
|
status: Status do título (ex: open, paid, cancelled)
|
|
447
1217
|
limit: Máximo de registros (padrão: 50, máx: 200)
|
|
448
1218
|
"""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
458
|
-
|
|
459
|
-
# Parâmetros obrigatórios
|
|
460
|
-
params = {"startDate": start_date, "endDate": end_date, "limit": min(limit or 50, 200)} # OBRIGATÓRIO pela API
|
|
461
|
-
|
|
462
|
-
# Parâmetros opcionais
|
|
463
|
-
if creditor_id:
|
|
464
|
-
params["creditor_id"] = creditor_id
|
|
465
|
-
if status:
|
|
466
|
-
params["status"] = status
|
|
467
|
-
|
|
468
|
-
result = await make_sienge_request("GET", "/bills", params=params)
|
|
1219
|
+
# Usar serviço interno
|
|
1220
|
+
result = await _svc_get_bills(
|
|
1221
|
+
start_date=start_date,
|
|
1222
|
+
end_date=end_date,
|
|
1223
|
+
creditor_id=creditor_id,
|
|
1224
|
+
status=status,
|
|
1225
|
+
limit=limit or 50
|
|
1226
|
+
)
|
|
469
1227
|
|
|
470
1228
|
if result["success"]:
|
|
471
1229
|
data = result["data"]
|
|
@@ -473,14 +1231,33 @@ async def get_sienge_bills(
|
|
|
473
1231
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
474
1232
|
total_count = metadata.get("count", len(bills))
|
|
475
1233
|
|
|
1234
|
+
# Aplicar parsing numérico nos valores
|
|
1235
|
+
for bill in bills:
|
|
1236
|
+
if "amount" in bill:
|
|
1237
|
+
bill["amount"] = _parse_numeric_value(bill["amount"])
|
|
1238
|
+
if "paid_amount" in bill:
|
|
1239
|
+
bill["paid_amount"] = _parse_numeric_value(bill["paid_amount"])
|
|
1240
|
+
if "remaining_amount" in bill:
|
|
1241
|
+
bill["remaining_amount"] = _parse_numeric_value(bill["remaining_amount"])
|
|
1242
|
+
|
|
1243
|
+
# Usar datas padrão se não fornecidas
|
|
1244
|
+
actual_start = start_date or (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
1245
|
+
actual_end = end_date or datetime.now().strftime("%Y-%m-%d")
|
|
1246
|
+
|
|
476
1247
|
return {
|
|
477
1248
|
"success": True,
|
|
478
|
-
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {
|
|
1249
|
+
"message": f"✅ Encontrados {len(bills)} títulos a pagar (total: {total_count}) - período: {actual_start} a {actual_end}",
|
|
479
1250
|
"bills": bills,
|
|
480
1251
|
"count": len(bills),
|
|
481
1252
|
"total_count": total_count,
|
|
482
|
-
"period": {"start_date":
|
|
483
|
-
"
|
|
1253
|
+
"period": {"start_date": actual_start, "end_date": actual_end},
|
|
1254
|
+
"filters_applied": {
|
|
1255
|
+
"start_date": actual_start, "end_date": actual_end,
|
|
1256
|
+
"creditor_id": creditor_id, "status": status, "limit": limit
|
|
1257
|
+
},
|
|
1258
|
+
"request_id": result.get("request_id"),
|
|
1259
|
+
"latency_ms": result.get("latency_ms"),
|
|
1260
|
+
"cache": result.get("cache")
|
|
484
1261
|
}
|
|
485
1262
|
|
|
486
1263
|
return {
|
|
@@ -488,6 +1265,7 @@ async def get_sienge_bills(
|
|
|
488
1265
|
"message": "❌ Erro ao buscar títulos a pagar",
|
|
489
1266
|
"error": result.get("error"),
|
|
490
1267
|
"details": result.get("message"),
|
|
1268
|
+
"request_id": result.get("request_id")
|
|
491
1269
|
}
|
|
492
1270
|
|
|
493
1271
|
|
|
@@ -533,6 +1311,8 @@ async def get_sienge_purchase_orders(
|
|
|
533
1311
|
"message": f"✅ Encontrados {len(orders)} pedidos de compra",
|
|
534
1312
|
"purchase_orders": orders,
|
|
535
1313
|
"count": len(orders),
|
|
1314
|
+
"request_id": result.get("request_id"),
|
|
1315
|
+
"latency_ms": result.get("latency_ms")
|
|
536
1316
|
}
|
|
537
1317
|
|
|
538
1318
|
return {
|
|
@@ -540,6 +1320,8 @@ async def get_sienge_purchase_orders(
|
|
|
540
1320
|
"message": "❌ Erro ao buscar pedidos de compra",
|
|
541
1321
|
"error": result.get("error"),
|
|
542
1322
|
"details": result.get("message"),
|
|
1323
|
+
"request_id": result.get("request_id"),
|
|
1324
|
+
"latency_ms": result.get("latency_ms")
|
|
543
1325
|
}
|
|
544
1326
|
|
|
545
1327
|
|
|
@@ -631,14 +1413,18 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
631
1413
|
"date": datetime.now().strftime("%Y-%m-%d"),
|
|
632
1414
|
}
|
|
633
1415
|
|
|
634
|
-
|
|
1416
|
+
# CORRIGIDO: Normalizar JSON payload
|
|
1417
|
+
json_data = to_camel_json(request_data)
|
|
1418
|
+
result = await make_sienge_request("POST", "/purchase-requests", json_data=json_data)
|
|
635
1419
|
|
|
636
1420
|
if result["success"]:
|
|
637
1421
|
return {
|
|
638
1422
|
"success": True,
|
|
639
1423
|
"message": "✅ Solicitação de compra criada com sucesso",
|
|
640
|
-
"request_id": result
|
|
1424
|
+
"request_id": result.get("request_id"),
|
|
1425
|
+
"purchase_request_id": result["data"].get("id"),
|
|
641
1426
|
"data": result["data"],
|
|
1427
|
+
"latency_ms": result.get("latency_ms")
|
|
642
1428
|
}
|
|
643
1429
|
|
|
644
1430
|
return {
|
|
@@ -646,6 +1432,7 @@ async def create_sienge_purchase_request(description: str, project_id: str, item
|
|
|
646
1432
|
"message": "❌ Erro ao criar solicitação de compra",
|
|
647
1433
|
"error": result.get("error"),
|
|
648
1434
|
"details": result.get("message"),
|
|
1435
|
+
"request_id": result.get("request_id")
|
|
649
1436
|
}
|
|
650
1437
|
|
|
651
1438
|
|
|
@@ -731,13 +1518,13 @@ async def create_sienge_purchase_invoice(
|
|
|
731
1518
|
notes: Observações (opcional)
|
|
732
1519
|
"""
|
|
733
1520
|
invoice_data = {
|
|
734
|
-
"
|
|
1521
|
+
"document_id": document_id,
|
|
735
1522
|
"number": number,
|
|
736
|
-
"
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
"
|
|
740
|
-
"
|
|
1523
|
+
"supplier_id": supplier_id,
|
|
1524
|
+
"company_id": company_id,
|
|
1525
|
+
"movement_type_id": movement_type_id,
|
|
1526
|
+
"movement_date": movement_date,
|
|
1527
|
+
"issue_date": issue_date,
|
|
741
1528
|
}
|
|
742
1529
|
|
|
743
1530
|
if series:
|
|
@@ -745,7 +1532,7 @@ async def create_sienge_purchase_invoice(
|
|
|
745
1532
|
if notes:
|
|
746
1533
|
invoice_data["notes"] = notes
|
|
747
1534
|
|
|
748
|
-
result = await make_sienge_request("POST", "/purchase-invoices", json_data=invoice_data)
|
|
1535
|
+
result = await make_sienge_request("POST", "/purchase-invoices", json_data=to_camel_json(invoice_data))
|
|
749
1536
|
|
|
750
1537
|
if result["success"]:
|
|
751
1538
|
return {"success": True, "message": f"✅ Nota fiscal {number} criada com sucesso", "invoice": result["data"]}
|
|
@@ -782,14 +1569,14 @@ async def add_items_to_purchase_invoice(
|
|
|
782
1569
|
copy_attachments_purchase_orders: Copiar anexos dos pedidos de compra
|
|
783
1570
|
"""
|
|
784
1571
|
item_data = {
|
|
785
|
-
"
|
|
786
|
-
"
|
|
787
|
-
"
|
|
788
|
-
"
|
|
1572
|
+
"deliveries_order": deliveries_order,
|
|
1573
|
+
"copy_notes_purchase_orders": copy_notes_purchase_orders,
|
|
1574
|
+
"copy_notes_resources": copy_notes_resources,
|
|
1575
|
+
"copy_attachments_purchase_orders": copy_attachments_purchase_orders,
|
|
789
1576
|
}
|
|
790
1577
|
|
|
791
1578
|
result = await make_sienge_request(
|
|
792
|
-
"POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=item_data
|
|
1579
|
+
"POST", f"/purchase-invoices/{sequential_number}/items/purchase-orders/delivery-schedules", json_data=to_camel_json(item_data)
|
|
793
1580
|
)
|
|
794
1581
|
|
|
795
1582
|
if result["success"]:
|
|
@@ -950,6 +1737,7 @@ async def get_sienge_projects(
|
|
|
950
1737
|
) -> Dict:
|
|
951
1738
|
"""
|
|
952
1739
|
Busca empreendimentos/obras no Sienge
|
|
1740
|
+
CORRIGIDO: Mapeamento correto da chave de resposta
|
|
953
1741
|
|
|
954
1742
|
Args:
|
|
955
1743
|
limit: Máximo de registros (padrão: 100, máximo: 200)
|
|
@@ -959,31 +1747,39 @@ async def get_sienge_projects(
|
|
|
959
1747
|
receivable_register: Filtro de registro de recebíveis (B3, CERC)
|
|
960
1748
|
only_buildings_enabled: Retornar apenas obras habilitadas para integração orçamentária
|
|
961
1749
|
"""
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
params["onlyBuildingsEnabledForIntegration"] = only_buildings_enabled
|
|
972
|
-
|
|
973
|
-
result = await make_sienge_request("GET", "/enterprises", params=params)
|
|
1750
|
+
# Usar serviço interno
|
|
1751
|
+
result = await _svc_get_projects(
|
|
1752
|
+
limit=limit or 100,
|
|
1753
|
+
offset=offset or 0,
|
|
1754
|
+
company_id=company_id,
|
|
1755
|
+
enterprise_type=enterprise_type,
|
|
1756
|
+
receivable_register=receivable_register,
|
|
1757
|
+
only_buildings_enabled=only_buildings_enabled or False
|
|
1758
|
+
)
|
|
974
1759
|
|
|
975
1760
|
if result["success"]:
|
|
976
1761
|
data = result["data"]
|
|
1762
|
+
# CORREÇÃO: API retorna em "results", mas paginador espera em "enterprises"
|
|
977
1763
|
enterprises = data.get("results", []) if isinstance(data, dict) else data
|
|
978
1764
|
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
1765
|
+
total_count = metadata.get("count", len(enterprises))
|
|
979
1766
|
|
|
980
1767
|
return {
|
|
981
1768
|
"success": True,
|
|
982
|
-
"message": f"✅ Encontrados {len(enterprises)} empreendimentos",
|
|
983
|
-
"enterprises": enterprises,
|
|
1769
|
+
"message": f"✅ Encontrados {len(enterprises)} empreendimentos (total: {total_count})",
|
|
1770
|
+
"enterprises": enterprises, # Manter consistência para paginador
|
|
1771
|
+
"projects": enterprises, # Alias para compatibilidade
|
|
984
1772
|
"count": len(enterprises),
|
|
1773
|
+
"total_count": total_count,
|
|
985
1774
|
"metadata": metadata,
|
|
986
|
-
"
|
|
1775
|
+
"filters_applied": {
|
|
1776
|
+
"limit": limit, "offset": offset, "company_id": company_id,
|
|
1777
|
+
"enterprise_type": enterprise_type, "receivable_register": receivable_register,
|
|
1778
|
+
"only_buildings_enabled": only_buildings_enabled
|
|
1779
|
+
},
|
|
1780
|
+
"request_id": result.get("request_id"),
|
|
1781
|
+
"latency_ms": result.get("latency_ms"),
|
|
1782
|
+
"cache": result.get("cache")
|
|
987
1783
|
}
|
|
988
1784
|
|
|
989
1785
|
return {
|
|
@@ -991,6 +1787,7 @@ async def get_sienge_projects(
|
|
|
991
1787
|
"message": "❌ Erro ao buscar empreendimentos",
|
|
992
1788
|
"error": result.get("error"),
|
|
993
1789
|
"details": result.get("message"),
|
|
1790
|
+
"request_id": result.get("request_id")
|
|
994
1791
|
}
|
|
995
1792
|
|
|
996
1793
|
|
|
@@ -1065,6 +1862,9 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0)
|
|
|
1065
1862
|
"message": f"✅ Encontradas {len(units)} unidades (total: {total_count})",
|
|
1066
1863
|
"units": units,
|
|
1067
1864
|
"count": len(units),
|
|
1865
|
+
"total_count": total_count,
|
|
1866
|
+
"request_id": result.get("request_id"),
|
|
1867
|
+
"latency_ms": result.get("latency_ms")
|
|
1068
1868
|
}
|
|
1069
1869
|
|
|
1070
1870
|
return {
|
|
@@ -1072,6 +1872,8 @@ async def get_sienge_units(limit: Optional[int] = 50, offset: Optional[int] = 0)
|
|
|
1072
1872
|
"message": "❌ Erro ao buscar unidades",
|
|
1073
1873
|
"error": result.get("error"),
|
|
1074
1874
|
"details": result.get("message"),
|
|
1875
|
+
"request_id": result.get("request_id"),
|
|
1876
|
+
"latency_ms": result.get("latency_ms")
|
|
1075
1877
|
}
|
|
1076
1878
|
|
|
1077
1879
|
|
|
@@ -1210,35 +2012,44 @@ async def search_sienge_data(
|
|
|
1210
2012
|
|
|
1211
2013
|
|
|
1212
2014
|
async def _search_specific_entity(entity_type: str, query: str, limit: int, filters: Dict) -> Dict:
|
|
1213
|
-
"""
|
|
2015
|
+
"""
|
|
2016
|
+
Função auxiliar para buscar em uma entidade específica
|
|
2017
|
+
CORRIGIDO: Usa serviços internos, nunca outras tools
|
|
2018
|
+
"""
|
|
1214
2019
|
|
|
1215
2020
|
if entity_type == "customers":
|
|
1216
|
-
result = await
|
|
2021
|
+
result = await _svc_get_customers(limit=limit, search=query)
|
|
1217
2022
|
if result["success"]:
|
|
2023
|
+
data = result["data"]
|
|
2024
|
+
customers = data.get("results", []) if isinstance(data, dict) else data
|
|
1218
2025
|
return {
|
|
1219
2026
|
"success": True,
|
|
1220
|
-
"data":
|
|
1221
|
-
"count":
|
|
2027
|
+
"data": customers,
|
|
2028
|
+
"count": len(customers),
|
|
1222
2029
|
"entity_type": "customers"
|
|
1223
2030
|
}
|
|
1224
2031
|
|
|
1225
2032
|
elif entity_type == "creditors":
|
|
1226
|
-
result = await
|
|
2033
|
+
result = await _svc_get_creditors(limit=limit, search=query)
|
|
1227
2034
|
if result["success"]:
|
|
2035
|
+
data = result["data"]
|
|
2036
|
+
creditors = data.get("results", []) if isinstance(data, dict) else data
|
|
1228
2037
|
return {
|
|
1229
2038
|
"success": True,
|
|
1230
|
-
"data":
|
|
1231
|
-
"count":
|
|
2039
|
+
"data": creditors,
|
|
2040
|
+
"count": len(creditors),
|
|
1232
2041
|
"entity_type": "creditors"
|
|
1233
2042
|
}
|
|
1234
2043
|
|
|
1235
2044
|
elif entity_type == "projects" or entity_type == "enterprises":
|
|
1236
2045
|
# Para projetos, usar filtros mais específicos se disponível
|
|
1237
2046
|
company_id = filters.get("company_id")
|
|
1238
|
-
result = await
|
|
2047
|
+
result = await _svc_get_projects(limit=limit, company_id=company_id)
|
|
1239
2048
|
if result["success"]:
|
|
2049
|
+
data = result["data"]
|
|
2050
|
+
projects = data.get("results", []) if isinstance(data, dict) else data
|
|
2051
|
+
|
|
1240
2052
|
# Filtrar por query se fornecida
|
|
1241
|
-
projects = result["enterprises"]
|
|
1242
2053
|
if query:
|
|
1243
2054
|
projects = [
|
|
1244
2055
|
p for p in projects
|
|
@@ -1257,23 +2068,27 @@ async def _search_specific_entity(entity_type: str, query: str, limit: int, filt
|
|
|
1257
2068
|
# Para títulos, usar data padrão se não especificada
|
|
1258
2069
|
start_date = filters.get("start_date")
|
|
1259
2070
|
end_date = filters.get("end_date")
|
|
1260
|
-
result = await
|
|
2071
|
+
result = await _svc_get_bills(
|
|
1261
2072
|
start_date=start_date,
|
|
1262
2073
|
end_date=end_date,
|
|
1263
2074
|
limit=limit
|
|
1264
2075
|
)
|
|
1265
2076
|
if result["success"]:
|
|
2077
|
+
data = result["data"]
|
|
2078
|
+
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
1266
2079
|
return {
|
|
1267
2080
|
"success": True,
|
|
1268
|
-
"data":
|
|
1269
|
-
"count":
|
|
2081
|
+
"data": bills,
|
|
2082
|
+
"count": len(bills),
|
|
1270
2083
|
"entity_type": "bills"
|
|
1271
2084
|
}
|
|
1272
2085
|
|
|
1273
2086
|
elif entity_type == "purchase_orders":
|
|
1274
|
-
result = await
|
|
2087
|
+
result = await _svc_get_purchase_orders(limit=limit)
|
|
1275
2088
|
if result["success"]:
|
|
1276
|
-
|
|
2089
|
+
data = result["data"]
|
|
2090
|
+
orders = data.get("results", []) if isinstance(data, dict) else data
|
|
2091
|
+
|
|
1277
2092
|
# Filtrar por query se fornecida
|
|
1278
2093
|
if query:
|
|
1279
2094
|
orders = [
|
|
@@ -1316,28 +2131,28 @@ async def list_sienge_entities() -> Dict:
|
|
|
1316
2131
|
"name": "Credores/Fornecedores",
|
|
1317
2132
|
"description": "Fornecedores e credores cadastrados",
|
|
1318
2133
|
"search_fields": ["nome", "documento"],
|
|
1319
|
-
"tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info"]
|
|
2134
|
+
"tools": ["get_sienge_creditors", "get_sienge_creditor_bank_info", "get_sienge_suppliers"]
|
|
1320
2135
|
},
|
|
1321
2136
|
{
|
|
1322
2137
|
"type": "projects",
|
|
1323
2138
|
"name": "Empreendimentos/Obras",
|
|
1324
2139
|
"description": "Projetos e obras cadastrados",
|
|
1325
2140
|
"search_fields": ["código", "descrição", "nome"],
|
|
1326
|
-
"tools": ["get_sienge_projects", "get_sienge_enterprise_by_id"]
|
|
2141
|
+
"tools": ["get_sienge_projects", "get_sienge_enterprises", "get_sienge_enterprise_by_id"]
|
|
1327
2142
|
},
|
|
1328
2143
|
{
|
|
1329
2144
|
"type": "bills",
|
|
1330
2145
|
"name": "Títulos a Pagar",
|
|
1331
2146
|
"description": "Contas a pagar e títulos financeiros",
|
|
1332
2147
|
"search_fields": ["número", "credor", "valor"],
|
|
1333
|
-
"tools": ["get_sienge_bills"]
|
|
2148
|
+
"tools": ["get_sienge_bills", "get_sienge_accounts_payable"]
|
|
1334
2149
|
},
|
|
1335
2150
|
{
|
|
1336
2151
|
"type": "purchase_orders",
|
|
1337
2152
|
"name": "Pedidos de Compra",
|
|
1338
2153
|
"description": "Pedidos de compra e solicitações",
|
|
1339
2154
|
"search_fields": ["id", "descrição", "status"],
|
|
1340
|
-
"tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests"]
|
|
2155
|
+
"tools": ["get_sienge_purchase_orders", "get_sienge_purchase_requests", "list_sienge_purchase_requests"]
|
|
1341
2156
|
},
|
|
1342
2157
|
{
|
|
1343
2158
|
"type": "invoices",
|
|
@@ -1358,7 +2173,14 @@ async def list_sienge_entities() -> Dict:
|
|
|
1358
2173
|
"name": "Financeiro",
|
|
1359
2174
|
"description": "Contas a receber e movimentações financeiras",
|
|
1360
2175
|
"search_fields": ["período", "cliente", "valor"],
|
|
1361
|
-
"tools": ["get_sienge_accounts_receivable"]
|
|
2176
|
+
"tools": ["get_sienge_accounts_receivable", "search_sienge_financial_data", "search_sienge_finances"]
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
"type": "suppliers",
|
|
2180
|
+
"name": "Fornecedores",
|
|
2181
|
+
"description": "Fornecedores e credores cadastrados",
|
|
2182
|
+
"search_fields": ["código", "nome", "razão social"],
|
|
2183
|
+
"tools": ["get_sienge_suppliers"]
|
|
1362
2184
|
}
|
|
1363
2185
|
]
|
|
1364
2186
|
|
|
@@ -1401,41 +2223,70 @@ async def get_sienge_data_paginated(
|
|
|
1401
2223
|
|
|
1402
2224
|
filters = filters or {}
|
|
1403
2225
|
|
|
1404
|
-
# Mapear para
|
|
2226
|
+
# CORRIGIDO: Mapear para serviços internos, não tools
|
|
1405
2227
|
if entity_type == "customers":
|
|
1406
2228
|
search = filters.get("search")
|
|
1407
2229
|
customer_type_id = filters.get("customer_type_id")
|
|
1408
|
-
result = await
|
|
2230
|
+
result = await _svc_get_customers(
|
|
1409
2231
|
limit=page_size,
|
|
1410
2232
|
offset=offset,
|
|
1411
2233
|
search=search,
|
|
1412
2234
|
customer_type_id=customer_type_id
|
|
1413
2235
|
)
|
|
2236
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2237
|
+
if result["success"]:
|
|
2238
|
+
data = result["data"]
|
|
2239
|
+
items, total = _extract_items_and_total(data)
|
|
2240
|
+
result["customers"] = items
|
|
2241
|
+
result["count"] = len(items)
|
|
2242
|
+
result["total_count"] = total
|
|
1414
2243
|
|
|
1415
2244
|
elif entity_type == "creditors":
|
|
1416
2245
|
search = filters.get("search")
|
|
1417
|
-
result = await
|
|
2246
|
+
result = await _svc_get_creditors(
|
|
1418
2247
|
limit=page_size,
|
|
1419
2248
|
offset=offset,
|
|
1420
2249
|
search=search
|
|
1421
2250
|
)
|
|
2251
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2252
|
+
if result["success"]:
|
|
2253
|
+
data = result["data"]
|
|
2254
|
+
items, total = _extract_items_and_total(data)
|
|
2255
|
+
result["creditors"] = items
|
|
2256
|
+
result["count"] = len(items)
|
|
2257
|
+
result["total_count"] = total
|
|
1422
2258
|
|
|
1423
2259
|
elif entity_type == "projects":
|
|
1424
|
-
result = await
|
|
2260
|
+
result = await _svc_get_projects(
|
|
1425
2261
|
limit=page_size,
|
|
1426
2262
|
offset=offset,
|
|
1427
2263
|
company_id=filters.get("company_id"),
|
|
1428
2264
|
enterprise_type=filters.get("enterprise_type")
|
|
1429
2265
|
)
|
|
2266
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2267
|
+
if result["success"]:
|
|
2268
|
+
data = result["data"]
|
|
2269
|
+
items, total = _extract_items_and_total(data)
|
|
2270
|
+
result["projects"] = items
|
|
2271
|
+
result["enterprises"] = items # Para compatibilidade
|
|
2272
|
+
result["count"] = len(items)
|
|
2273
|
+
result["total_count"] = total
|
|
1430
2274
|
|
|
1431
2275
|
elif entity_type == "bills":
|
|
1432
|
-
result = await
|
|
2276
|
+
result = await _svc_get_bills(
|
|
1433
2277
|
start_date=filters.get("start_date"),
|
|
1434
2278
|
end_date=filters.get("end_date"),
|
|
1435
2279
|
creditor_id=filters.get("creditor_id"),
|
|
1436
2280
|
status=filters.get("status"),
|
|
1437
2281
|
limit=page_size
|
|
1438
2282
|
)
|
|
2283
|
+
# CORRIGIDO: Extrair items e total corretamente
|
|
2284
|
+
if result["success"]:
|
|
2285
|
+
data = result["data"]
|
|
2286
|
+
items, total = _extract_items_and_total(data)
|
|
2287
|
+
result["bills"] = items
|
|
2288
|
+
result["count"] = len(items)
|
|
2289
|
+
result["total_count"] = total
|
|
1439
2290
|
|
|
1440
2291
|
else:
|
|
1441
2292
|
return {
|
|
@@ -1499,20 +2350,21 @@ async def search_sienge_financial_data(
|
|
|
1499
2350
|
# Buscar contas a receber
|
|
1500
2351
|
if search_type in ["receivable", "both"]:
|
|
1501
2352
|
try:
|
|
1502
|
-
|
|
2353
|
+
# CORRIGIDO: Usar serviço interno
|
|
2354
|
+
receivable_result = await _svc_get_accounts_receivable(
|
|
1503
2355
|
start_date=period_start,
|
|
1504
2356
|
end_date=period_end,
|
|
1505
2357
|
selection_type="D" # Por vencimento
|
|
1506
2358
|
)
|
|
1507
2359
|
|
|
1508
2360
|
if receivable_result["success"]:
|
|
1509
|
-
receivable_data = receivable_result
|
|
2361
|
+
receivable_data = receivable_result.get("data", [])
|
|
1510
2362
|
|
|
1511
|
-
# Aplicar filtros de valor
|
|
2363
|
+
# CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
|
|
1512
2364
|
if amount_min is not None or amount_max is not None:
|
|
1513
2365
|
filtered_data = []
|
|
1514
2366
|
for item in receivable_data:
|
|
1515
|
-
amount =
|
|
2367
|
+
amount = _parse_numeric_value(item.get("amount", 0))
|
|
1516
2368
|
if amount_min is not None and amount < amount_min:
|
|
1517
2369
|
continue
|
|
1518
2370
|
if amount_max is not None and amount > amount_max:
|
|
@@ -1545,20 +2397,22 @@ async def search_sienge_financial_data(
|
|
|
1545
2397
|
# Buscar contas a pagar
|
|
1546
2398
|
if search_type in ["payable", "both"]:
|
|
1547
2399
|
try:
|
|
1548
|
-
|
|
2400
|
+
# CORRIGIDO: Usar serviço interno
|
|
2401
|
+
payable_result = await _svc_get_bills(
|
|
1549
2402
|
start_date=period_start,
|
|
1550
2403
|
end_date=period_end,
|
|
1551
2404
|
limit=100
|
|
1552
2405
|
)
|
|
1553
2406
|
|
|
1554
2407
|
if payable_result["success"]:
|
|
1555
|
-
|
|
2408
|
+
data = payable_result["data"]
|
|
2409
|
+
payable_data = data.get("results", []) if isinstance(data, dict) else data
|
|
1556
2410
|
|
|
1557
|
-
# Aplicar filtros de valor
|
|
2411
|
+
# CORRIGIDO: Aplicar filtros de valor usando _parse_numeric_value
|
|
1558
2412
|
if amount_min is not None or amount_max is not None:
|
|
1559
2413
|
filtered_data = []
|
|
1560
2414
|
for item in payable_data:
|
|
1561
|
-
amount =
|
|
2415
|
+
amount = _parse_numeric_value(item.get("amount", 0))
|
|
1562
2416
|
if amount_min is not None and amount < amount_min:
|
|
1563
2417
|
continue
|
|
1564
2418
|
if amount_max is not None and amount > amount_max:
|
|
@@ -1650,23 +2504,26 @@ async def get_sienge_dashboard_summary() -> Dict:
|
|
|
1650
2504
|
|
|
1651
2505
|
# 2. Contar clientes (amostra)
|
|
1652
2506
|
try:
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
else:
|
|
1657
|
-
dashboard_data["customers_available"] = False
|
|
2507
|
+
# CORRIGIDO: Usar serviço interno
|
|
2508
|
+
customers_result = await _svc_get_customers(limit=1)
|
|
2509
|
+
dashboard_data["customers"] = {"available": customers_result["success"]}
|
|
1658
2510
|
except Exception as e:
|
|
1659
2511
|
errors.append(f"Clientes: {str(e)}")
|
|
1660
|
-
dashboard_data["
|
|
2512
|
+
dashboard_data["customers"] = {"available": False}
|
|
1661
2513
|
|
|
1662
2514
|
# 3. Contar projetos (amostra)
|
|
1663
2515
|
try:
|
|
1664
|
-
|
|
2516
|
+
# CORRIGIDO: Usar serviço interno
|
|
2517
|
+
projects_result = await _svc_get_projects(limit=5)
|
|
1665
2518
|
if projects_result["success"]:
|
|
2519
|
+
data = projects_result["data"]
|
|
2520
|
+
enterprises = data.get("results", []) if isinstance(data, dict) else data
|
|
2521
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
2522
|
+
|
|
1666
2523
|
dashboard_data["projects"] = {
|
|
1667
2524
|
"available": True,
|
|
1668
|
-
"sample_count": len(
|
|
1669
|
-
"total_count":
|
|
2525
|
+
"sample_count": len(enterprises),
|
|
2526
|
+
"total_count": metadata.get("count", "N/A")
|
|
1670
2527
|
}
|
|
1671
2528
|
else:
|
|
1672
2529
|
dashboard_data["projects"] = {"available": False}
|
|
@@ -1676,16 +2533,21 @@ async def get_sienge_dashboard_summary() -> Dict:
|
|
|
1676
2533
|
|
|
1677
2534
|
# 4. Títulos a pagar do mês atual
|
|
1678
2535
|
try:
|
|
1679
|
-
|
|
2536
|
+
# CORRIGIDO: Usar serviço interno
|
|
2537
|
+
bills_result = await _svc_get_bills(
|
|
1680
2538
|
start_date=current_month_start,
|
|
1681
2539
|
end_date=current_month_end,
|
|
1682
2540
|
limit=10
|
|
1683
2541
|
)
|
|
1684
2542
|
if bills_result["success"]:
|
|
2543
|
+
data = bills_result["data"]
|
|
2544
|
+
bills = data.get("results", []) if isinstance(data, dict) else data
|
|
2545
|
+
metadata = data.get("resultSetMetadata", {}) if isinstance(data, dict) else {}
|
|
2546
|
+
|
|
1685
2547
|
dashboard_data["monthly_bills"] = {
|
|
1686
2548
|
"available": True,
|
|
1687
|
-
"count": len(
|
|
1688
|
-
"total_count":
|
|
2549
|
+
"count": len(bills),
|
|
2550
|
+
"total_count": metadata.get("count", len(bills))
|
|
1689
2551
|
}
|
|
1690
2552
|
else:
|
|
1691
2553
|
dashboard_data["monthly_bills"] = {"available": False}
|
|
@@ -1695,11 +2557,15 @@ async def get_sienge_dashboard_summary() -> Dict:
|
|
|
1695
2557
|
|
|
1696
2558
|
# 5. Tipos de clientes
|
|
1697
2559
|
try:
|
|
1698
|
-
|
|
2560
|
+
# CORRIGIDO: Usar serviço interno
|
|
2561
|
+
customer_types_result = await _svc_get_customer_types()
|
|
1699
2562
|
if customer_types_result["success"]:
|
|
2563
|
+
data = customer_types_result["data"]
|
|
2564
|
+
customer_types = data.get("results", []) if isinstance(data, dict) else data
|
|
2565
|
+
|
|
1700
2566
|
dashboard_data["customer_types"] = {
|
|
1701
2567
|
"available": True,
|
|
1702
|
-
"count": len(
|
|
2568
|
+
"count": len(customer_types)
|
|
1703
2569
|
}
|
|
1704
2570
|
else:
|
|
1705
2571
|
dashboard_data["customer_types"] = {"available": False}
|
|
@@ -1737,6 +2603,21 @@ def add(a: int, b: int) -> int:
|
|
|
1737
2603
|
return a + b
|
|
1738
2604
|
|
|
1739
2605
|
|
|
2606
|
+
def _mask(s: str) -> str:
|
|
2607
|
+
"""Mascara dados sensíveis mantendo apenas o início e fim"""
|
|
2608
|
+
if not s:
|
|
2609
|
+
return None
|
|
2610
|
+
if len(s) == 1:
|
|
2611
|
+
return s + "*"
|
|
2612
|
+
if len(s) == 2:
|
|
2613
|
+
return s
|
|
2614
|
+
if len(s) <= 4:
|
|
2615
|
+
return s[:2] + "*" * (len(s) - 2)
|
|
2616
|
+
# Para strings > 4: usar no máximo 4 asteriscos no meio
|
|
2617
|
+
middle_asterisks = min(4, len(s) - 4)
|
|
2618
|
+
return s[:2] + "*" * middle_asterisks + s[-2:]
|
|
2619
|
+
|
|
2620
|
+
|
|
1740
2621
|
def _get_auth_info_internal() -> Dict:
|
|
1741
2622
|
"""Função interna para verificar configuração de autenticação"""
|
|
1742
2623
|
if SIENGE_API_KEY and SIENGE_API_KEY != "sua_api_key_aqui":
|
|
@@ -1747,7 +2628,7 @@ def _get_auth_info_internal() -> Dict:
|
|
|
1747
2628
|
"configured": True,
|
|
1748
2629
|
"base_url": SIENGE_BASE_URL,
|
|
1749
2630
|
"subdomain": SIENGE_SUBDOMAIN,
|
|
1750
|
-
"username": SIENGE_USERNAME,
|
|
2631
|
+
"username": _mask(SIENGE_USERNAME) if SIENGE_USERNAME else None,
|
|
1751
2632
|
}
|
|
1752
2633
|
else:
|
|
1753
2634
|
return {
|