stackin-python-sdk 0.1.1__tar.gz → 0.2.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.
- stackin_python_sdk-0.2.0/PKG-INFO +209 -0
- stackin_python_sdk-0.2.0/README.md +179 -0
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/pyproject.toml +2 -2
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/__init__.py +1 -1
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/br/__init__.py +2 -0
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/br/product.py +30 -1
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/br/tax.py +23 -1
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/core/client.py +104 -10
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/core/exceptions.py +1 -2
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/core/types.py +1 -4
- stackin_python_sdk-0.1.1/PKG-INFO +0 -108
- stackin_python_sdk-0.1.1/README.md +0 -78
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/LICENSE +0 -0
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/core/__init__.py +0 -0
- {stackin_python_sdk-0.1.1 → stackin_python_sdk-0.2.0}/stackin/core/address.py +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stackin-python-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python SDK for issuing, consulting and cancelling electronic invoices.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: nfe,nfse,nota-fiscal,nota-fiscal-eletronica,invoice,fiscal-document,brazil,sefaz,e-invoicing
|
|
8
|
+
Author: Fernando Celmer
|
|
9
|
+
Author-email: email@fernandocelmer.com
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Natural Language :: Portuguese (Brazilian)
|
|
21
|
+
Requires-Dist: pydantic
|
|
22
|
+
Requires-Dist: requests
|
|
23
|
+
Project-URL: Changelog, https://github.com/stackin-io/stackin-python-sdk/releases
|
|
24
|
+
Project-URL: Documentation, https://docs.stackin.io/blog/python-sdk
|
|
25
|
+
Project-URL: Homepage, https://docs.stackin.io
|
|
26
|
+
Project-URL: Issues, https://github.com/stackin-io/stackin-python-sdk/issues
|
|
27
|
+
Project-URL: Repository, https://github.com/stackin-io/stackin-python-sdk
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
<div align="center">
|
|
31
|
+
|
|
32
|
+
<img src="https://raw.githubusercontent.com/stackin-io/stackin-python-sdk/master/docs/assets/stackin.png" width="120" />
|
|
33
|
+
|
|
34
|
+
**Integrate once. Issue everywhere.**
|
|
35
|
+
|
|
36
|
+
[](pyproject.toml)
|
|
37
|
+
[](https://pypi.org/project/stackin-python-sdk/)
|
|
38
|
+
[](https://github.com/stackin-io/stackin-python-sdk)
|
|
39
|
+
|
|
40
|
+
[API Reference](https://docs.stackin.io) · [Python SDK guide](https://docs.stackin.io/blog/python-sdk)
|
|
41
|
+
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
# stackin
|
|
47
|
+
|
|
48
|
+
Python SDK for fiscal document issuance — a handful of business fields, nothing about certificates, XML, XSD, signing or SOAP. The API resolves all of that from the issuer's own configuration, identified by `api_key`.
|
|
49
|
+
|
|
50
|
+
**One class, `Invoice`** — `issue()`/`consult()`/`cancel()`/`reissue()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description`/`amount` are universal, everything else (`ncm`/`cfop`/`cest`/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install stackin-python-sdk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key (context `sdk`). One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from stackin import Invoice, DocumentType, Address
|
|
64
|
+
from stackin.br import Product # Brazil-specific line item — NCM/CFOP
|
|
65
|
+
|
|
66
|
+
client = Invoice(api_key="COMPANY_API_KEY") # defaults to https://sdk.stackin.io
|
|
67
|
+
|
|
68
|
+
invoice = client.issue(
|
|
69
|
+
document_type=DocumentType.NFSE,
|
|
70
|
+
client_name="John Doe",
|
|
71
|
+
tax_id="00000000000",
|
|
72
|
+
items=[Product(description="Software development", amount=5000.00)],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
|
|
76
|
+
client.cancel(
|
|
77
|
+
"ACCESS_KEY...",
|
|
78
|
+
document_type=DocumentType.NFSE,
|
|
79
|
+
reason="Typo",
|
|
80
|
+
)
|
|
81
|
+
client.reissue(invoice["id"]) # retries a rejected/failed submission
|
|
82
|
+
|
|
83
|
+
# NFE requires ncm/cfop on every item, plus the buyer's full recipient_address:
|
|
84
|
+
client.issue(
|
|
85
|
+
document_type=DocumentType.NFE,
|
|
86
|
+
client_name="Buyer Company Ltd",
|
|
87
|
+
tax_id="11111111111111",
|
|
88
|
+
items=[Product(description="Test product", amount=100.00, ncm="84713012", cfop="5102")],
|
|
89
|
+
recipient_address=Address(
|
|
90
|
+
street="Avenida Atlantica",
|
|
91
|
+
number="500",
|
|
92
|
+
neighborhood="Copacabana",
|
|
93
|
+
city="Rio de Janeiro",
|
|
94
|
+
state="RJ",
|
|
95
|
+
zip_code="22010000",
|
|
96
|
+
city_code="3304557",
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`recipient_address` is an `Address` — the buyer's address, **required for NFE** and ignored for NFSE. Every field is required, `city_code` (the 7-digit IBGE municipality code) included: it becomes `enderDest` on the wire and the SEFAZ rejects a partial one. `state` is also what resolves `idDest` — a buyer in another state is emitted as an interstate operation automatically. A missing or incomplete address raises a `ValueError` locally, before the request goes out.
|
|
102
|
+
|
|
103
|
+
`items` is a list of `Product` (`stackin.br`) — `description`/`amount` apply to any document type; `ncm`/`cfop` (plus everything else on `Product`: `cest`, tax groups, presumed credits...) are Brazil-specific and required per item for NFE, ignored for NFSE (a service isn't a physical good).
|
|
104
|
+
|
|
105
|
+
## Retrying safely
|
|
106
|
+
|
|
107
|
+
Issuing is the one call you must not repeat blindly. If the response is lost — a
|
|
108
|
+
timeout, a dropped connection — the document may well have been authorized, and a
|
|
109
|
+
second attempt issues a **second** fiscal document: another credit, another number
|
|
110
|
+
burned, and undoing it means cancelling, which has a deadline.
|
|
111
|
+
|
|
112
|
+
Pass `idempotency_key` to make the retry safe:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
key = str(uuid.uuid4())
|
|
116
|
+
|
|
117
|
+
result = invoice.issue(
|
|
118
|
+
document_type=DocumentType.NFSE,
|
|
119
|
+
client_name="Maria Silva",
|
|
120
|
+
tax_id="12345678909",
|
|
121
|
+
items=[Product(description="Consultoria", amount=1500.00)],
|
|
122
|
+
idempotency_key=key,
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Retry with the **same key and the same body** and you get the first response back,
|
|
127
|
+
replayed — no second document, no credit consumed. `reissue()` takes the same
|
|
128
|
+
argument.
|
|
129
|
+
|
|
130
|
+
| Situation | What the API does |
|
|
131
|
+
|---|---|
|
|
132
|
+
| New key | issues normally, records the response |
|
|
133
|
+
| Same key, same body | replays the recorded response |
|
|
134
|
+
| Same key, different body | `APIError` 422 |
|
|
135
|
+
| Same key, first call still running | `APIError` 409 |
|
|
136
|
+
| Previous attempt failed | key is released — the retry issues |
|
|
137
|
+
| Key older than 24 hours | treated as new |
|
|
138
|
+
|
|
139
|
+
Generate the key yourself and keep it for as long as you might retry — a `uuid4` per
|
|
140
|
+
business event, not per HTTP call. The SDK never generates one, because a key minted
|
|
141
|
+
per call would protect nothing, and because two genuinely separate invoices for the
|
|
142
|
+
same customer and amount on the same day are a normal thing to issue.
|
|
143
|
+
|
|
144
|
+
## Correcting a document
|
|
145
|
+
|
|
146
|
+
Some mistakes don't need a cancellation. A wrong product name, wrong
|
|
147
|
+
transport details, a typo in the extra information — a **CC-e** (carta de
|
|
148
|
+
correção) fixes those, and it is free: no new credit, no burned series
|
|
149
|
+
number, no reissue.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
result = invoice.correct(
|
|
153
|
+
"35240912345678000199550010000000011000000017",
|
|
154
|
+
document_type=DocumentType.NFE,
|
|
155
|
+
correction="Transportadora corrigida para Rapido Ltda",
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The correction text is 15 to 1000 characters, checked locally before the call.
|
|
160
|
+
|
|
161
|
+
What a CC-e **cannot** fix: anything that changes the tax owed (base, rate,
|
|
162
|
+
price, quantity, totals), the buyer or the seller, or the issue date. Those
|
|
163
|
+
still mean cancelling and reissuing. The API sends the legally fixed wording
|
|
164
|
+
that says exactly this, attached to every correction.
|
|
165
|
+
|
|
166
|
+
The original document does not change — the CC-e is an event attached to it, and
|
|
167
|
+
the authorized XML stays as it was. A document accepts at most 20 of them, and
|
|
168
|
+
they are numbered for you.
|
|
169
|
+
|
|
170
|
+
**NF-e only.** NFS-e has no correction letter, and asking for one returns
|
|
171
|
+
a `409`.
|
|
172
|
+
|
|
173
|
+
## Invalidating unused numbers
|
|
174
|
+
|
|
175
|
+
NF-e numbering is sequential and the SEFAZ expects it to have no gaps. A number
|
|
176
|
+
gets reserved the moment issuing starts, so a submission that fails afterwards —
|
|
177
|
+
a rejection, a timeout — leaves a hole in the series. Reporting that range is how
|
|
178
|
+
you close it.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
result = invoice.invalidate(
|
|
182
|
+
series="1",
|
|
183
|
+
number_start=10,
|
|
184
|
+
number_end=12,
|
|
185
|
+
reason="Numeracao reservada e nao utilizada por falha no ERP",
|
|
186
|
+
)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The reason is 15 to 255 characters and the range is inclusive; both are checked
|
|
190
|
+
locally, as is `number_end` not being below `number_start`.
|
|
191
|
+
|
|
192
|
+
A number that already reached the authorizer can't be invalidated. The API checks
|
|
193
|
+
its own records first and answers `409` naming the offending numbers, without a
|
|
194
|
+
round trip — and the authorizer checks again for what we can't see from here.
|
|
195
|
+
|
|
196
|
+
**NF-e only**, and it takes no access key: there is no document to point at.
|
|
197
|
+
|
|
198
|
+
## Errors
|
|
199
|
+
|
|
200
|
+
- `stackin.APIError` — the API responded with a non-2xx status (`status_code`, `detail`) — a 401 here means `api_key` is missing, wrong, or was rotated.
|
|
201
|
+
- `stackin.ConnectionFailedError` — the API didn't respond (network/DNS/timeout).
|
|
202
|
+
- `ValueError` — `issue()`'s `items` is empty, missing `ncm`/`cfop` on an item for NFE, or a missing/incomplete `recipient_address` on NFE.
|
|
203
|
+
|
|
204
|
+
Building the full fiscal document (issuer data, service code, tax groups, schema-accurate XML) is the API's job — configured once per company, not passed on every call.
|
|
205
|
+
|
|
206
|
+
## Examples
|
|
207
|
+
|
|
208
|
+
Runnable end-to-end scripts in [`examples/nfe/`](examples/nfe/) and [`examples/nfse/`](examples/nfse/) — one file per field/variant, from the bare minimum to every field filled. `examples/consult_invoice.py`, `examples/cancel_invoice.py`, and `examples/reissue_invoice.py` cover the operations that act on an already-issued document.
|
|
209
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/stackin-io/stackin-python-sdk/master/docs/assets/stackin.png" width="120" />
|
|
4
|
+
|
|
5
|
+
**Integrate once. Issue everywhere.**
|
|
6
|
+
|
|
7
|
+
[](pyproject.toml)
|
|
8
|
+
[](https://pypi.org/project/stackin-python-sdk/)
|
|
9
|
+
[](https://github.com/stackin-io/stackin-python-sdk)
|
|
10
|
+
|
|
11
|
+
[API Reference](https://docs.stackin.io) · [Python SDK guide](https://docs.stackin.io/blog/python-sdk)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# stackin
|
|
18
|
+
|
|
19
|
+
Python SDK for fiscal document issuance — a handful of business fields, nothing about certificates, XML, XSD, signing or SOAP. The API resolves all of that from the issuer's own configuration, identified by `api_key`.
|
|
20
|
+
|
|
21
|
+
**One class, `Invoice`** — `issue()`/`consult()`/`cancel()`/`reissue()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description`/`amount` are universal, everything else (`ncm`/`cfop`/`cest`/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install stackin-python-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key (context `sdk`). One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from stackin import Invoice, DocumentType, Address
|
|
35
|
+
from stackin.br import Product # Brazil-specific line item — NCM/CFOP
|
|
36
|
+
|
|
37
|
+
client = Invoice(api_key="COMPANY_API_KEY") # defaults to https://sdk.stackin.io
|
|
38
|
+
|
|
39
|
+
invoice = client.issue(
|
|
40
|
+
document_type=DocumentType.NFSE,
|
|
41
|
+
client_name="John Doe",
|
|
42
|
+
tax_id="00000000000",
|
|
43
|
+
items=[Product(description="Software development", amount=5000.00)],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
|
|
47
|
+
client.cancel(
|
|
48
|
+
"ACCESS_KEY...",
|
|
49
|
+
document_type=DocumentType.NFSE,
|
|
50
|
+
reason="Typo",
|
|
51
|
+
)
|
|
52
|
+
client.reissue(invoice["id"]) # retries a rejected/failed submission
|
|
53
|
+
|
|
54
|
+
# NFE requires ncm/cfop on every item, plus the buyer's full recipient_address:
|
|
55
|
+
client.issue(
|
|
56
|
+
document_type=DocumentType.NFE,
|
|
57
|
+
client_name="Buyer Company Ltd",
|
|
58
|
+
tax_id="11111111111111",
|
|
59
|
+
items=[Product(description="Test product", amount=100.00, ncm="84713012", cfop="5102")],
|
|
60
|
+
recipient_address=Address(
|
|
61
|
+
street="Avenida Atlantica",
|
|
62
|
+
number="500",
|
|
63
|
+
neighborhood="Copacabana",
|
|
64
|
+
city="Rio de Janeiro",
|
|
65
|
+
state="RJ",
|
|
66
|
+
zip_code="22010000",
|
|
67
|
+
city_code="3304557",
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`recipient_address` is an `Address` — the buyer's address, **required for NFE** and ignored for NFSE. Every field is required, `city_code` (the 7-digit IBGE municipality code) included: it becomes `enderDest` on the wire and the SEFAZ rejects a partial one. `state` is also what resolves `idDest` — a buyer in another state is emitted as an interstate operation automatically. A missing or incomplete address raises a `ValueError` locally, before the request goes out.
|
|
73
|
+
|
|
74
|
+
`items` is a list of `Product` (`stackin.br`) — `description`/`amount` apply to any document type; `ncm`/`cfop` (plus everything else on `Product`: `cest`, tax groups, presumed credits...) are Brazil-specific and required per item for NFE, ignored for NFSE (a service isn't a physical good).
|
|
75
|
+
|
|
76
|
+
## Retrying safely
|
|
77
|
+
|
|
78
|
+
Issuing is the one call you must not repeat blindly. If the response is lost — a
|
|
79
|
+
timeout, a dropped connection — the document may well have been authorized, and a
|
|
80
|
+
second attempt issues a **second** fiscal document: another credit, another number
|
|
81
|
+
burned, and undoing it means cancelling, which has a deadline.
|
|
82
|
+
|
|
83
|
+
Pass `idempotency_key` to make the retry safe:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
key = str(uuid.uuid4())
|
|
87
|
+
|
|
88
|
+
result = invoice.issue(
|
|
89
|
+
document_type=DocumentType.NFSE,
|
|
90
|
+
client_name="Maria Silva",
|
|
91
|
+
tax_id="12345678909",
|
|
92
|
+
items=[Product(description="Consultoria", amount=1500.00)],
|
|
93
|
+
idempotency_key=key,
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Retry with the **same key and the same body** and you get the first response back,
|
|
98
|
+
replayed — no second document, no credit consumed. `reissue()` takes the same
|
|
99
|
+
argument.
|
|
100
|
+
|
|
101
|
+
| Situation | What the API does |
|
|
102
|
+
|---|---|
|
|
103
|
+
| New key | issues normally, records the response |
|
|
104
|
+
| Same key, same body | replays the recorded response |
|
|
105
|
+
| Same key, different body | `APIError` 422 |
|
|
106
|
+
| Same key, first call still running | `APIError` 409 |
|
|
107
|
+
| Previous attempt failed | key is released — the retry issues |
|
|
108
|
+
| Key older than 24 hours | treated as new |
|
|
109
|
+
|
|
110
|
+
Generate the key yourself and keep it for as long as you might retry — a `uuid4` per
|
|
111
|
+
business event, not per HTTP call. The SDK never generates one, because a key minted
|
|
112
|
+
per call would protect nothing, and because two genuinely separate invoices for the
|
|
113
|
+
same customer and amount on the same day are a normal thing to issue.
|
|
114
|
+
|
|
115
|
+
## Correcting a document
|
|
116
|
+
|
|
117
|
+
Some mistakes don't need a cancellation. A wrong product name, wrong
|
|
118
|
+
transport details, a typo in the extra information — a **CC-e** (carta de
|
|
119
|
+
correção) fixes those, and it is free: no new credit, no burned series
|
|
120
|
+
number, no reissue.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
result = invoice.correct(
|
|
124
|
+
"35240912345678000199550010000000011000000017",
|
|
125
|
+
document_type=DocumentType.NFE,
|
|
126
|
+
correction="Transportadora corrigida para Rapido Ltda",
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The correction text is 15 to 1000 characters, checked locally before the call.
|
|
131
|
+
|
|
132
|
+
What a CC-e **cannot** fix: anything that changes the tax owed (base, rate,
|
|
133
|
+
price, quantity, totals), the buyer or the seller, or the issue date. Those
|
|
134
|
+
still mean cancelling and reissuing. The API sends the legally fixed wording
|
|
135
|
+
that says exactly this, attached to every correction.
|
|
136
|
+
|
|
137
|
+
The original document does not change — the CC-e is an event attached to it, and
|
|
138
|
+
the authorized XML stays as it was. A document accepts at most 20 of them, and
|
|
139
|
+
they are numbered for you.
|
|
140
|
+
|
|
141
|
+
**NF-e only.** NFS-e has no correction letter, and asking for one returns
|
|
142
|
+
a `409`.
|
|
143
|
+
|
|
144
|
+
## Invalidating unused numbers
|
|
145
|
+
|
|
146
|
+
NF-e numbering is sequential and the SEFAZ expects it to have no gaps. A number
|
|
147
|
+
gets reserved the moment issuing starts, so a submission that fails afterwards —
|
|
148
|
+
a rejection, a timeout — leaves a hole in the series. Reporting that range is how
|
|
149
|
+
you close it.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
result = invoice.invalidate(
|
|
153
|
+
series="1",
|
|
154
|
+
number_start=10,
|
|
155
|
+
number_end=12,
|
|
156
|
+
reason="Numeracao reservada e nao utilizada por falha no ERP",
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The reason is 15 to 255 characters and the range is inclusive; both are checked
|
|
161
|
+
locally, as is `number_end` not being below `number_start`.
|
|
162
|
+
|
|
163
|
+
A number that already reached the authorizer can't be invalidated. The API checks
|
|
164
|
+
its own records first and answers `409` naming the offending numbers, without a
|
|
165
|
+
round trip — and the authorizer checks again for what we can't see from here.
|
|
166
|
+
|
|
167
|
+
**NF-e only**, and it takes no access key: there is no document to point at.
|
|
168
|
+
|
|
169
|
+
## Errors
|
|
170
|
+
|
|
171
|
+
- `stackin.APIError` — the API responded with a non-2xx status (`status_code`, `detail`) — a 401 here means `api_key` is missing, wrong, or was rotated.
|
|
172
|
+
- `stackin.ConnectionFailedError` — the API didn't respond (network/DNS/timeout).
|
|
173
|
+
- `ValueError` — `issue()`'s `items` is empty, missing `ncm`/`cfop` on an item for NFE, or a missing/incomplete `recipient_address` on NFE.
|
|
174
|
+
|
|
175
|
+
Building the full fiscal document (issuer data, service code, tax groups, schema-accurate XML) is the API's job — configured once per company, not passed on every call.
|
|
176
|
+
|
|
177
|
+
## Examples
|
|
178
|
+
|
|
179
|
+
Runnable end-to-end scripts in [`examples/nfe/`](examples/nfe/) and [`examples/nfse/`](examples/nfse/) — one file per field/variant, from the bare minimum to every field filled. `examples/consult_invoice.py`, `examples/cancel_invoice.py`, and `examples/reissue_invoice.py` cover the operations that act on an already-issued document.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stackin-python-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name = "Fernando Celmer", email = "email@fernandocelmer.com" },
|
|
6
6
|
]
|
|
@@ -45,7 +45,7 @@ Changelog = "https://github.com/stackin-io/stackin-python-sdk/releases"
|
|
|
45
45
|
|
|
46
46
|
[tool.poetry]
|
|
47
47
|
name = "stackin-python-sdk"
|
|
48
|
-
version = "0.
|
|
48
|
+
version = "0.2.0"
|
|
49
49
|
description = "Python SDK for issuing, consulting and cancelling electronic invoices."
|
|
50
50
|
authors = ["Fernando Celmer <email@fernandocelmer.com>"]
|
|
51
51
|
readme = "README.md"
|
|
@@ -10,6 +10,7 @@ from stackin.br.tax import (
|
|
|
10
10
|
Icms60,
|
|
11
11
|
IcmsSn101,
|
|
12
12
|
IcmsSn102,
|
|
13
|
+
IcmsSn900,
|
|
13
14
|
IcmsUfDest,
|
|
14
15
|
Ipi,
|
|
15
16
|
IpiNt,
|
|
@@ -29,6 +30,7 @@ __all__ = [
|
|
|
29
30
|
"Icms60",
|
|
30
31
|
"IcmsSn101",
|
|
31
32
|
"IcmsSn102",
|
|
33
|
+
"IcmsSn900",
|
|
32
34
|
"IcmsUfDest",
|
|
33
35
|
"Ipi",
|
|
34
36
|
"IpiTrib",
|
|
@@ -73,11 +73,36 @@ class Product(BaseModel):
|
|
|
73
73
|
extra_groups: dict[str, Any] | None = Field(default=None)
|
|
74
74
|
tax: Tax | dict[str, Any] | None = Field(default=None)
|
|
75
75
|
|
|
76
|
+
service_code: str | None = Field(
|
|
77
|
+
default=None, description="LC 116/2003 item.subitem, nfse only."
|
|
78
|
+
)
|
|
79
|
+
service_discount: float | None = Field(
|
|
80
|
+
default=None, description="Unconditional discount, nfse only."
|
|
81
|
+
)
|
|
82
|
+
tax_retained: bool = Field(
|
|
83
|
+
default=False, description="ISSQN retained by the tomador, nfse only."
|
|
84
|
+
)
|
|
85
|
+
observations: str | None = Field(
|
|
86
|
+
default=None, max_length=2000, description="nfse only."
|
|
87
|
+
)
|
|
88
|
+
|
|
76
89
|
def to_dict(self) -> dict:
|
|
77
90
|
"""Returns the item as a plain dict, ready for the request body."""
|
|
91
|
+
_NFSE_FIELDS = {
|
|
92
|
+
"service_code",
|
|
93
|
+
"service_discount",
|
|
94
|
+
"tax_retained",
|
|
95
|
+
"observations",
|
|
96
|
+
}
|
|
78
97
|
data = self.model_dump(
|
|
79
98
|
exclude_none=True,
|
|
80
|
-
exclude={
|
|
99
|
+
exclude={
|
|
100
|
+
"description",
|
|
101
|
+
"amount",
|
|
102
|
+
"tax",
|
|
103
|
+
*_BR_FIELDS,
|
|
104
|
+
*_NFSE_FIELDS,
|
|
105
|
+
},
|
|
81
106
|
)
|
|
82
107
|
br = self.model_dump(exclude_none=True, include=_BR_FIELDS - {"tax"})
|
|
83
108
|
if isinstance(self.tax, Tax):
|
|
@@ -90,4 +115,8 @@ class Product(BaseModel):
|
|
|
90
115
|
"description": self.description,
|
|
91
116
|
"amount": self.amount,
|
|
92
117
|
"product": data,
|
|
118
|
+
"service_code": self.service_code,
|
|
119
|
+
"discount": self.service_discount,
|
|
120
|
+
"tax_retained": self.tax_retained,
|
|
121
|
+
"observations": self.observations,
|
|
93
122
|
}
|
|
@@ -77,6 +77,19 @@ class IcmsSn102(BaseModel):
|
|
|
77
77
|
csosn: str = Field(alias="CSOSN")
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
class IcmsSn900(BaseModel):
|
|
81
|
+
"""Simples Nacional ICMS, other cases (used with interstate partilha)."""
|
|
82
|
+
|
|
83
|
+
model_config = _CONFIG
|
|
84
|
+
|
|
85
|
+
orig: str | None = None
|
|
86
|
+
csosn: str = Field(default="900", alias="CSOSN")
|
|
87
|
+
mod_bc: str | None = Field(default=None, alias="modBC")
|
|
88
|
+
v_bc: str | None = Field(default=None, alias="vBC")
|
|
89
|
+
p_icms: str | None = Field(default=None, alias="pICMS")
|
|
90
|
+
v_icms: str | None = Field(default=None, alias="vICMS")
|
|
91
|
+
|
|
92
|
+
|
|
80
93
|
class IcmsUfDest(BaseModel):
|
|
81
94
|
"""Interstate ICMS share owed to the destination state."""
|
|
82
95
|
|
|
@@ -193,7 +206,15 @@ class CofinsOutr(BaseModel):
|
|
|
193
206
|
v_cofins: str = Field(alias="vCOFINS")
|
|
194
207
|
|
|
195
208
|
|
|
196
|
-
IcmsGroup =
|
|
209
|
+
IcmsGroup = (
|
|
210
|
+
Icms00
|
|
211
|
+
| Icms40
|
|
212
|
+
| Icms60
|
|
213
|
+
| IcmsSn101
|
|
214
|
+
| IcmsSn102
|
|
215
|
+
| IcmsSn900
|
|
216
|
+
| dict[str, Any]
|
|
217
|
+
)
|
|
197
218
|
PisGroup = PisAliq | PisNt | PisOutr | dict[str, Any]
|
|
198
219
|
CofinsGroup = CofinsAliq | CofinsNt | CofinsOutr | dict[str, Any]
|
|
199
220
|
|
|
@@ -203,6 +224,7 @@ _ICMS_TAGS = {
|
|
|
203
224
|
Icms60: "ICMS60",
|
|
204
225
|
IcmsSn101: "ICMSSN101",
|
|
205
226
|
IcmsSn102: "ICMSSN102",
|
|
227
|
+
IcmsSn900: "ICMSSN900",
|
|
206
228
|
}
|
|
207
229
|
_PIS_TAGS = {PisAliq: "PISAliq", PisNt: "PISNT", PisOutr: "PISOutr"}
|
|
208
230
|
_COFINS_TAGS = {
|
|
@@ -23,9 +23,7 @@ _ENVIRONMENT_URLS = {
|
|
|
23
23
|
def _resolve_base_url(
|
|
24
24
|
base_url: str | None, environment: Environment | str | None
|
|
25
25
|
) -> str:
|
|
26
|
-
"""Resolution order
|
|
27
|
-
then env var, then the environment's default — `base_url` always
|
|
28
|
-
wins over `environment` at each step."""
|
|
26
|
+
"""Resolution order: explicit param, then env var, then environment's default."""
|
|
29
27
|
if base_url:
|
|
30
28
|
return base_url
|
|
31
29
|
if url := os.environ.get("STACKIN_BASE_URL"):
|
|
@@ -38,6 +36,32 @@ def _resolve_base_url(
|
|
|
38
36
|
return DEFAULT_BASE_URL
|
|
39
37
|
|
|
40
38
|
|
|
39
|
+
_NFE_ADDRESS_FIELDS = (
|
|
40
|
+
"street",
|
|
41
|
+
"number",
|
|
42
|
+
"neighborhood",
|
|
43
|
+
"city",
|
|
44
|
+
"state",
|
|
45
|
+
"zip_code",
|
|
46
|
+
"city_code",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _validate_nfe_address(address: Address | None) -> None:
|
|
51
|
+
"""NFE needs the buyer's full address — a partial one is a SEFAZ rejection."""
|
|
52
|
+
if address is None:
|
|
53
|
+
raise ValueError("recipient_address is required for NFE")
|
|
54
|
+
|
|
55
|
+
missing = [
|
|
56
|
+
field for field in _NFE_ADDRESS_FIELDS if not getattr(address, field)
|
|
57
|
+
]
|
|
58
|
+
if missing:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"recipient_address is missing required fields for NFE: "
|
|
61
|
+
+ ", ".join(missing)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
41
65
|
class Invoice:
|
|
42
66
|
"""Client for issuing, consulting, and cancelling fiscal documents."""
|
|
43
67
|
|
|
@@ -61,6 +85,9 @@ class Invoice:
|
|
|
61
85
|
tax_id: str,
|
|
62
86
|
items: list[Product],
|
|
63
87
|
recipient_address: Address | None = None,
|
|
88
|
+
series: str | None = None,
|
|
89
|
+
number: str | None = None,
|
|
90
|
+
idempotency_key: str | None = None,
|
|
64
91
|
) -> dict:
|
|
65
92
|
"""Issues a fiscal document."""
|
|
66
93
|
if not items:
|
|
@@ -74,6 +101,7 @@ class Invoice:
|
|
|
74
101
|
raise ValueError(
|
|
75
102
|
f"items[{index}].cfop is required for NFE"
|
|
76
103
|
)
|
|
104
|
+
_validate_nfe_address(recipient_address)
|
|
77
105
|
|
|
78
106
|
payload = {
|
|
79
107
|
"document_type": document_type.value,
|
|
@@ -81,10 +109,19 @@ class Invoice:
|
|
|
81
109
|
"tax_id": tax_id,
|
|
82
110
|
"items": [item.to_dict() for item in items],
|
|
83
111
|
}
|
|
84
|
-
if recipient_address
|
|
85
|
-
payload["
|
|
112
|
+
if recipient_address:
|
|
113
|
+
payload["recipient_address"] = recipient_address.to_dict()
|
|
114
|
+
if series:
|
|
115
|
+
payload["series"] = series
|
|
116
|
+
if number:
|
|
117
|
+
payload["number"] = number
|
|
86
118
|
|
|
87
|
-
return self._request(
|
|
119
|
+
return self._request(
|
|
120
|
+
"POST",
|
|
121
|
+
"/invoices",
|
|
122
|
+
json=payload,
|
|
123
|
+
idempotency_key=idempotency_key,
|
|
124
|
+
)
|
|
88
125
|
|
|
89
126
|
def consult(
|
|
90
127
|
self,
|
|
@@ -114,10 +151,66 @@ class Invoice:
|
|
|
114
151
|
"POST", f"/invoices/{access_key}/cancel", json=payload
|
|
115
152
|
)
|
|
116
153
|
|
|
117
|
-
def
|
|
154
|
+
def invalidate(
|
|
155
|
+
self,
|
|
156
|
+
*,
|
|
157
|
+
series: str,
|
|
158
|
+
number_start: int,
|
|
159
|
+
number_end: int,
|
|
160
|
+
reason: str,
|
|
161
|
+
) -> dict:
|
|
162
|
+
"""Reports a reserved but never used NFE numbering range."""
|
|
163
|
+
if not 15 <= len(reason) <= 255:
|
|
164
|
+
raise ValueError("reason must be 15 to 255 characters")
|
|
165
|
+
if number_end < number_start:
|
|
166
|
+
raise ValueError("number_end can't be below number_start")
|
|
167
|
+
|
|
168
|
+
payload = {
|
|
169
|
+
"series": series,
|
|
170
|
+
"number_start": number_start,
|
|
171
|
+
"number_end": number_end,
|
|
172
|
+
"reason": reason,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return self._request("POST", "/invoices/invalidations", json=payload)
|
|
176
|
+
|
|
177
|
+
def correct(
|
|
178
|
+
self,
|
|
179
|
+
access_key: str,
|
|
180
|
+
*,
|
|
181
|
+
document_type: DocumentType,
|
|
182
|
+
correction: str,
|
|
183
|
+
) -> dict:
|
|
184
|
+
"""Files a correction letter (CC-e) against an issued document."""
|
|
185
|
+
if not 15 <= len(correction) <= 1000:
|
|
186
|
+
raise ValueError("correction must be 15 to 1000 characters")
|
|
187
|
+
|
|
188
|
+
payload = {
|
|
189
|
+
"document_type": document_type.value,
|
|
190
|
+
"correction": correction,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return self._request(
|
|
194
|
+
"POST", f"/invoices/{access_key}/correction", json=payload
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def reissue(
|
|
198
|
+
self, invoice_id: str, *, idempotency_key: str | None = None
|
|
199
|
+
) -> dict:
|
|
200
|
+
"""Retries a previous invoice submission by its local id."""
|
|
201
|
+
return self._request(
|
|
202
|
+
"POST",
|
|
203
|
+
f"/invoices/{invoice_id}/reissue",
|
|
204
|
+
idempotency_key=idempotency_key,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _headers(self, idempotency_key: str | None = None) -> dict:
|
|
208
|
+
headers = {}
|
|
118
209
|
if self.api_key:
|
|
119
|
-
|
|
120
|
-
|
|
210
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
211
|
+
if idempotency_key:
|
|
212
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
213
|
+
return headers
|
|
121
214
|
|
|
122
215
|
def _request(
|
|
123
216
|
self,
|
|
@@ -126,6 +219,7 @@ class Invoice:
|
|
|
126
219
|
*,
|
|
127
220
|
json: dict | None = None,
|
|
128
221
|
params: dict | None = None,
|
|
222
|
+
idempotency_key: str | None = None,
|
|
129
223
|
) -> dict:
|
|
130
224
|
url = f"{self.base_url}/api/v1{path}"
|
|
131
225
|
|
|
@@ -135,7 +229,7 @@ class Invoice:
|
|
|
135
229
|
url,
|
|
136
230
|
json=json,
|
|
137
231
|
params=params,
|
|
138
|
-
headers=self._headers(),
|
|
232
|
+
headers=self._headers(idempotency_key),
|
|
139
233
|
timeout=self.timeout,
|
|
140
234
|
)
|
|
141
235
|
except requests.RequestException as error:
|
|
@@ -13,10 +13,7 @@ class DocumentType(str, Enum):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Environment(str, Enum):
|
|
16
|
-
"""Which host to talk to — pass to `Invoice(environment=...)`
|
|
17
|
-
instead of a raw `base_url`. `TEST` and `PRODUCTION` resolve to the
|
|
18
|
-
same host: homologation vs. production invoicing is a per-company
|
|
19
|
-
setting on the platform side, not a different SDK host."""
|
|
16
|
+
"""Which host to talk to — pass to `Invoice(environment=...)` instead of a raw `base_url`."""
|
|
20
17
|
|
|
21
18
|
LOCAL = "local"
|
|
22
19
|
TEST = "test"
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: stackin-python-sdk
|
|
3
|
-
Version: 0.1.1
|
|
4
|
-
Summary: Python SDK for issuing, consulting and cancelling electronic invoices.
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Keywords: nfe,nfse,nota-fiscal,nota-fiscal-eletronica,invoice,fiscal-document,brazil,sefaz,e-invoicing
|
|
8
|
-
Author: Fernando Celmer
|
|
9
|
-
Author-email: email@fernandocelmer.com
|
|
10
|
-
Requires-Python: >=3.10
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
-
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
19
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
-
Classifier: Natural Language :: Portuguese (Brazilian)
|
|
21
|
-
Requires-Dist: pydantic
|
|
22
|
-
Requires-Dist: requests
|
|
23
|
-
Project-URL: Changelog, https://github.com/stackin-io/stackin-python-sdk/releases
|
|
24
|
-
Project-URL: Documentation, https://docs.stackin.io/blog/python-sdk
|
|
25
|
-
Project-URL: Homepage, https://docs.stackin.io
|
|
26
|
-
Project-URL: Issues, https://github.com/stackin-io/stackin-python-sdk/issues
|
|
27
|
-
Project-URL: Repository, https://github.com/stackin-io/stackin-python-sdk
|
|
28
|
-
Description-Content-Type: text/markdown
|
|
29
|
-
|
|
30
|
-
<div align="center">
|
|
31
|
-
|
|
32
|
-
<img src="https://raw.githubusercontent.com/stackin-io/stackin-python-sdk/master/docs/assets/stackin.png" width="120" />
|
|
33
|
-
|
|
34
|
-
**Integrate once. Issue everywhere.**
|
|
35
|
-
|
|
36
|
-
[](pyproject.toml)
|
|
37
|
-
[](https://pypi.org/project/stackin-python-sdk/)
|
|
38
|
-
[](https://github.com/stackin-io/stackin-python-sdk)
|
|
39
|
-
|
|
40
|
-
[API Reference](https://docs.stackin.io) · [Python SDK guide](https://docs.stackin.io/blog/python-sdk)
|
|
41
|
-
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
# stackin
|
|
47
|
-
|
|
48
|
-
Python SDK for fiscal document issuance — a handful of business fields, nothing about certificates, XML, XSD, signing or SOAP. The API resolves all of that from the issuer's own configuration, identified by `api_key`.
|
|
49
|
-
|
|
50
|
-
**One class, `Invoice`** — `issue()`/`consult()`/`cancel()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description`/`amount` are universal, everything else (`ncm`/`cfop`/`cest`/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.
|
|
51
|
-
|
|
52
|
-
## Install
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
pip install stackin-python-sdk
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Usage
|
|
59
|
-
|
|
60
|
-
Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key (context `sdk`). One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
from stackin import Invoice, DocumentType, Address
|
|
64
|
-
from stackin.br import Product # Brazil-specific line item — NCM/CFOP
|
|
65
|
-
|
|
66
|
-
client = Invoice(api_key="COMPANY_API_KEY") # defaults to https://sdk.stackin.io
|
|
67
|
-
|
|
68
|
-
invoice = client.issue(
|
|
69
|
-
document_type=DocumentType.NFSE,
|
|
70
|
-
client_name="John Doe",
|
|
71
|
-
tax_id="00000000000",
|
|
72
|
-
items=[Product(description="Software development", amount=5000.00)],
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
|
|
76
|
-
client.cancel(
|
|
77
|
-
"ACCESS_KEY...",
|
|
78
|
-
document_type=DocumentType.NFSE,
|
|
79
|
-
reason="Typo",
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# NFE requires ncm/cfop on every item, and optionally recipient_address.state
|
|
83
|
-
# to get idDest right on interstate sales:
|
|
84
|
-
client.issue(
|
|
85
|
-
document_type=DocumentType.NFE,
|
|
86
|
-
client_name="Buyer Company Ltd",
|
|
87
|
-
tax_id="11111111111111",
|
|
88
|
-
items=[Product(description="Test product", amount=100.00, ncm="84713012", cfop="5102")],
|
|
89
|
-
recipient_address=Address(state="RJ"),
|
|
90
|
-
)
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
`recipient_address` is an `Address`, but despite the name only `.state` is read — the rest of the fields aren't sent anywhere yet. It's the actual customer's state, used only to set `idDest` (interstate vs internal) on NFE — optional, omitting it always produces `idDest=1` (internal).
|
|
94
|
-
|
|
95
|
-
`items` is a list of `Product` (`stackin.br`) — `description`/`amount` apply to any document type; `ncm`/`cfop` (plus everything else on `Product`: `cest`, tax groups, presumed credits...) are Brazil-specific and required per item for NFE, ignored for NFSE (a service isn't a physical good).
|
|
96
|
-
|
|
97
|
-
## Errors
|
|
98
|
-
|
|
99
|
-
- `stackin.APIError` — the API responded with a non-2xx status (`status_code`, `detail`) — a 401 here means `api_key` is missing, wrong, or was rotated.
|
|
100
|
-
- `stackin.ConnectionFailedError` — the API didn't respond (network/DNS/timeout).
|
|
101
|
-
- `ValueError` — `issue()`'s `items` is empty, or missing `ncm`/`cfop` on an item for NFE.
|
|
102
|
-
|
|
103
|
-
Building the full fiscal document (issuer data, service code, tax groups, schema-accurate XML) is the API's job — configured once per company, not passed on every call.
|
|
104
|
-
|
|
105
|
-
## Examples
|
|
106
|
-
|
|
107
|
-
Runnable end-to-end scripts in [`examples/`](examples/) — `simple_issue_nfe.py` and `simple_issue_nfse.py`, each with a catalog of realistic line items covering every optional field.
|
|
108
|
-
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
<img src="https://raw.githubusercontent.com/stackin-io/stackin-python-sdk/master/docs/assets/stackin.png" width="120" />
|
|
4
|
-
|
|
5
|
-
**Integrate once. Issue everywhere.**
|
|
6
|
-
|
|
7
|
-
[](pyproject.toml)
|
|
8
|
-
[](https://pypi.org/project/stackin-python-sdk/)
|
|
9
|
-
[](https://github.com/stackin-io/stackin-python-sdk)
|
|
10
|
-
|
|
11
|
-
[API Reference](https://docs.stackin.io) · [Python SDK guide](https://docs.stackin.io/blog/python-sdk)
|
|
12
|
-
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
# stackin
|
|
18
|
-
|
|
19
|
-
Python SDK for fiscal document issuance — a handful of business fields, nothing about certificates, XML, XSD, signing or SOAP. The API resolves all of that from the issuer's own configuration, identified by `api_key`.
|
|
20
|
-
|
|
21
|
-
**One class, `Invoice`** — `issue()`/`consult()`/`cancel()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description`/`amount` are universal, everything else (`ncm`/`cfop`/`cest`/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.
|
|
22
|
-
|
|
23
|
-
## Install
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
pip install stackin-python-sdk
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Usage
|
|
30
|
-
|
|
31
|
-
Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key (context `sdk`). One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
|
|
32
|
-
|
|
33
|
-
```python
|
|
34
|
-
from stackin import Invoice, DocumentType, Address
|
|
35
|
-
from stackin.br import Product # Brazil-specific line item — NCM/CFOP
|
|
36
|
-
|
|
37
|
-
client = Invoice(api_key="COMPANY_API_KEY") # defaults to https://sdk.stackin.io
|
|
38
|
-
|
|
39
|
-
invoice = client.issue(
|
|
40
|
-
document_type=DocumentType.NFSE,
|
|
41
|
-
client_name="John Doe",
|
|
42
|
-
tax_id="00000000000",
|
|
43
|
-
items=[Product(description="Software development", amount=5000.00)],
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
|
|
47
|
-
client.cancel(
|
|
48
|
-
"ACCESS_KEY...",
|
|
49
|
-
document_type=DocumentType.NFSE,
|
|
50
|
-
reason="Typo",
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# NFE requires ncm/cfop on every item, and optionally recipient_address.state
|
|
54
|
-
# to get idDest right on interstate sales:
|
|
55
|
-
client.issue(
|
|
56
|
-
document_type=DocumentType.NFE,
|
|
57
|
-
client_name="Buyer Company Ltd",
|
|
58
|
-
tax_id="11111111111111",
|
|
59
|
-
items=[Product(description="Test product", amount=100.00, ncm="84713012", cfop="5102")],
|
|
60
|
-
recipient_address=Address(state="RJ"),
|
|
61
|
-
)
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
`recipient_address` is an `Address`, but despite the name only `.state` is read — the rest of the fields aren't sent anywhere yet. It's the actual customer's state, used only to set `idDest` (interstate vs internal) on NFE — optional, omitting it always produces `idDest=1` (internal).
|
|
65
|
-
|
|
66
|
-
`items` is a list of `Product` (`stackin.br`) — `description`/`amount` apply to any document type; `ncm`/`cfop` (plus everything else on `Product`: `cest`, tax groups, presumed credits...) are Brazil-specific and required per item for NFE, ignored for NFSE (a service isn't a physical good).
|
|
67
|
-
|
|
68
|
-
## Errors
|
|
69
|
-
|
|
70
|
-
- `stackin.APIError` — the API responded with a non-2xx status (`status_code`, `detail`) — a 401 here means `api_key` is missing, wrong, or was rotated.
|
|
71
|
-
- `stackin.ConnectionFailedError` — the API didn't respond (network/DNS/timeout).
|
|
72
|
-
- `ValueError` — `issue()`'s `items` is empty, or missing `ncm`/`cfop` on an item for NFE.
|
|
73
|
-
|
|
74
|
-
Building the full fiscal document (issuer data, service code, tax groups, schema-accurate XML) is the API's job — configured once per company, not passed on every call.
|
|
75
|
-
|
|
76
|
-
## Examples
|
|
77
|
-
|
|
78
|
-
Runnable end-to-end scripts in [`examples/`](examples/) — `simple_issue_nfe.py` and `simple_issue_nfse.py`, each with a catalog of realistic line items covering every optional field.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|