stackin-python-sdk 0.7.0__tar.gz → 0.9.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.7.0 → stackin_python_sdk-0.9.0}/PKG-INFO +84 -5
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/README.md +83 -4
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/pyproject.toml +2 -2
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/__init__.py +1 -1
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/br/__init__.py +2 -1
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/br/product.py +24 -2
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/LICENSE +0 -0
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/br/tax.py +0 -0
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/core/__init__.py +0 -0
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/core/address.py +0 -0
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/core/client.py +0 -0
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/core/exceptions.py +0 -0
- {stackin_python_sdk-0.7.0 → stackin_python_sdk-0.9.0}/stackin/core/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackin-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Python SDK for issuing, consulting and cancelling electronic invoices.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -47,7 +47,73 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
|
|
48
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
49
|
|
|
50
|
-
**One class, `Invoice`** — `issue()`/`consult()`/`cancel()`/`reissue()`/`correct()`/`invalidate()`/`pdf()`/`received()`/`manifest()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description
|
|
50
|
+
**One class, `Invoice`** — `issue()`/`consult()`/`cancel()`/`reissue()`/`correct()`/`invalidate()`/`pdf()`/`received()`/`manifest()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description` plus either `unit_price` or `amount` are universal, everything else (`ncm`/`cfop`/`cest`/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.
|
|
51
|
+
|
|
52
|
+
## What a line item is worth
|
|
53
|
+
|
|
54
|
+
`unit_price` is the price of **one unit**. `amount` is the **gross total of
|
|
55
|
+
the line's products**, before discount, freight, insurance and other
|
|
56
|
+
expenses. Send either; sending both asserts that they agree.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from decimal import Decimal
|
|
60
|
+
|
|
61
|
+
# One unit
|
|
62
|
+
Product(description="Consultoria", quantity=Decimal("1"), unit_price=Decimal("1500.00"))
|
|
63
|
+
|
|
64
|
+
# More than one unit — amount is computed as 2 x 120.00 = 240.00
|
|
65
|
+
Product(description="Teclado", quantity=Decimal("2"), unit_price=Decimal("120.00"))
|
|
66
|
+
|
|
67
|
+
# A fractional quantity — 1.5 x 30.00 = 45.00
|
|
68
|
+
Product(description="Cabo", quantity=Decimal("1.5"), unit_price=Decimal("30.00"))
|
|
69
|
+
|
|
70
|
+
# Legacy: amount alone still means the line's gross total
|
|
71
|
+
Product(description="Servico", quantity=Decimal("3"), amount=Decimal("150.00"))
|
|
72
|
+
|
|
73
|
+
# Both, checked against each other before the document is transmitted
|
|
74
|
+
Product(
|
|
75
|
+
description="Teclado",
|
|
76
|
+
quantity=Decimal("2"),
|
|
77
|
+
unit_price=Decimal("120.00"),
|
|
78
|
+
amount=Decimal("240.00"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# With charges: 5000.00 - 100.50 + 40.00 + 10.00 = 4949.50 on the note
|
|
82
|
+
Product(
|
|
83
|
+
description="Servidor",
|
|
84
|
+
quantity=Decimal("2"),
|
|
85
|
+
unit_price=Decimal("2500.00"),
|
|
86
|
+
discount=100.50,
|
|
87
|
+
freight=40.00,
|
|
88
|
+
insurance=10.00,
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Amounts that do not add up are refused before the authorizer sees them,
|
|
93
|
+
with a `422` naming the line and both numbers:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"detail": {
|
|
98
|
+
"code": "ITEM_TOTAL_MISMATCH",
|
|
99
|
+
"field": "items[0].amount",
|
|
100
|
+
"message": "amount is 50.00, but quantity x unit_price is 150.00.",
|
|
101
|
+
"expected": "150.00",
|
|
102
|
+
"received": "50.00"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Migrating
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
Before: quantity = 3, amount = 150.00
|
|
111
|
+
After: quantity = 3, unit_price = 50.00
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Nothing has to migrate. `amount` keeps the meaning it always had and is
|
|
115
|
+
not deprecated in this release; a line sent with `amount` alone is built
|
|
116
|
+
exactly as it was before.
|
|
51
117
|
|
|
52
118
|
## Install
|
|
53
119
|
|
|
@@ -60,6 +126,8 @@ pip install stackin-python-sdk
|
|
|
60
126
|
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
127
|
|
|
62
128
|
```python
|
|
129
|
+
from decimal import Decimal
|
|
130
|
+
|
|
63
131
|
from stackin import Invoice, DocumentType, Address
|
|
64
132
|
from stackin.br import Product # Brazil-specific line item — NCM/CFOP
|
|
65
133
|
|
|
@@ -69,7 +137,7 @@ invoice = client.issue(
|
|
|
69
137
|
document_type=DocumentType.NFSE,
|
|
70
138
|
client_name="John Doe",
|
|
71
139
|
tax_id="00000000000",
|
|
72
|
-
items=[Product(description="Software development",
|
|
140
|
+
items=[Product(description="Software development", unit_price=Decimal("5000.00"))],
|
|
73
141
|
)
|
|
74
142
|
|
|
75
143
|
status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
|
|
@@ -90,7 +158,16 @@ client.issue(
|
|
|
90
158
|
document_type=DocumentType.NFE,
|
|
91
159
|
client_name="Buyer Company Ltd",
|
|
92
160
|
tax_id="11111111111111",
|
|
93
|
-
items=[
|
|
161
|
+
items=[
|
|
162
|
+
Product(
|
|
163
|
+
description="Teclado",
|
|
164
|
+
quantity=Decimal("2"),
|
|
165
|
+
unit_price=Decimal("120.00"),
|
|
166
|
+
unit="UN",
|
|
167
|
+
ncm="84716052",
|
|
168
|
+
cfop="5102",
|
|
169
|
+
)
|
|
170
|
+
],
|
|
94
171
|
recipient_address=Address(
|
|
95
172
|
street="Avenida Atlantica",
|
|
96
173
|
number="500",
|
|
@@ -117,13 +194,15 @@ burned, and undoing it means cancelling, which has a deadline.
|
|
|
117
194
|
Pass `idempotency_key` to make the retry safe:
|
|
118
195
|
|
|
119
196
|
```python
|
|
197
|
+
from decimal import Decimal
|
|
198
|
+
|
|
120
199
|
key = str(uuid.uuid4())
|
|
121
200
|
|
|
122
201
|
result = invoice.issue(
|
|
123
202
|
document_type=DocumentType.NFSE,
|
|
124
203
|
client_name="Maria Silva",
|
|
125
204
|
tax_id="12345678909",
|
|
126
|
-
items=[Product(description="Consultoria",
|
|
205
|
+
items=[Product(description="Consultoria", unit_price=Decimal("1500.00"))],
|
|
127
206
|
idempotency_key=key,
|
|
128
207
|
)
|
|
129
208
|
```
|
|
@@ -18,7 +18,73 @@
|
|
|
18
18
|
|
|
19
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
20
|
|
|
21
|
-
**One class, `Invoice`** — `issue()`/`consult()`/`cancel()`/`reissue()`/`correct()`/`invalidate()`/`pdf()`/`received()`/`manifest()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description
|
|
21
|
+
**One class, `Invoice`** — `issue()`/`consult()`/`cancel()`/`reissue()`/`correct()`/`invalidate()`/`pdf()`/`received()`/`manifest()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description` plus either `unit_price` or `amount` are universal, everything else (`ncm`/`cfop`/`cest`/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.
|
|
22
|
+
|
|
23
|
+
## What a line item is worth
|
|
24
|
+
|
|
25
|
+
`unit_price` is the price of **one unit**. `amount` is the **gross total of
|
|
26
|
+
the line's products**, before discount, freight, insurance and other
|
|
27
|
+
expenses. Send either; sending both asserts that they agree.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from decimal import Decimal
|
|
31
|
+
|
|
32
|
+
# One unit
|
|
33
|
+
Product(description="Consultoria", quantity=Decimal("1"), unit_price=Decimal("1500.00"))
|
|
34
|
+
|
|
35
|
+
# More than one unit — amount is computed as 2 x 120.00 = 240.00
|
|
36
|
+
Product(description="Teclado", quantity=Decimal("2"), unit_price=Decimal("120.00"))
|
|
37
|
+
|
|
38
|
+
# A fractional quantity — 1.5 x 30.00 = 45.00
|
|
39
|
+
Product(description="Cabo", quantity=Decimal("1.5"), unit_price=Decimal("30.00"))
|
|
40
|
+
|
|
41
|
+
# Legacy: amount alone still means the line's gross total
|
|
42
|
+
Product(description="Servico", quantity=Decimal("3"), amount=Decimal("150.00"))
|
|
43
|
+
|
|
44
|
+
# Both, checked against each other before the document is transmitted
|
|
45
|
+
Product(
|
|
46
|
+
description="Teclado",
|
|
47
|
+
quantity=Decimal("2"),
|
|
48
|
+
unit_price=Decimal("120.00"),
|
|
49
|
+
amount=Decimal("240.00"),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# With charges: 5000.00 - 100.50 + 40.00 + 10.00 = 4949.50 on the note
|
|
53
|
+
Product(
|
|
54
|
+
description="Servidor",
|
|
55
|
+
quantity=Decimal("2"),
|
|
56
|
+
unit_price=Decimal("2500.00"),
|
|
57
|
+
discount=100.50,
|
|
58
|
+
freight=40.00,
|
|
59
|
+
insurance=10.00,
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Amounts that do not add up are refused before the authorizer sees them,
|
|
64
|
+
with a `422` naming the line and both numbers:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"detail": {
|
|
69
|
+
"code": "ITEM_TOTAL_MISMATCH",
|
|
70
|
+
"field": "items[0].amount",
|
|
71
|
+
"message": "amount is 50.00, but quantity x unit_price is 150.00.",
|
|
72
|
+
"expected": "150.00",
|
|
73
|
+
"received": "50.00"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Migrating
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
Before: quantity = 3, amount = 150.00
|
|
82
|
+
After: quantity = 3, unit_price = 50.00
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Nothing has to migrate. `amount` keeps the meaning it always had and is
|
|
86
|
+
not deprecated in this release; a line sent with `amount` alone is built
|
|
87
|
+
exactly as it was before.
|
|
22
88
|
|
|
23
89
|
## Install
|
|
24
90
|
|
|
@@ -31,6 +97,8 @@ pip install stackin-python-sdk
|
|
|
31
97
|
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
98
|
|
|
33
99
|
```python
|
|
100
|
+
from decimal import Decimal
|
|
101
|
+
|
|
34
102
|
from stackin import Invoice, DocumentType, Address
|
|
35
103
|
from stackin.br import Product # Brazil-specific line item — NCM/CFOP
|
|
36
104
|
|
|
@@ -40,7 +108,7 @@ invoice = client.issue(
|
|
|
40
108
|
document_type=DocumentType.NFSE,
|
|
41
109
|
client_name="John Doe",
|
|
42
110
|
tax_id="00000000000",
|
|
43
|
-
items=[Product(description="Software development",
|
|
111
|
+
items=[Product(description="Software development", unit_price=Decimal("5000.00"))],
|
|
44
112
|
)
|
|
45
113
|
|
|
46
114
|
status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
|
|
@@ -61,7 +129,16 @@ client.issue(
|
|
|
61
129
|
document_type=DocumentType.NFE,
|
|
62
130
|
client_name="Buyer Company Ltd",
|
|
63
131
|
tax_id="11111111111111",
|
|
64
|
-
items=[
|
|
132
|
+
items=[
|
|
133
|
+
Product(
|
|
134
|
+
description="Teclado",
|
|
135
|
+
quantity=Decimal("2"),
|
|
136
|
+
unit_price=Decimal("120.00"),
|
|
137
|
+
unit="UN",
|
|
138
|
+
ncm="84716052",
|
|
139
|
+
cfop="5102",
|
|
140
|
+
)
|
|
141
|
+
],
|
|
65
142
|
recipient_address=Address(
|
|
66
143
|
street="Avenida Atlantica",
|
|
67
144
|
number="500",
|
|
@@ -88,13 +165,15 @@ burned, and undoing it means cancelling, which has a deadline.
|
|
|
88
165
|
Pass `idempotency_key` to make the retry safe:
|
|
89
166
|
|
|
90
167
|
```python
|
|
168
|
+
from decimal import Decimal
|
|
169
|
+
|
|
91
170
|
key = str(uuid.uuid4())
|
|
92
171
|
|
|
93
172
|
result = invoice.issue(
|
|
94
173
|
document_type=DocumentType.NFSE,
|
|
95
174
|
client_name="Maria Silva",
|
|
96
175
|
tax_id="12345678909",
|
|
97
|
-
items=[Product(description="Consultoria",
|
|
176
|
+
items=[Product(description="Consultoria", unit_price=Decimal("1500.00"))],
|
|
98
177
|
idempotency_key=key,
|
|
99
178
|
)
|
|
100
179
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stackin-python-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.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.9.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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Brazil-specific document fields."""
|
|
2
2
|
|
|
3
|
-
from stackin.br.product import PresumedCredit, Product
|
|
3
|
+
from stackin.br.product import IbsCbs, PresumedCredit, Product
|
|
4
4
|
from stackin.br.tax import (
|
|
5
5
|
CofinsAliq,
|
|
6
6
|
CofinsNt,
|
|
@@ -42,6 +42,7 @@ from stackin.br.tax import (
|
|
|
42
42
|
|
|
43
43
|
__all__ = [
|
|
44
44
|
"Product",
|
|
45
|
+
"IbsCbs",
|
|
45
46
|
"PresumedCredit",
|
|
46
47
|
"Tax",
|
|
47
48
|
"Icms00",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from decimal import Decimal
|
|
5
6
|
from typing import Annotated, Any
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel, Field
|
|
@@ -9,6 +10,17 @@ from pydantic import BaseModel, Field
|
|
|
9
10
|
from stackin.br.tax import Tax
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
class IbsCbs(BaseModel):
|
|
14
|
+
"""The Reforma Tributária group for one item."""
|
|
15
|
+
|
|
16
|
+
cst: str = Field(pattern=r"^\d{3}$")
|
|
17
|
+
classification: str = Field(pattern=r"^\d{6}$")
|
|
18
|
+
base: float | None = Field(default=None, gt=0)
|
|
19
|
+
rate_state: float = Field(ge=0)
|
|
20
|
+
rate_city: float = Field(ge=0)
|
|
21
|
+
rate_federal: float = Field(ge=0)
|
|
22
|
+
|
|
23
|
+
|
|
12
24
|
class PresumedCredit(BaseModel):
|
|
13
25
|
"""A presumed tax credit applied to this item."""
|
|
14
26
|
|
|
@@ -32,15 +44,22 @@ _BR_FIELDS = {
|
|
|
32
44
|
"import_content_control_number",
|
|
33
45
|
"recopi_number",
|
|
34
46
|
"extra_groups",
|
|
47
|
+
"ibs_cbs",
|
|
35
48
|
"tax",
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
|
|
52
|
+
def _decimal_str(value: Decimal | None) -> str | None:
|
|
53
|
+
"""JSON has no decimal; a float would lose the tenth place."""
|
|
54
|
+
return None if value is None else format(value, "f")
|
|
55
|
+
|
|
56
|
+
|
|
39
57
|
class Product(BaseModel):
|
|
40
58
|
"""One product or service line item on an invoice."""
|
|
41
59
|
|
|
42
60
|
description: str = Field(..., min_length=1)
|
|
43
|
-
amount:
|
|
61
|
+
amount: Decimal | None = Field(default=None, gt=0)
|
|
62
|
+
unit_price: Decimal | None = Field(default=None, gt=0)
|
|
44
63
|
unit: str = Field(default="UN")
|
|
45
64
|
quantity: float = Field(default=1.0, gt=0)
|
|
46
65
|
barcode: str | None = Field(default=None)
|
|
@@ -73,6 +92,7 @@ class Product(BaseModel):
|
|
|
73
92
|
),
|
|
74
93
|
)
|
|
75
94
|
recopi_number: str | None = Field(default=None, pattern=r"^\d{20}$")
|
|
95
|
+
ibs_cbs: IbsCbs | None = Field(default=None)
|
|
76
96
|
extra_groups: dict[str, Any] | None = Field(default=None)
|
|
77
97
|
tax: Tax | dict[str, Any] | None = Field(default=None)
|
|
78
98
|
|
|
@@ -102,6 +122,7 @@ class Product(BaseModel):
|
|
|
102
122
|
exclude={
|
|
103
123
|
"description",
|
|
104
124
|
"amount",
|
|
125
|
+
"unit_price",
|
|
105
126
|
"tax",
|
|
106
127
|
*_BR_FIELDS,
|
|
107
128
|
*_NFSE_FIELDS,
|
|
@@ -116,7 +137,8 @@ class Product(BaseModel):
|
|
|
116
137
|
data["br"] = br
|
|
117
138
|
return {
|
|
118
139
|
"description": self.description,
|
|
119
|
-
"amount": self.amount,
|
|
140
|
+
"amount": _decimal_str(self.amount),
|
|
141
|
+
"unit_price": _decimal_str(self.unit_price),
|
|
120
142
|
"product": data,
|
|
121
143
|
"service_code": self.service_code,
|
|
122
144
|
"discount": self.service_discount,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|