sienge-python 0.1.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.
sienge/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """
2
+ sienge — Cliente Python para a API REST do Sienge (ERP de construcao civil).
3
+
4
+ Primeiro wrapper Python do mundo para o Sienge.
5
+
6
+ Uso:
7
+ from sienge import SiengeClient
8
+
9
+ client = SiengeClient("conin", "usuario", "senha")
10
+
11
+ # Engenharia
12
+ obras = client.engenharia.list_obras()
13
+ progresso = client.engenharia.get_progresso(123)
14
+
15
+ # Financeiro
16
+ titulos = client.financeiro.list_titulos()
17
+ centros = client.financeiro.list_centros_custo()
18
+
19
+ # Suprimentos
20
+ pedidos = client.suprimentos.list_pedidos(building_id=123)
21
+
22
+ # Credores
23
+ fornecedores = client.credores.list_credores()
24
+
25
+ # Contabilidade
26
+ lancamentos = client.contabilidade.list_lancamentos(company_id=1)
27
+
28
+ # Bulk (plano Ultimate)
29
+ dados = client.bulk.export_income("2026-01-01")
30
+ """
31
+
32
+ __version__ = "0.1.0"
33
+
34
+ from .client import SiengeClient
35
+ from .exceptions import (
36
+ SiengeError,
37
+ AuthError,
38
+ NotFoundError,
39
+ RateLimitError,
40
+ MaintenanceError,
41
+ ValidationError,
42
+ )
43
+
44
+ __all__ = [
45
+ "SiengeClient",
46
+ "SiengeError",
47
+ "AuthError",
48
+ "NotFoundError",
49
+ "RateLimitError",
50
+ "MaintenanceError",
51
+ "ValidationError",
52
+ ]
sienge/auth.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ sienge.auth — Autenticacao HTTP Basic Auth para a API Sienge.
3
+ """
4
+
5
+ from requests.auth import HTTPBasicAuth
6
+
7
+
8
+ class SiengeAuth:
9
+ """Gerencia credenciais e autenticacao para a API Sienge."""
10
+
11
+ def __init__(self, username: str, password: str):
12
+ if not username or not password:
13
+ raise ValueError("Username e password sao obrigatorios para autenticacao no Sienge.")
14
+ self._username = username
15
+ self._password = password
16
+
17
+ def get_auth(self) -> HTTPBasicAuth:
18
+ """Retorna objeto HTTPBasicAuth para uso com requests."""
19
+ return HTTPBasicAuth(self._username, self._password)
20
+
21
+ @property
22
+ def username(self) -> str:
23
+ return self._username
sienge/client.py ADDED
@@ -0,0 +1,146 @@
1
+ """
2
+ sienge.client — Cliente principal para a API REST do Sienge.
3
+
4
+ Uso:
5
+ from sienge import SiengeClient
6
+
7
+ client = SiengeClient("conin", "usuario", "senha")
8
+ obras = client.engenharia.list_obras()
9
+ titulos = client.financeiro.list_titulos()
10
+ """
11
+
12
+ import logging
13
+ import os
14
+
15
+ import requests
16
+
17
+ from .auth import SiengeAuth
18
+ from .rate_limiter import get_rest_limiter, get_bulk_limiter, RateLimiter
19
+ from .endpoints.engenharia import EngenhariaEndpoints
20
+ from .endpoints.financeiro import FinanceiroEndpoints
21
+ from .endpoints.suprimentos import SuprimentosEndpoints
22
+ from .endpoints.comercial import ComercialEndpoints
23
+ from .endpoints.contabilidade import ContabilidadeEndpoints
24
+ from .endpoints.credores import CredoresEndpoints
25
+ from .endpoints.bulk import BulkEndpoints
26
+ from .endpoints.patrimonio import PatrimonioEndpoints
27
+ from .endpoints.webhooks import WebhooksEndpoints
28
+ from .endpoints.tabelas import TabelasEndpoints
29
+
30
+ logger = logging.getLogger("sienge")
31
+
32
+ DEFAULT_TIMEOUT = 30
33
+ DEFAULT_MAX_RETRIES = 3
34
+
35
+
36
+ class SiengeClient:
37
+ """Cliente para a API REST do Sienge.
38
+
39
+ Uso basico:
40
+ client = SiengeClient("conin", "usuario", "senha")
41
+ obras = client.engenharia.list_obras()
42
+
43
+ Ou via variaveis de ambiente:
44
+ # Defina SIENGE_SUBDOMAIN, SIENGE_USERNAME, SIENGE_PASSWORD
45
+ client = SiengeClient.from_env()
46
+
47
+ Args:
48
+ subdomain: Subdominio da empresa no Sienge (ex: "conin").
49
+ username: Usuario de API (criado no painel Sienge).
50
+ password: Senha do usuario de API.
51
+ timeout: Timeout em segundos para cada requisicao.
52
+ max_retries: Numero maximo de retentativas em erro transiente.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ subdomain: str,
58
+ username: str,
59
+ password: str,
60
+ timeout: int = DEFAULT_TIMEOUT,
61
+ max_retries: int = DEFAULT_MAX_RETRIES,
62
+ ):
63
+ self._subdomain = subdomain
64
+ self._auth = SiengeAuth(username, password)
65
+ self._timeout = timeout
66
+ self._max_retries = max_retries
67
+
68
+ # URLs
69
+ self._base_url = f"https://api.sienge.com.br/{subdomain}/public/api/v1"
70
+ self._bulk_url = f"https://api.sienge.com.br/{subdomain}/public/api/bulk-data/v1"
71
+
72
+ # Session com auth persistente
73
+ self._session = requests.Session()
74
+ self._session.auth = self._auth.get_auth()
75
+ self._session.headers.update({"Accept": "application/json"})
76
+
77
+ # Rate limiters
78
+ self._rest_limiter: RateLimiter = get_rest_limiter()
79
+ self._bulk_limiter: RateLimiter = get_bulk_limiter()
80
+
81
+ # Endpoint groups
82
+ self.engenharia = EngenhariaEndpoints(self)
83
+ self.financeiro = FinanceiroEndpoints(self)
84
+ self.suprimentos = SuprimentosEndpoints(self)
85
+ self.comercial = ComercialEndpoints(self)
86
+ self.contabilidade = ContabilidadeEndpoints(self)
87
+ self.credores = CredoresEndpoints(self)
88
+ self.bulk = BulkEndpoints(self)
89
+ self.patrimonio = PatrimonioEndpoints(self)
90
+ self.webhooks = WebhooksEndpoints(self)
91
+ self.tabelas = TabelasEndpoints(self)
92
+
93
+ logger.info("SiengeClient inicializado para '%s' (user: %s)", subdomain, username)
94
+
95
+ @classmethod
96
+ def from_env(
97
+ cls,
98
+ subdomain_var: str = "SIENGE_SUBDOMAIN",
99
+ username_var: str = "SIENGE_USERNAME",
100
+ password_var: str = "SIENGE_PASSWORD",
101
+ **kwargs,
102
+ ) -> "SiengeClient":
103
+ """Cria cliente a partir de variaveis de ambiente.
104
+
105
+ Args:
106
+ subdomain_var: Nome da env var para subdominio.
107
+ username_var: Nome da env var para usuario.
108
+ password_var: Nome da env var para senha.
109
+ **kwargs: Argumentos extras passados ao construtor.
110
+
111
+ Returns:
112
+ SiengeClient configurado.
113
+
114
+ Raises:
115
+ ValueError: Se variaveis de ambiente estao faltando.
116
+ """
117
+ subdomain = os.getenv(subdomain_var, "")
118
+ username = os.getenv(username_var, "")
119
+ password = os.getenv(password_var, "")
120
+
121
+ if not subdomain:
122
+ raise ValueError(f"Variavel de ambiente '{subdomain_var}' nao definida.")
123
+ if not username:
124
+ raise ValueError(f"Variavel de ambiente '{username_var}' nao definida.")
125
+ if not password:
126
+ raise ValueError(f"Variavel de ambiente '{password_var}' nao definida.")
127
+
128
+ return cls(subdomain, username, password, **kwargs)
129
+
130
+ @property
131
+ def subdomain(self) -> str:
132
+ """Subdominio da empresa no Sienge."""
133
+ return self._subdomain
134
+
135
+ @property
136
+ def base_url(self) -> str:
137
+ """URL base da API REST."""
138
+ return self._base_url
139
+
140
+ @property
141
+ def bulk_url(self) -> str:
142
+ """URL base da API Bulk."""
143
+ return self._bulk_url
144
+
145
+ def __repr__(self) -> str:
146
+ return f"SiengeClient(subdomain='{self._subdomain}', user='{self._auth.username}')"
@@ -0,0 +1,27 @@
1
+ """
2
+ sienge.endpoints — Modulos de endpoints agrupados por area funcional.
3
+ """
4
+
5
+ from .engenharia import EngenhariaEndpoints
6
+ from .financeiro import FinanceiroEndpoints
7
+ from .suprimentos import SuprimentosEndpoints
8
+ from .comercial import ComercialEndpoints
9
+ from .contabilidade import ContabilidadeEndpoints
10
+ from .credores import CredoresEndpoints
11
+ from .bulk import BulkEndpoints
12
+ from .patrimonio import PatrimonioEndpoints
13
+ from .webhooks import WebhooksEndpoints
14
+ from .tabelas import TabelasEndpoints
15
+
16
+ __all__ = [
17
+ "EngenhariaEndpoints",
18
+ "FinanceiroEndpoints",
19
+ "SuprimentosEndpoints",
20
+ "ComercialEndpoints",
21
+ "ContabilidadeEndpoints",
22
+ "CredoresEndpoints",
23
+ "BulkEndpoints",
24
+ "PatrimonioEndpoints",
25
+ "WebhooksEndpoints",
26
+ "TabelasEndpoints",
27
+ ]
@@ -0,0 +1,187 @@
1
+ """
2
+ sienge.endpoints.base — Classe base para grupos de endpoints.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ import requests
11
+
12
+ from ..exceptions import (
13
+ AuthError,
14
+ MaintenanceError,
15
+ NotFoundError,
16
+ RateLimitError,
17
+ SiengeError,
18
+ ValidationError,
19
+ )
20
+ from ..rate_limiter import RateLimiter
21
+ from ..utils import retry_with_backoff
22
+
23
+ if TYPE_CHECKING:
24
+ from ..client import SiengeClient
25
+
26
+ logger = logging.getLogger("sienge")
27
+
28
+
29
+ class BaseEndpoints:
30
+ """Classe base que fornece metodos HTTP autenticados e com rate limiting."""
31
+
32
+ def __init__(self, client: "SiengeClient"):
33
+ self._client = client
34
+
35
+ @property
36
+ def _base_url(self) -> str:
37
+ return self._client._base_url
38
+
39
+ @property
40
+ def _bulk_url(self) -> str:
41
+ return self._client._bulk_url
42
+
43
+ @property
44
+ def _session(self) -> requests.Session:
45
+ return self._client._session
46
+
47
+ @property
48
+ def _rest_limiter(self) -> RateLimiter:
49
+ return self._client._rest_limiter
50
+
51
+ @property
52
+ def _bulk_limiter(self) -> RateLimiter:
53
+ return self._client._bulk_limiter
54
+
55
+ @property
56
+ def _timeout(self) -> int:
57
+ return self._client._timeout
58
+
59
+ @property
60
+ def _max_retries(self) -> int:
61
+ return self._client._max_retries
62
+
63
+ def _request(
64
+ self,
65
+ method: str,
66
+ path: str,
67
+ params: dict[str, Any] | None = None,
68
+ json_body: dict[str, Any] | None = None,
69
+ is_bulk: bool = False,
70
+ ) -> dict:
71
+ """Faz requisicao autenticada com rate limiting e retry.
72
+
73
+ Args:
74
+ method: HTTP method (GET, POST, PUT, DELETE).
75
+ path: Path relativo (ex: "/bills").
76
+ params: Query parameters.
77
+ json_body: Body JSON para POST/PUT.
78
+ is_bulk: Se True, usa bulk URL e bulk rate limiter.
79
+
80
+ Returns:
81
+ Resposta JSON como dict.
82
+
83
+ Raises:
84
+ SiengeError: Em caso de erro.
85
+ """
86
+ base = self._bulk_url if is_bulk else self._base_url
87
+ url = f"{base}{path}"
88
+ limiter = self._bulk_limiter if is_bulk else self._rest_limiter
89
+
90
+ def _do_request() -> dict:
91
+ limiter.acquire()
92
+ resp = self._session.request(
93
+ method=method,
94
+ url=url,
95
+ params=params,
96
+ json=json_body,
97
+ timeout=self._timeout,
98
+ )
99
+ return self._handle_response(resp, method, url)
100
+
101
+ return retry_with_backoff(
102
+ _do_request,
103
+ max_retries=self._max_retries,
104
+ retryable_exceptions=(
105
+ RateLimitError, MaintenanceError,
106
+ requests.exceptions.ConnectionError,
107
+ requests.exceptions.Timeout,
108
+ ),
109
+ )
110
+
111
+ def _handle_response(self, resp: requests.Response, method: str, url: str) -> dict:
112
+ """Trata status codes da resposta."""
113
+ if resp.status_code == 200:
114
+ if not resp.content:
115
+ return {}
116
+ try:
117
+ return resp.json()
118
+ except ValueError:
119
+ return {"_raw": resp.text}
120
+
121
+ if resp.status_code == 201:
122
+ try:
123
+ return resp.json()
124
+ except ValueError:
125
+ return {"_created": True}
126
+
127
+ if resp.status_code == 204:
128
+ return {"_no_content": True}
129
+
130
+ body = resp.text[:500] if resp.text else ""
131
+
132
+ if resp.status_code == 400:
133
+ raise ValidationError(
134
+ f"Parametros invalidos: {body}",
135
+ status_code=400,
136
+ response_body=body,
137
+ )
138
+
139
+ if resp.status_code == 401:
140
+ raise AuthError(
141
+ f"Nao autorizado. Verifique credenciais ou libere o recurso no painel Sienge. "
142
+ f"URL: {url}",
143
+ status_code=401,
144
+ response_body=body,
145
+ )
146
+
147
+ if resp.status_code == 404:
148
+ raise NotFoundError(
149
+ f"Recurso nao encontrado: {url}",
150
+ status_code=404,
151
+ response_body=body,
152
+ )
153
+
154
+ if resp.status_code == 429:
155
+ raise RateLimitError(
156
+ f"Rate limit atingido para {url}",
157
+ retry_after=60,
158
+ response_body=body,
159
+ )
160
+
161
+ if resp.status_code == 503:
162
+ raise MaintenanceError(
163
+ f"Sienge em manutencao (00:00-06:30 UTC). URL: {url}",
164
+ status_code=503,
165
+ response_body=body,
166
+ )
167
+
168
+ raise SiengeError(
169
+ f"Erro HTTP {resp.status_code} em {method} {url}: {body}",
170
+ status_code=resp.status_code,
171
+ response_body=body,
172
+ )
173
+
174
+ def _get(self, path: str, params: dict[str, Any] | None = None, is_bulk: bool = False) -> dict:
175
+ return self._request("GET", path, params=params, is_bulk=is_bulk)
176
+
177
+ def _post(self, path: str, json_body: dict[str, Any] | None = None, params: dict[str, Any] | None = None) -> dict:
178
+ return self._request("POST", path, params=params, json_body=json_body)
179
+
180
+ def _put(self, path: str, json_body: dict[str, Any] | None = None) -> dict:
181
+ return self._request("PUT", path, json_body=json_body)
182
+
183
+ def _patch(self, path: str, json_body: dict[str, Any] | None = None) -> dict:
184
+ return self._request("PATCH", path, json_body=json_body)
185
+
186
+ def _delete(self, path: str) -> dict:
187
+ return self._request("DELETE", path)
@@ -0,0 +1,264 @@
1
+ """
2
+ sienge.endpoints.bulk — Endpoints de Bulk Data (exportacao em massa).
3
+
4
+ ENDPOINTS BULK CONFIRMADOS (auditoria 2026-03-14):
5
+ ✅ /income — parcelas a receber (startDate, endDate, selectionType=D)
6
+ ✅ /outcome — parcelas a pagar (startDate, endDate, selectionType=D, correctionIndexerId, correctionDate)
7
+ ✅ /bank-movement — movimentos caixa/banco (startDate, endDate) — 784 registros
8
+ ✅ /purchase-quotations — cotacoes (startDate, endDate) — 75 itens
9
+ ✅ /customer-extract-history — extrato cliente
10
+ ✅ /customer-debt-balance — saldo devedor
11
+ ✅ /defaulters-receivable-bills — inadimplentes
12
+ ✅ /building/resources — insumos da obra
13
+ ✅ /business-budget — orcamento empresarial
14
+ ✅ /accountancy/accountBalance — saldos contabeis empresa
15
+ ✅ /accountancy/accountCostCenterBalance — saldos contabeis CC
16
+ ✅ /invoice-itens — itens notas fiscais
17
+ ❌ /payable-bills — 404
18
+ ❌ /building-cost-estimations — 404
19
+ ❌ /cash-bank-movements — 404 (path correto: /bank-movement)
20
+ ❌ /invoices — 404 (path correto: /invoice-itens)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from .base import BaseEndpoints
26
+
27
+
28
+ class BulkEndpoints(BaseEndpoints):
29
+ """Endpoints de Bulk Data para exportacao em massa."""
30
+
31
+ def export_income(self, start_date: str, end_date: str, selection_type: str = "D") -> list[dict]:
32
+ """Exporta parcelas a receber.
33
+
34
+ Args:
35
+ start_date: Data inicio (YYYY-MM-DD).
36
+ end_date: Data fim (YYYY-MM-DD).
37
+ selection_type: Tipo selecao — I(ssue), P(ay), D(ue), B(ill), C.
38
+
39
+ Returns:
40
+ Lista de dicts com dados.
41
+ """
42
+ data = self._get("/income", params={
43
+ "startDate": start_date,
44
+ "endDate": end_date,
45
+ "selectionType": selection_type,
46
+ }, is_bulk=True)
47
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
48
+ return result if isinstance(result, list) else []
49
+
50
+ def export_outcome(self, start_date: str, end_date: str, selection_type: str = "D",
51
+ correction_indexer_id: int = 1, correction_date: str = "") -> list[dict]:
52
+ """Exporta parcelas a pagar.
53
+
54
+ Args:
55
+ start_date: Data inicio (YYYY-MM-DD).
56
+ end_date: Data fim (YYYY-MM-DD).
57
+ selection_type: Tipo selecao — I(ssue), P(ay), D(ue).
58
+ correction_indexer_id: ID do indexador de correcao.
59
+ correction_date: Data de correcao (YYYY-MM-DD).
60
+
61
+ Returns:
62
+ Lista de dicts.
63
+ """
64
+ if not correction_date:
65
+ correction_date = end_date
66
+ data = self._get("/outcome", params={
67
+ "startDate": start_date,
68
+ "endDate": end_date,
69
+ "selectionType": selection_type,
70
+ "correctionIndexerId": correction_indexer_id,
71
+ "correctionDate": correction_date,
72
+ }, is_bulk=True)
73
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
74
+ return result if isinstance(result, list) else []
75
+
76
+ def export_bank_movements(self, start_date: str, end_date: str) -> list[dict]:
77
+ """Exporta movimentos de caixa e bancos.
78
+
79
+ NOTA: Path correto e /bank-movement (NAO /cash-bank-movements).
80
+
81
+ Args:
82
+ start_date: Data inicio (YYYY-MM-DD).
83
+ end_date: Data fim (YYYY-MM-DD).
84
+
85
+ Returns:
86
+ Lista de dicts com movimentos (784 na CONIN em 7 dias).
87
+ """
88
+ data = self._get("/bank-movement", params={
89
+ "startDate": start_date,
90
+ "endDate": end_date,
91
+ }, is_bulk=True)
92
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
93
+ return result if isinstance(result, list) else []
94
+
95
+ def export_purchase_quotations(self, start_date: str, end_date: str) -> list[dict]:
96
+ """Exporta cotacoes de precos em massa.
97
+
98
+ Args:
99
+ start_date: Data inicio (YYYY-MM-DD).
100
+ end_date: Data fim (YYYY-MM-DD).
101
+
102
+ Returns:
103
+ Lista de dicts (75 na CONIN).
104
+ """
105
+ data = self._get("/purchase-quotations", params={
106
+ "startDate": start_date,
107
+ "endDate": end_date,
108
+ }, is_bulk=True)
109
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
110
+ return result if isinstance(result, list) else []
111
+
112
+ def export_customer_extract(self, start_due_date: str, end_due_date: str) -> list[dict]:
113
+ """Exporta historico de extrato de clientes.
114
+
115
+ Args:
116
+ start_due_date: Data inicio vencimento (YYYY-MM-DD).
117
+ end_due_date: Data fim vencimento (YYYY-MM-DD).
118
+
119
+ Returns:
120
+ Lista de dicts.
121
+ """
122
+ data = self._get("/customer-extract-history", params={
123
+ "startDueDate": start_due_date,
124
+ "endDueDate": end_due_date,
125
+ }, is_bulk=True)
126
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
127
+ return result if isinstance(result, list) else []
128
+
129
+ def export_customer_debt(self, start_due_date: str, end_due_date: str) -> list[dict]:
130
+ """Exporta saldo devedor de clientes.
131
+
132
+ Args:
133
+ start_due_date: Data inicio vencimento (YYYY-MM-DD).
134
+ end_due_date: Data fim vencimento (YYYY-MM-DD).
135
+
136
+ Returns:
137
+ Lista de dicts.
138
+ """
139
+ data = self._get("/customer-debt-balance", params={
140
+ "startDueDate": start_due_date,
141
+ "endDueDate": end_due_date,
142
+ }, is_bulk=True)
143
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
144
+ return result if isinstance(result, list) else []
145
+
146
+ def export_defaulters(self, company_id: int) -> list[dict]:
147
+ """Exporta titulos de inadimplentes.
148
+
149
+ Args:
150
+ company_id: ID da empresa (OBRIGATORIO).
151
+
152
+ Returns:
153
+ Lista de dicts.
154
+ """
155
+ data = self._get("/defaulters-receivable-bills", params={
156
+ "companyId": company_id,
157
+ }, is_bulk=True)
158
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
159
+ return result if isinstance(result, list) else []
160
+
161
+ def export_building_resources(self, building_id: int, start_date: str, end_date: str) -> list[dict]:
162
+ """Exporta insumos de uma obra.
163
+
164
+ Args:
165
+ building_id: ID do empreendimento (OBRIGATORIO).
166
+ start_date: Data inicio (YYYY-MM-DD).
167
+ end_date: Data fim (YYYY-MM-DD).
168
+
169
+ Returns:
170
+ Lista de dicts.
171
+ """
172
+ data = self._get("/building/resources", params={
173
+ "buildingId": building_id,
174
+ "startDate": start_date,
175
+ "endDate": end_date,
176
+ }, is_bulk=True)
177
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
178
+ return result if isinstance(result, list) else []
179
+
180
+ def export_business_budget(self, start_date: str, end_date: str) -> list[dict]:
181
+ """Exporta orcamento empresarial.
182
+
183
+ Args:
184
+ start_date: Data inicio (YYYY-MM-DD).
185
+ end_date: Data fim (YYYY-MM-DD).
186
+
187
+ Returns:
188
+ Lista de dicts.
189
+ """
190
+ data = self._get("/business-budget", params={
191
+ "startDate": start_date,
192
+ "endDate": end_date,
193
+ }, is_bulk=True)
194
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
195
+ return result if isinstance(result, list) else []
196
+
197
+ def export_account_balance(self, start_date: str, end_date: str) -> list[dict]:
198
+ """Exporta saldos contabeis por empresa.
199
+
200
+ Args:
201
+ start_date: Data inicio (YYYY-MM-DD).
202
+ end_date: Data fim (YYYY-MM-DD).
203
+
204
+ Returns:
205
+ Lista de dicts.
206
+ """
207
+ data = self._get("/accountancy/accountBalance", params={
208
+ "startDate": start_date,
209
+ "endDate": end_date,
210
+ }, is_bulk=True)
211
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
212
+ return result if isinstance(result, list) else []
213
+
214
+ def export_account_cc_balance(self, start_date: str, end_date: str) -> list[dict]:
215
+ """Exporta saldos contabeis por centro de custo.
216
+
217
+ Args:
218
+ start_date: Data inicio (YYYY-MM-DD).
219
+ end_date: Data fim (YYYY-MM-DD).
220
+
221
+ Returns:
222
+ Lista de dicts.
223
+ """
224
+ data = self._get("/accountancy/accountCostCenterBalance", params={
225
+ "startDate": start_date,
226
+ "endDate": end_date,
227
+ }, is_bulk=True)
228
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
229
+ return result if isinstance(result, list) else []
230
+
231
+ def export_invoice_items(self, company_id: int, start_date: str, end_date: str) -> list[dict]:
232
+ """Exporta itens de notas fiscais.
233
+
234
+ NOTA: Path correto e /invoice-itens (NAO /invoices).
235
+
236
+ Args:
237
+ company_id: ID da empresa (OBRIGATORIO).
238
+ start_date: Data inicio (YYYY-MM-DD).
239
+ end_date: Data fim (YYYY-MM-DD).
240
+
241
+ Returns:
242
+ Lista de dicts.
243
+ """
244
+ data = self._get("/invoice-itens", params={
245
+ "companyId": company_id,
246
+ "startDate": start_date,
247
+ "endDate": end_date,
248
+ }, is_bulk=True)
249
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
250
+ return result if isinstance(result, list) else []
251
+
252
+ def export(self, resource: str, params: dict | None = None) -> list[dict]:
253
+ """Exporta qualquer recurso bulk generico.
254
+
255
+ Args:
256
+ resource: Nome do recurso (ex: "income", "bank-movement").
257
+ params: Query params.
258
+
259
+ Returns:
260
+ Lista de dicts.
261
+ """
262
+ data = self._get(f"/{resource}", params=params, is_bulk=True)
263
+ result = data.get("data", data.get("results", data if isinstance(data, list) else []))
264
+ return result if isinstance(result, list) else []