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 +52 -0
- sienge/auth.py +23 -0
- sienge/client.py +146 -0
- sienge/endpoints/__init__.py +27 -0
- sienge/endpoints/base.py +187 -0
- sienge/endpoints/bulk.py +264 -0
- sienge/endpoints/comercial.py +237 -0
- sienge/endpoints/contabilidade.py +165 -0
- sienge/endpoints/credores.py +75 -0
- sienge/endpoints/engenharia.py +200 -0
- sienge/endpoints/financeiro.py +361 -0
- sienge/endpoints/patrimonio.py +57 -0
- sienge/endpoints/suprimentos.py +280 -0
- sienge/endpoints/tabelas.py +121 -0
- sienge/endpoints/webhooks.py +52 -0
- sienge/exceptions.py +40 -0
- sienge/models/__init__.py +17 -0
- sienge/models/comercial.py +103 -0
- sienge/models/credores.py +50 -0
- sienge/models/financeiro.py +179 -0
- sienge/models/obra.py +76 -0
- sienge/models/suprimentos.py +137 -0
- sienge/rate_limiter.py +84 -0
- sienge/utils.py +118 -0
- sienge_python-0.1.0.dist-info/METADATA +208 -0
- sienge_python-0.1.0.dist-info/RECORD +28 -0
- sienge_python-0.1.0.dist-info/WHEEL +4 -0
- sienge_python-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+
]
|
sienge/endpoints/base.py
ADDED
|
@@ -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)
|
sienge/endpoints/bulk.py
ADDED
|
@@ -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 []
|