clisitef-odoo 0.1.0__tar.gz
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.
- clisitef_odoo-0.1.0/PKG-INFO +262 -0
- clisitef_odoo-0.1.0/README.md +246 -0
- clisitef_odoo-0.1.0/clisitef_odoo/__init__.py +41 -0
- clisitef_odoo-0.1.0/clisitef_odoo/client.py +239 -0
- clisitef_odoo-0.1.0/clisitef_odoo/constants.py +617 -0
- clisitef_odoo-0.1.0/clisitef_odoo/exceptions.py +29 -0
- clisitef_odoo-0.1.0/clisitef_odoo/models.py +32 -0
- clisitef_odoo-0.1.0/clisitef_odoo/transaction.py +258 -0
- clisitef_odoo-0.1.0/clisitef_odoo.egg-info/PKG-INFO +262 -0
- clisitef_odoo-0.1.0/clisitef_odoo.egg-info/SOURCES.txt +15 -0
- clisitef_odoo-0.1.0/clisitef_odoo.egg-info/dependency_links.txt +1 -0
- clisitef_odoo-0.1.0/clisitef_odoo.egg-info/requires.txt +4 -0
- clisitef_odoo-0.1.0/clisitef_odoo.egg-info/top_level.txt +1 -0
- clisitef_odoo-0.1.0/pyproject.toml +32 -0
- clisitef_odoo-0.1.0/setup.cfg +4 -0
- clisitef_odoo-0.1.0/tests/test_client.py +120 -0
- clisitef_odoo-0.1.0/tests/test_transaction.py +128 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clisitef-odoo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cliente Python para o AgenteCliSiTef (TEF), para uso em integracoes com Odoo
|
|
5
|
+
Author-email: Sadson Diego <sadsondiego@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/GrupoZenir/clisitef_odoo
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: requests>=2.20
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
|
|
17
|
+
# clisitef_odoo
|
|
18
|
+
|
|
19
|
+
Cliente Python para o **AgenteCliSiTef** (a API REST local que expõe o TEF
|
|
20
|
+
CliSiTef/Software Express), construído a partir da lógica de
|
|
21
|
+
`agenteCliSiTef.js` deste projeto. O objetivo é permitir que um backend em
|
|
22
|
+
Python — em particular o Odoo — conduza transações de TEF (crédito, débito
|
|
23
|
+
etc.) sem depender de um navegador/DOM.
|
|
24
|
+
|
|
25
|
+
> O código em `agenteCliSiTef.js` e os HTMLs de exemplo (`index.html`,
|
|
26
|
+
> `venda_com_sessao.html`, `sessao.html`) foram usados como especificação do
|
|
27
|
+
> protocolo. Esta lib reimplementa o mesmo fluxo (`/state`, `/session`,
|
|
28
|
+
> `/startTransaction`, `/continueTransaction`, `/finishTransaction`,
|
|
29
|
+
> `/pinpad/*`) em Python puro, trocando os `onclick`/DOM por *callbacks*.
|
|
30
|
+
|
|
31
|
+
## Instalação
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e /caminho/para/clisitef_odoo
|
|
35
|
+
# ou, publicando em um índice interno:
|
|
36
|
+
pip install clisitef-odoo
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Dependência única: `requests`.
|
|
40
|
+
|
|
41
|
+
Para rodar os testes:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install -e ".[dev]"
|
|
45
|
+
pytest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Conceitos e mapeamento com o AgenteCliSiTef
|
|
49
|
+
|
|
50
|
+
| Endpoint AgenteCliSiTef | Método em `AgenteCliSiTefClient` |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `GET /state` | `get_state()` |
|
|
53
|
+
| `POST /getVersion` | `get_version()` |
|
|
54
|
+
| `POST /session` | `create_session()` |
|
|
55
|
+
| `GET /session` | `get_session()` |
|
|
56
|
+
| `DELETE /session` | `delete_session()` |
|
|
57
|
+
| `POST /startTransaction` | `start_transaction()` |
|
|
58
|
+
| `POST /continueTransaction` | `continue_transaction()` |
|
|
59
|
+
| `POST /finishTransaction` | `finish_transaction()` |
|
|
60
|
+
| `POST /pinpad/open` | `pinpad_open()` |
|
|
61
|
+
| `POST /pinpad/close` | `pinpad_close()` |
|
|
62
|
+
| `POST /pinpad/isPresent` | `pinpad_is_present()` |
|
|
63
|
+
| `POST /pinpad/setDisplayMessage` | `pinpad_set_display_message()` |
|
|
64
|
+
| `POST /pinpad/readYesNo` | `pinpad_read_yes_no()` |
|
|
65
|
+
|
|
66
|
+
`AgenteCliSiTefClient` é *stateless* em relação ao fluxo: cada chamada apenas
|
|
67
|
+
faz o HTTP e valida `serviceStatus`. Quem interpreta o fluxo interativo da
|
|
68
|
+
CliSiTef (`clisitefStatus`/`commandId`) é a classe `TefTransaction`.
|
|
69
|
+
|
|
70
|
+
### Dois modos de sessão (igual ao JS original)
|
|
71
|
+
|
|
72
|
+
- **Sessão efêmera** (`inicio(1, funcao)` no JS): passe `sitef_ip`,
|
|
73
|
+
`store_id`, `terminal_id` diretamente para `start_transaction`/`run` — o
|
|
74
|
+
agente cria e destrói a sessão internamente a cada transação.
|
|
75
|
+
- **Sessão persistente** (`inicio(2, funcao)` no JS): crie a sessão uma vez
|
|
76
|
+
com `create_session(...)` e reaproveite `sessionId` em várias
|
|
77
|
+
transações (útil para caixas que ficam o turno todo conectados),
|
|
78
|
+
encerrando com `delete_session()` ao final.
|
|
79
|
+
|
|
80
|
+
### O loop interativo (`TefTransaction`)
|
|
81
|
+
|
|
82
|
+
A CliSiTef conduz a transação em passos: a cada `continueTransaction`, ela
|
|
83
|
+
retorna um `commandId` que diz o que fazer a seguir. `TefTransaction.run()`
|
|
84
|
+
implementa esse loop (equivalente à função `continua()` do JS) e traduz cada
|
|
85
|
+
`commandId` em uma chamada de callback:
|
|
86
|
+
|
|
87
|
+
| commandId | Significado | Callback |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `0` | Campo capturado (ex.: cupom, NSU, etc.) | acumulado em `result.fields`; `on_receipt` para os fieldId 121/122 (cupom estabelecimento/cliente) |
|
|
90
|
+
| `1,2,3,4,15` | Atualiza mensagem de status | `on_message(texto)` |
|
|
91
|
+
| `11,12,13,14,16` | Limpa mensagem de status | `on_message("")` |
|
|
92
|
+
| `22` | Alerta | `on_message(texto)` |
|
|
93
|
+
| `23` | Aguardando pinpad/cliente (pode cancelar) | `on_cancel_check()` a cada `poll_interval` segundos |
|
|
94
|
+
| `20` | Pergunta Sim/Não | `on_confirm(pergunta) -> bool` |
|
|
95
|
+
| `21,30-35,38` | Coleta de dado (CPF, senha, valor, etc.) | `on_input(prompt, field_id, min_len, max_len) -> str \| None` (`None` cancela a coleta) |
|
|
96
|
+
|
|
97
|
+
Todos os callbacks têm um padrão conservador (perguntas são respondidas
|
|
98
|
+
"Não", coletas de dado são canceladas) — **implemente `on_confirm`/`on_input`
|
|
99
|
+
de acordo com a regra de negócio real antes de usar em produção**, já que a
|
|
100
|
+
resposta correta depende do que está configurado na CliSiTef para aquele
|
|
101
|
+
terminal/loja.
|
|
102
|
+
|
|
103
|
+
Ao final do loop, `run()` chama automaticamente `finishTransaction`
|
|
104
|
+
(`confirm=1` se `clisitefStatus == 0`, `confirm=0` caso contrário). Isso é
|
|
105
|
+
mais seguro que o HTML de exemplo original, que só finaliza no caminho de
|
|
106
|
+
sucesso — use `auto_finish=False` e o método `finish()` se você quiser
|
|
107
|
+
controlar isso manualmente (ex.: pedir confirmação do operador antes de
|
|
108
|
+
efetivar).
|
|
109
|
+
|
|
110
|
+
## Exemplo básico
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from clisitef_odoo import AgenteCliSiTefClient, TefTransaction, constants
|
|
114
|
+
|
|
115
|
+
client = AgenteCliSiTefClient(base_url="https://127.0.0.1/agente/clisitef")
|
|
116
|
+
|
|
117
|
+
def responder_pergunta(pergunta):
|
|
118
|
+
print("CliSiTef pergunta:", pergunta)
|
|
119
|
+
return True # confirma
|
|
120
|
+
|
|
121
|
+
def coletar_dado(prompt, field_id, min_len, max_len):
|
|
122
|
+
return input(f"{prompt} ({min_len}-{max_len} caracteres): ")
|
|
123
|
+
|
|
124
|
+
tef = TefTransaction(
|
|
125
|
+
client,
|
|
126
|
+
on_message=lambda msg: print("Status:", msg),
|
|
127
|
+
on_confirm=responder_pergunta,
|
|
128
|
+
on_input=coletar_dado,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
result = tef.run(
|
|
132
|
+
function_id=constants.FUNCTION_VENDA_CREDITO,
|
|
133
|
+
trn_amount=1000, # R$ 10,00 em centavos
|
|
134
|
+
tax_invoice_number="1234",
|
|
135
|
+
tax_invoice_date="20260714",
|
|
136
|
+
tax_invoice_time="153000",
|
|
137
|
+
cashier_operator="CAIXA",
|
|
138
|
+
sitef_ip="127.0.0.1",
|
|
139
|
+
store_id="00000000",
|
|
140
|
+
terminal_id="REST0001",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if result.success:
|
|
144
|
+
print("Aprovado! NSU/campos:", result.fields)
|
|
145
|
+
print("Cupom estabelecimento:\n", result.merchant_receipt)
|
|
146
|
+
print("Cupom cliente:\n", result.customer_receipt)
|
|
147
|
+
else:
|
|
148
|
+
print("Transação não aprovada, status CliSiTef:", result.clisitef_status)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Usando sessão persistente
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
session = client.create_session(sitef_ip="127.0.0.1", store_id="00000000", terminal_id="REST0001")
|
|
155
|
+
session_id = session["sessionId"]
|
|
156
|
+
|
|
157
|
+
result = tef.run(function_id=constants.FUNCTION_VENDA_CREDITO, trn_amount=2500, session_id=session_id)
|
|
158
|
+
|
|
159
|
+
# ... outras transações reaproveitando session_id ...
|
|
160
|
+
|
|
161
|
+
client.delete_session()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Cancelando fora do fluxo normal
|
|
165
|
+
|
|
166
|
+
Equivalente ao botão "Finaliza estornando" dos exemplos HTML — útil quando a
|
|
167
|
+
aplicação perdeu o estado em memória (ex.: crash) mas precisa desfazer uma
|
|
168
|
+
transação pendente na CliSiTef:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
tef.finish_out_of_band(
|
|
172
|
+
confirm=0,
|
|
173
|
+
sitef_ip="127.0.0.1", store_id="00000000", terminal_id="REST0001",
|
|
174
|
+
tax_invoice_number="1234", tax_invoice_date="20260714", tax_invoice_time="153000",
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Funções de pinpad
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
client.pinpad_open(session_id)
|
|
182
|
+
client.pinpad_set_display_message(session_id, "Aguarde...", persistent=True)
|
|
183
|
+
presente = client.pinpad_is_present(session_id)["clisitefStatus"] == 1
|
|
184
|
+
resposta = client.pinpad_read_yes_no(session_id, "Aceita o valor?") # clisitefStatus: 0=Anula, 1=Entra
|
|
185
|
+
client.pinpad_close(session_id)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Tratamento de erros
|
|
189
|
+
|
|
190
|
+
- `AgentServiceError`: o **agente** (não a CliSiTef) reportou um problema —
|
|
191
|
+
`serviceStatus != 0` (agente ocupado, sessão inválida, parâmetros
|
|
192
|
+
incorretos). Contém `.service_status` e `.service_message`.
|
|
193
|
+
- `ClisitefProtocolError`: resposta em formato inesperado (ex.: sem
|
|
194
|
+
`commandId` quando deveria ter).
|
|
195
|
+
- Falhas de rede (`requests.RequestException`) propagam normalmente — trate
|
|
196
|
+
como indisponibilidade do agente (ex.: serviço não iniciado na máquina do
|
|
197
|
+
caixa).
|
|
198
|
+
|
|
199
|
+
`result.success` (`clisitef_status == 0`) indica que a transação foi
|
|
200
|
+
aprovada; qualquer outro valor de `clisitef_status` (inclusive cancelamento
|
|
201
|
+
pelo operador/cliente) é considerado não sucesso e `run()` desfaz
|
|
202
|
+
automaticamente (`confirm=0`) quando `auto_finish=True`.
|
|
203
|
+
|
|
204
|
+
## Códigos de função (`functionId`)
|
|
205
|
+
|
|
206
|
+
`FUNCTION_VENDA_CREDITO = 3` está confirmado pelos exemplos deste projeto
|
|
207
|
+
(comentário "Inicia uma venda crédito" nos HTMLs). `FUNCTION_VENDA_DEBITO = 2`
|
|
208
|
+
foi confirmado no manual/contrato SiTef do terminal em uso neste projeto. Os
|
|
209
|
+
demais códigos de função (voucher, cheque, funções administrativas etc.)
|
|
210
|
+
variam conforme a versão/contrato da CliSiTef instalada — confirme com o
|
|
211
|
+
manual do fornecedor (Software Express) ou com quem configurou o SiTef antes
|
|
212
|
+
de utilizá-los em produção.
|
|
213
|
+
|
|
214
|
+
## Integração com Odoo
|
|
215
|
+
|
|
216
|
+
Veja [`examples/odoo_addon_example/`](examples/odoo_addon_example/) para um
|
|
217
|
+
esqueleto de addon do Odoo (Point of Sale) que:
|
|
218
|
+
|
|
219
|
+
1. Adiciona campos de configuração (`clisitef_base_url`, `sitef_ip`,
|
|
220
|
+
`store_id`, `terminal_id`, `function_id`) em `pos.payment.method`.
|
|
221
|
+
2. Expõe um método `clisitef_run_payment()` que roda `TefTransaction.run()`
|
|
222
|
+
inteiramente no servidor e devolve um dict serializável em JSON (sucesso,
|
|
223
|
+
status, cupons, campos capturados).
|
|
224
|
+
3. Expõe esse método via um controller JSON (`/clisitef_tef/pay`) para ser
|
|
225
|
+
chamado pela UI do PDV.
|
|
226
|
+
4. Traz um esqueleto de `PaymentInterface` em JS que apenas chama o backend
|
|
227
|
+
e trata o retorno — **a API exata do `PaymentInterface` muda entre
|
|
228
|
+
versões do Odoo** (14/16/17/18 têm assinaturas diferentes), então adapte
|
|
229
|
+
os nomes de método conforme a sua versão.
|
|
230
|
+
|
|
231
|
+
Pontos importantes para adaptar aos seus módulos:
|
|
232
|
+
|
|
233
|
+
- **Onde roda o agente**: o AgenteCliSiTef roda na máquina física do caixa
|
|
234
|
+
(pinpad conectado nela). Se o servidor Odoo for centralizado (não é a
|
|
235
|
+
mesma máquina do caixa), o controller do exemplo não vai alcançar
|
|
236
|
+
`127.0.0.1` do caixa — nesse caso, rode a lógica de pagamento em um
|
|
237
|
+
processo local no caixa (ex.: um pequeno serviço Python que expõe uma rota
|
|
238
|
+
para a UI do PDV) ao invés de no controller do servidor Odoo central.
|
|
239
|
+
- **Callbacks de negócio**: `on_confirm`/`on_input` no exemplo usam os
|
|
240
|
+
padrões conservadores da lib (recusam/cancelam). Decida como isso deve se
|
|
241
|
+
comportar no seu fluxo antes de ir para produção — por exemplo, se sua
|
|
242
|
+
CliSiTef está configurada para pedir CPF do cliente, você precisa buscar
|
|
243
|
+
esse dado do próprio pedido do Odoo e devolvê-lo via `on_input`.
|
|
244
|
+
- Este addon de exemplo **não foi testado contra uma instância real do
|
|
245
|
+
Odoo** (não há um Odoo disponível no ambiente onde esta lib foi escrita) —
|
|
246
|
+
trate-o como ponto de partida, não como módulo pronto para produção.
|
|
247
|
+
|
|
248
|
+
## Estrutura do pacote
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
clisitef_odoo/
|
|
252
|
+
client.py # AgenteCliSiTefClient - chamadas HTTP 1:1 com o agente
|
|
253
|
+
transaction.py # TefTransaction - loop interativo orientado a callbacks
|
|
254
|
+
constants.py # clisitefStatus, commandId, fieldId, functionId conhecidos
|
|
255
|
+
models.py # FieldCapture, TransactionResult
|
|
256
|
+
exceptions.py # AgentServiceError, ClisitefProtocolError, ...
|
|
257
|
+
examples/
|
|
258
|
+
odoo_addon_example/ # esqueleto de addon Odoo (POS payment)
|
|
259
|
+
tests/
|
|
260
|
+
test_client.py # testes do cliente HTTP com mocks
|
|
261
|
+
test_transaction.py # testes do loop interativo com mocks
|
|
262
|
+
```
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# clisitef_odoo
|
|
2
|
+
|
|
3
|
+
Cliente Python para o **AgenteCliSiTef** (a API REST local que expõe o TEF
|
|
4
|
+
CliSiTef/Software Express), construído a partir da lógica de
|
|
5
|
+
`agenteCliSiTef.js` deste projeto. O objetivo é permitir que um backend em
|
|
6
|
+
Python — em particular o Odoo — conduza transações de TEF (crédito, débito
|
|
7
|
+
etc.) sem depender de um navegador/DOM.
|
|
8
|
+
|
|
9
|
+
> O código em `agenteCliSiTef.js` e os HTMLs de exemplo (`index.html`,
|
|
10
|
+
> `venda_com_sessao.html`, `sessao.html`) foram usados como especificação do
|
|
11
|
+
> protocolo. Esta lib reimplementa o mesmo fluxo (`/state`, `/session`,
|
|
12
|
+
> `/startTransaction`, `/continueTransaction`, `/finishTransaction`,
|
|
13
|
+
> `/pinpad/*`) em Python puro, trocando os `onclick`/DOM por *callbacks*.
|
|
14
|
+
|
|
15
|
+
## Instalação
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e /caminho/para/clisitef_odoo
|
|
19
|
+
# ou, publicando em um índice interno:
|
|
20
|
+
pip install clisitef-odoo
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Dependência única: `requests`.
|
|
24
|
+
|
|
25
|
+
Para rodar os testes:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
pytest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Conceitos e mapeamento com o AgenteCliSiTef
|
|
33
|
+
|
|
34
|
+
| Endpoint AgenteCliSiTef | Método em `AgenteCliSiTefClient` |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `GET /state` | `get_state()` |
|
|
37
|
+
| `POST /getVersion` | `get_version()` |
|
|
38
|
+
| `POST /session` | `create_session()` |
|
|
39
|
+
| `GET /session` | `get_session()` |
|
|
40
|
+
| `DELETE /session` | `delete_session()` |
|
|
41
|
+
| `POST /startTransaction` | `start_transaction()` |
|
|
42
|
+
| `POST /continueTransaction` | `continue_transaction()` |
|
|
43
|
+
| `POST /finishTransaction` | `finish_transaction()` |
|
|
44
|
+
| `POST /pinpad/open` | `pinpad_open()` |
|
|
45
|
+
| `POST /pinpad/close` | `pinpad_close()` |
|
|
46
|
+
| `POST /pinpad/isPresent` | `pinpad_is_present()` |
|
|
47
|
+
| `POST /pinpad/setDisplayMessage` | `pinpad_set_display_message()` |
|
|
48
|
+
| `POST /pinpad/readYesNo` | `pinpad_read_yes_no()` |
|
|
49
|
+
|
|
50
|
+
`AgenteCliSiTefClient` é *stateless* em relação ao fluxo: cada chamada apenas
|
|
51
|
+
faz o HTTP e valida `serviceStatus`. Quem interpreta o fluxo interativo da
|
|
52
|
+
CliSiTef (`clisitefStatus`/`commandId`) é a classe `TefTransaction`.
|
|
53
|
+
|
|
54
|
+
### Dois modos de sessão (igual ao JS original)
|
|
55
|
+
|
|
56
|
+
- **Sessão efêmera** (`inicio(1, funcao)` no JS): passe `sitef_ip`,
|
|
57
|
+
`store_id`, `terminal_id` diretamente para `start_transaction`/`run` — o
|
|
58
|
+
agente cria e destrói a sessão internamente a cada transação.
|
|
59
|
+
- **Sessão persistente** (`inicio(2, funcao)` no JS): crie a sessão uma vez
|
|
60
|
+
com `create_session(...)` e reaproveite `sessionId` em várias
|
|
61
|
+
transações (útil para caixas que ficam o turno todo conectados),
|
|
62
|
+
encerrando com `delete_session()` ao final.
|
|
63
|
+
|
|
64
|
+
### O loop interativo (`TefTransaction`)
|
|
65
|
+
|
|
66
|
+
A CliSiTef conduz a transação em passos: a cada `continueTransaction`, ela
|
|
67
|
+
retorna um `commandId` que diz o que fazer a seguir. `TefTransaction.run()`
|
|
68
|
+
implementa esse loop (equivalente à função `continua()` do JS) e traduz cada
|
|
69
|
+
`commandId` em uma chamada de callback:
|
|
70
|
+
|
|
71
|
+
| commandId | Significado | Callback |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `0` | Campo capturado (ex.: cupom, NSU, etc.) | acumulado em `result.fields`; `on_receipt` para os fieldId 121/122 (cupom estabelecimento/cliente) |
|
|
74
|
+
| `1,2,3,4,15` | Atualiza mensagem de status | `on_message(texto)` |
|
|
75
|
+
| `11,12,13,14,16` | Limpa mensagem de status | `on_message("")` |
|
|
76
|
+
| `22` | Alerta | `on_message(texto)` |
|
|
77
|
+
| `23` | Aguardando pinpad/cliente (pode cancelar) | `on_cancel_check()` a cada `poll_interval` segundos |
|
|
78
|
+
| `20` | Pergunta Sim/Não | `on_confirm(pergunta) -> bool` |
|
|
79
|
+
| `21,30-35,38` | Coleta de dado (CPF, senha, valor, etc.) | `on_input(prompt, field_id, min_len, max_len) -> str \| None` (`None` cancela a coleta) |
|
|
80
|
+
|
|
81
|
+
Todos os callbacks têm um padrão conservador (perguntas são respondidas
|
|
82
|
+
"Não", coletas de dado são canceladas) — **implemente `on_confirm`/`on_input`
|
|
83
|
+
de acordo com a regra de negócio real antes de usar em produção**, já que a
|
|
84
|
+
resposta correta depende do que está configurado na CliSiTef para aquele
|
|
85
|
+
terminal/loja.
|
|
86
|
+
|
|
87
|
+
Ao final do loop, `run()` chama automaticamente `finishTransaction`
|
|
88
|
+
(`confirm=1` se `clisitefStatus == 0`, `confirm=0` caso contrário). Isso é
|
|
89
|
+
mais seguro que o HTML de exemplo original, que só finaliza no caminho de
|
|
90
|
+
sucesso — use `auto_finish=False` e o método `finish()` se você quiser
|
|
91
|
+
controlar isso manualmente (ex.: pedir confirmação do operador antes de
|
|
92
|
+
efetivar).
|
|
93
|
+
|
|
94
|
+
## Exemplo básico
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from clisitef_odoo import AgenteCliSiTefClient, TefTransaction, constants
|
|
98
|
+
|
|
99
|
+
client = AgenteCliSiTefClient(base_url="https://127.0.0.1/agente/clisitef")
|
|
100
|
+
|
|
101
|
+
def responder_pergunta(pergunta):
|
|
102
|
+
print("CliSiTef pergunta:", pergunta)
|
|
103
|
+
return True # confirma
|
|
104
|
+
|
|
105
|
+
def coletar_dado(prompt, field_id, min_len, max_len):
|
|
106
|
+
return input(f"{prompt} ({min_len}-{max_len} caracteres): ")
|
|
107
|
+
|
|
108
|
+
tef = TefTransaction(
|
|
109
|
+
client,
|
|
110
|
+
on_message=lambda msg: print("Status:", msg),
|
|
111
|
+
on_confirm=responder_pergunta,
|
|
112
|
+
on_input=coletar_dado,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
result = tef.run(
|
|
116
|
+
function_id=constants.FUNCTION_VENDA_CREDITO,
|
|
117
|
+
trn_amount=1000, # R$ 10,00 em centavos
|
|
118
|
+
tax_invoice_number="1234",
|
|
119
|
+
tax_invoice_date="20260714",
|
|
120
|
+
tax_invoice_time="153000",
|
|
121
|
+
cashier_operator="CAIXA",
|
|
122
|
+
sitef_ip="127.0.0.1",
|
|
123
|
+
store_id="00000000",
|
|
124
|
+
terminal_id="REST0001",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if result.success:
|
|
128
|
+
print("Aprovado! NSU/campos:", result.fields)
|
|
129
|
+
print("Cupom estabelecimento:\n", result.merchant_receipt)
|
|
130
|
+
print("Cupom cliente:\n", result.customer_receipt)
|
|
131
|
+
else:
|
|
132
|
+
print("Transação não aprovada, status CliSiTef:", result.clisitef_status)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Usando sessão persistente
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
session = client.create_session(sitef_ip="127.0.0.1", store_id="00000000", terminal_id="REST0001")
|
|
139
|
+
session_id = session["sessionId"]
|
|
140
|
+
|
|
141
|
+
result = tef.run(function_id=constants.FUNCTION_VENDA_CREDITO, trn_amount=2500, session_id=session_id)
|
|
142
|
+
|
|
143
|
+
# ... outras transações reaproveitando session_id ...
|
|
144
|
+
|
|
145
|
+
client.delete_session()
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Cancelando fora do fluxo normal
|
|
149
|
+
|
|
150
|
+
Equivalente ao botão "Finaliza estornando" dos exemplos HTML — útil quando a
|
|
151
|
+
aplicação perdeu o estado em memória (ex.: crash) mas precisa desfazer uma
|
|
152
|
+
transação pendente na CliSiTef:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
tef.finish_out_of_band(
|
|
156
|
+
confirm=0,
|
|
157
|
+
sitef_ip="127.0.0.1", store_id="00000000", terminal_id="REST0001",
|
|
158
|
+
tax_invoice_number="1234", tax_invoice_date="20260714", tax_invoice_time="153000",
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Funções de pinpad
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
client.pinpad_open(session_id)
|
|
166
|
+
client.pinpad_set_display_message(session_id, "Aguarde...", persistent=True)
|
|
167
|
+
presente = client.pinpad_is_present(session_id)["clisitefStatus"] == 1
|
|
168
|
+
resposta = client.pinpad_read_yes_no(session_id, "Aceita o valor?") # clisitefStatus: 0=Anula, 1=Entra
|
|
169
|
+
client.pinpad_close(session_id)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Tratamento de erros
|
|
173
|
+
|
|
174
|
+
- `AgentServiceError`: o **agente** (não a CliSiTef) reportou um problema —
|
|
175
|
+
`serviceStatus != 0` (agente ocupado, sessão inválida, parâmetros
|
|
176
|
+
incorretos). Contém `.service_status` e `.service_message`.
|
|
177
|
+
- `ClisitefProtocolError`: resposta em formato inesperado (ex.: sem
|
|
178
|
+
`commandId` quando deveria ter).
|
|
179
|
+
- Falhas de rede (`requests.RequestException`) propagam normalmente — trate
|
|
180
|
+
como indisponibilidade do agente (ex.: serviço não iniciado na máquina do
|
|
181
|
+
caixa).
|
|
182
|
+
|
|
183
|
+
`result.success` (`clisitef_status == 0`) indica que a transação foi
|
|
184
|
+
aprovada; qualquer outro valor de `clisitef_status` (inclusive cancelamento
|
|
185
|
+
pelo operador/cliente) é considerado não sucesso e `run()` desfaz
|
|
186
|
+
automaticamente (`confirm=0`) quando `auto_finish=True`.
|
|
187
|
+
|
|
188
|
+
## Códigos de função (`functionId`)
|
|
189
|
+
|
|
190
|
+
`FUNCTION_VENDA_CREDITO = 3` está confirmado pelos exemplos deste projeto
|
|
191
|
+
(comentário "Inicia uma venda crédito" nos HTMLs). `FUNCTION_VENDA_DEBITO = 2`
|
|
192
|
+
foi confirmado no manual/contrato SiTef do terminal em uso neste projeto. Os
|
|
193
|
+
demais códigos de função (voucher, cheque, funções administrativas etc.)
|
|
194
|
+
variam conforme a versão/contrato da CliSiTef instalada — confirme com o
|
|
195
|
+
manual do fornecedor (Software Express) ou com quem configurou o SiTef antes
|
|
196
|
+
de utilizá-los em produção.
|
|
197
|
+
|
|
198
|
+
## Integração com Odoo
|
|
199
|
+
|
|
200
|
+
Veja [`examples/odoo_addon_example/`](examples/odoo_addon_example/) para um
|
|
201
|
+
esqueleto de addon do Odoo (Point of Sale) que:
|
|
202
|
+
|
|
203
|
+
1. Adiciona campos de configuração (`clisitef_base_url`, `sitef_ip`,
|
|
204
|
+
`store_id`, `terminal_id`, `function_id`) em `pos.payment.method`.
|
|
205
|
+
2. Expõe um método `clisitef_run_payment()` que roda `TefTransaction.run()`
|
|
206
|
+
inteiramente no servidor e devolve um dict serializável em JSON (sucesso,
|
|
207
|
+
status, cupons, campos capturados).
|
|
208
|
+
3. Expõe esse método via um controller JSON (`/clisitef_tef/pay`) para ser
|
|
209
|
+
chamado pela UI do PDV.
|
|
210
|
+
4. Traz um esqueleto de `PaymentInterface` em JS que apenas chama o backend
|
|
211
|
+
e trata o retorno — **a API exata do `PaymentInterface` muda entre
|
|
212
|
+
versões do Odoo** (14/16/17/18 têm assinaturas diferentes), então adapte
|
|
213
|
+
os nomes de método conforme a sua versão.
|
|
214
|
+
|
|
215
|
+
Pontos importantes para adaptar aos seus módulos:
|
|
216
|
+
|
|
217
|
+
- **Onde roda o agente**: o AgenteCliSiTef roda na máquina física do caixa
|
|
218
|
+
(pinpad conectado nela). Se o servidor Odoo for centralizado (não é a
|
|
219
|
+
mesma máquina do caixa), o controller do exemplo não vai alcançar
|
|
220
|
+
`127.0.0.1` do caixa — nesse caso, rode a lógica de pagamento em um
|
|
221
|
+
processo local no caixa (ex.: um pequeno serviço Python que expõe uma rota
|
|
222
|
+
para a UI do PDV) ao invés de no controller do servidor Odoo central.
|
|
223
|
+
- **Callbacks de negócio**: `on_confirm`/`on_input` no exemplo usam os
|
|
224
|
+
padrões conservadores da lib (recusam/cancelam). Decida como isso deve se
|
|
225
|
+
comportar no seu fluxo antes de ir para produção — por exemplo, se sua
|
|
226
|
+
CliSiTef está configurada para pedir CPF do cliente, você precisa buscar
|
|
227
|
+
esse dado do próprio pedido do Odoo e devolvê-lo via `on_input`.
|
|
228
|
+
- Este addon de exemplo **não foi testado contra uma instância real do
|
|
229
|
+
Odoo** (não há um Odoo disponível no ambiente onde esta lib foi escrita) —
|
|
230
|
+
trate-o como ponto de partida, não como módulo pronto para produção.
|
|
231
|
+
|
|
232
|
+
## Estrutura do pacote
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
clisitef_odoo/
|
|
236
|
+
client.py # AgenteCliSiTefClient - chamadas HTTP 1:1 com o agente
|
|
237
|
+
transaction.py # TefTransaction - loop interativo orientado a callbacks
|
|
238
|
+
constants.py # clisitefStatus, commandId, fieldId, functionId conhecidos
|
|
239
|
+
models.py # FieldCapture, TransactionResult
|
|
240
|
+
exceptions.py # AgentServiceError, ClisitefProtocolError, ...
|
|
241
|
+
examples/
|
|
242
|
+
odoo_addon_example/ # esqueleto de addon Odoo (POS payment)
|
|
243
|
+
tests/
|
|
244
|
+
test_client.py # testes do cliente HTTP com mocks
|
|
245
|
+
test_transaction.py # testes do loop interativo com mocks
|
|
246
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""clisitef_odoo - cliente Python para o AgenteCliSiTef, pensado para uso em
|
|
2
|
+
integracoes com Odoo (pos_payment, wizards de pagamento, etc.).
|
|
3
|
+
|
|
4
|
+
Uso tipico:
|
|
5
|
+
|
|
6
|
+
from clisitef_odoo import AgenteCliSiTefClient, TefTransaction, constants
|
|
7
|
+
|
|
8
|
+
client = AgenteCliSiTefClient(base_url="https://127.0.0.1/agente/clisitef")
|
|
9
|
+
tef = TefTransaction(client)
|
|
10
|
+
result = tef.run(
|
|
11
|
+
function_id=constants.FUNCTION_VENDA_CREDITO,
|
|
12
|
+
trn_amount=1000, # R$ 10,00, em centavos
|
|
13
|
+
sitef_ip="127.0.0.1", store_id="00000000", terminal_id="REST0001",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
Veja o README.md do projeto para detalhes de configuracao e exemplos de
|
|
17
|
+
integracao com Odoo.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .client import AgenteCliSiTefClient
|
|
21
|
+
from .exceptions import (
|
|
22
|
+
AgentServiceError,
|
|
23
|
+
ClisitefError,
|
|
24
|
+
ClisitefProtocolError,
|
|
25
|
+
TransactionCancelled,
|
|
26
|
+
)
|
|
27
|
+
from .models import FieldCapture, TransactionResult
|
|
28
|
+
from .transaction import TefTransaction
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"AgenteCliSiTefClient",
|
|
32
|
+
"TefTransaction",
|
|
33
|
+
"TransactionResult",
|
|
34
|
+
"FieldCapture",
|
|
35
|
+
"ClisitefError",
|
|
36
|
+
"AgentServiceError",
|
|
37
|
+
"ClisitefProtocolError",
|
|
38
|
+
"TransactionCancelled",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
__version__ = "0.1.0"
|