moy-nalog-api 1.0.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.
- moy_nalog_api-1.0.0/.claude/settings.local.json +25 -0
- moy_nalog_api-1.0.0/.gitignore +43 -0
- moy_nalog_api-1.0.0/LICENSE +21 -0
- moy_nalog_api-1.0.0/PKG-INFO +533 -0
- moy_nalog_api-1.0.0/README.md +495 -0
- moy_nalog_api-1.0.0/README.ru.md +495 -0
- moy_nalog_api-1.0.0/examples/async_example.py +104 -0
- moy_nalog_api-1.0.0/examples/sms_auth.py +58 -0
- moy_nalog_api-1.0.0/examples/sync_example.py +47 -0
- moy_nalog_api-1.0.0/moy_nalog/__init__.py +88 -0
- moy_nalog_api-1.0.0/moy_nalog/client.py +879 -0
- moy_nalog_api-1.0.0/moy_nalog/enums.py +38 -0
- moy_nalog_api-1.0.0/moy_nalog/exceptions.py +68 -0
- moy_nalog_api-1.0.0/moy_nalog/models.py +180 -0
- moy_nalog_api-1.0.0/pyproject.toml +102 -0
- moy_nalog_api-1.0.0/scripts/integration_test.py +727 -0
- moy_nalog_api-1.0.0/tests/__init__.py +1 -0
- moy_nalog_api-1.0.0/tests/test_client.py +111 -0
- moy_nalog_api-1.0.0/tests/test_models.py +152 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(python -m pytest:*)",
|
|
5
|
+
"Bash(source .venv/bin/activate)",
|
|
6
|
+
"Bash(pip install:*)",
|
|
7
|
+
"Bash(pytest:*)",
|
|
8
|
+
"Bash(.venv/bin/pip install:*)",
|
|
9
|
+
"Bash(uv run pytest:*)",
|
|
10
|
+
"Bash(uv run ruff check:*)",
|
|
11
|
+
"Bash(uv pip install:*)",
|
|
12
|
+
"Bash(uv run python:*)",
|
|
13
|
+
"Bash(git init:*)",
|
|
14
|
+
"Bash(git add:*)",
|
|
15
|
+
"Bash(git commit:*)",
|
|
16
|
+
"Bash(git branch:*)",
|
|
17
|
+
"Bash(git remote add:*)",
|
|
18
|
+
"Bash(git push:*)",
|
|
19
|
+
"Bash(git remote set-url:*)",
|
|
20
|
+
"Bash(gh auth:*)",
|
|
21
|
+
"Bash(cat:*)",
|
|
22
|
+
"Bash(python -m build:*)"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
venv/
|
|
15
|
+
.venv/
|
|
16
|
+
env/
|
|
17
|
+
|
|
18
|
+
# Environment
|
|
19
|
+
.env
|
|
20
|
+
.env.local
|
|
21
|
+
session.json
|
|
22
|
+
*.pkl
|
|
23
|
+
|
|
24
|
+
# IDE
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
*.swp
|
|
28
|
+
*.swo
|
|
29
|
+
.DS_Store
|
|
30
|
+
|
|
31
|
+
# Testing
|
|
32
|
+
.pytest_cache/
|
|
33
|
+
.coverage
|
|
34
|
+
htmlcov/
|
|
35
|
+
.tox/
|
|
36
|
+
.mypy_cache/
|
|
37
|
+
.ruff_cache/
|
|
38
|
+
test_output/
|
|
39
|
+
|
|
40
|
+
# Misc
|
|
41
|
+
*.log
|
|
42
|
+
Thumbs.db
|
|
43
|
+
uv.lock
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kirill Nikulin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moy-nalog-api
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The most complete async Python client for Russian self-employed tax service (Moy Nalog / lknpd.nalog.ru)
|
|
5
|
+
Project-URL: Homepage, https://github.com/inache-su/moy-nalog-api
|
|
6
|
+
Project-URL: Documentation, https://github.com/inache-su/moy-nalog-api#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/inache-su/moy-nalog-api
|
|
8
|
+
Project-URL: Issues, https://github.com/inache-su/moy-nalog-api/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/inache-su/moy-nalog-api/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Kirill Nikulin <me@kirodev.eu>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,async,fns,httpx,moy-nalog,nalog,npd,pydantic,receipt,russia,samozanyatiy,self-employed,tax
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Framework :: Pydantic :: 2
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: httpx>=0.25.0
|
|
29
|
+
Requires-Dist: pydantic>=2.0.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# moy-nalog-api
|
|
40
|
+
|
|
41
|
+
[](https://github.com/inache-su/moy-nalog-api)
|
|
42
|
+
[](https://www.python.org/downloads/)
|
|
43
|
+
[](https://badge.fury.io/py/moy-nalog-api)
|
|
44
|
+
[](https://opensource.org/licenses/MIT)
|
|
45
|
+
[](https://github.com/astral-sh/ruff)
|
|
46
|
+
|
|
47
|
+
**The most complete and modern Python client for Russian self-employed tax service (lknpd.nalog.ru).**
|
|
48
|
+
|
|
49
|
+
Unofficial Python client for "Moy Nalog" API (self-employed, NPD tax regime).
|
|
50
|
+
|
|
51
|
+
[Документация на русском](README.ru.md)
|
|
52
|
+
|
|
53
|
+
## Why moy-nalog-api?
|
|
54
|
+
|
|
55
|
+
There are several Python libraries for the Moy Nalog API. Here's why you should choose this one:
|
|
56
|
+
|
|
57
|
+
| Feature | moy-nalog-api | Others |
|
|
58
|
+
|---------|---------------|--------|
|
|
59
|
+
| **Async/await support** | Native httpx async | Often sync-only or requests-based |
|
|
60
|
+
| **Sync wrapper included** | Yes, for non-async code | Usually one or the other |
|
|
61
|
+
| **SMS authentication** | Full support (request + verify) | Often missing or broken |
|
|
62
|
+
| **Session persistence** | Built-in JSON file storage | Manual implementation required |
|
|
63
|
+
| **Auto token refresh** | Automatic before expiration | Manual refresh needed |
|
|
64
|
+
| **Type hints** | 100% typed, mypy-compatible | Partial or none |
|
|
65
|
+
| **Pydantic v2** | Full validation and serialization | Often dict-based or Pydantic v1 |
|
|
66
|
+
| **Modern Python** | 3.10+ with latest syntax | Often 3.7+ with legacy code |
|
|
67
|
+
| **Error handling** | Typed exception hierarchy | Generic exceptions |
|
|
68
|
+
| **Retry logic** | Exponential backoff built-in | Usually none |
|
|
69
|
+
| **Multiple items** | Native support for multi-item receipts | Single item only |
|
|
70
|
+
| **Documentation** | Comprehensive with examples | Often minimal |
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- Async (httpx) and sync client support
|
|
75
|
+
- Password and SMS authentication
|
|
76
|
+
- Automatic token refresh with session persistence
|
|
77
|
+
- Retry with exponential backoff
|
|
78
|
+
- Full Pydantic v2 validation
|
|
79
|
+
- Multiple receipt items in single receipt
|
|
80
|
+
- All client types (individual, legal entity, foreign)
|
|
81
|
+
- Income list with pagination and filtering
|
|
82
|
+
- Receipt cancellation with reason
|
|
83
|
+
- Complete type hints for IDE support
|
|
84
|
+
|
|
85
|
+
## Installation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install moy-nalog-api
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For development:
|
|
92
|
+
```bash
|
|
93
|
+
pip install moy-nalog-api[dev]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Quick Start
|
|
97
|
+
|
|
98
|
+
### Async (Recommended)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import asyncio
|
|
102
|
+
from decimal import Decimal
|
|
103
|
+
from moy_nalog import MoyNalogClient
|
|
104
|
+
|
|
105
|
+
async def main():
|
|
106
|
+
# Create client with session persistence
|
|
107
|
+
async with MoyNalogClient(session_file="session.json") as client:
|
|
108
|
+
|
|
109
|
+
# First run: authenticate
|
|
110
|
+
if not client.is_authenticated:
|
|
111
|
+
await client.auth_by_password("your_inn", "your_password")
|
|
112
|
+
|
|
113
|
+
# Create receipt
|
|
114
|
+
receipt = await client.create_receipt(
|
|
115
|
+
name="Consulting services",
|
|
116
|
+
amount=Decimal("5000.00")
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
print(f"Receipt created: {receipt.print_url}")
|
|
120
|
+
|
|
121
|
+
asyncio.run(main())
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Sync
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from decimal import Decimal
|
|
128
|
+
from moy_nalog import MoyNalogClientSync
|
|
129
|
+
|
|
130
|
+
with MoyNalogClientSync(session_file="session.json") as client:
|
|
131
|
+
if not client.is_authenticated:
|
|
132
|
+
client.auth_by_password("your_inn", "your_password")
|
|
133
|
+
|
|
134
|
+
receipt = client.create_receipt(
|
|
135
|
+
name="Consulting services",
|
|
136
|
+
amount=Decimal("5000.00")
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
print(f"Receipt created: {receipt.print_url}")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Authentication
|
|
143
|
+
|
|
144
|
+
### Password Authentication
|
|
145
|
+
|
|
146
|
+
Use your INN (tax identification number) or phone and password from nalog.ru:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
profile = await client.auth_by_password(
|
|
150
|
+
username="123456789012", # INN (12 digits) or phone
|
|
151
|
+
password="your_password"
|
|
152
|
+
)
|
|
153
|
+
print(f"Authenticated as: {profile.display_name}")
|
|
154
|
+
print(f"INN: {profile.inn}")
|
|
155
|
+
print(f"Status: {profile.status}")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### SMS Authentication
|
|
159
|
+
|
|
160
|
+
Two-step process for phone-based authentication:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Step 1: Request SMS code
|
|
164
|
+
phone = "79001234567" # Format: 7XXXXXXXXXX (11 digits)
|
|
165
|
+
challenge = await client.request_sms_code(phone)
|
|
166
|
+
print(f"SMS sent! Code expires in {challenge.expire_in} seconds")
|
|
167
|
+
|
|
168
|
+
# Step 2: Enter code and authenticate
|
|
169
|
+
code = input("Enter 6-digit code from SMS: ")
|
|
170
|
+
profile = await client.auth_by_sms(phone, challenge.challenge_token, code)
|
|
171
|
+
print(f"Authenticated as: {profile.display_name}")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Session Persistence
|
|
175
|
+
|
|
176
|
+
Save and restore authentication tokens automatically:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
# Session file stores tokens between runs
|
|
180
|
+
client = MoyNalogClient(session_file="session.json")
|
|
181
|
+
|
|
182
|
+
# Check if already authenticated from previous session
|
|
183
|
+
if client.is_authenticated:
|
|
184
|
+
print("Session restored from file")
|
|
185
|
+
else:
|
|
186
|
+
# Authenticate (tokens saved automatically)
|
|
187
|
+
await client.auth_by_password(username, password)
|
|
188
|
+
|
|
189
|
+
# Tokens auto-refresh when expired
|
|
190
|
+
# Session auto-saves on close
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Session file contains:
|
|
194
|
+
- Access token (for API requests)
|
|
195
|
+
- Refresh token (for token renewal)
|
|
196
|
+
- Token expiration time
|
|
197
|
+
- User INN and device ID
|
|
198
|
+
|
|
199
|
+
## Creating Receipts
|
|
200
|
+
|
|
201
|
+
### Simple Receipt
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from decimal import Decimal
|
|
205
|
+
|
|
206
|
+
receipt = await client.create_receipt(
|
|
207
|
+
name="Web development",
|
|
208
|
+
amount=Decimal("15000.00")
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
print(f"UUID: {receipt.uuid}")
|
|
212
|
+
print(f"Amount: {receipt.total_amount} RUB")
|
|
213
|
+
print(f"Print URL: {receipt.print_url}")
|
|
214
|
+
print(f"JSON URL: {receipt.json_url}")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Multiple Items
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from decimal import Decimal
|
|
221
|
+
from moy_nalog import ServiceItem
|
|
222
|
+
|
|
223
|
+
items = [
|
|
224
|
+
ServiceItem(name="Consulting", amount=Decimal("3000"), quantity=2),
|
|
225
|
+
ServiceItem(name="Development", amount=Decimal("10000"), quantity=1),
|
|
226
|
+
ServiceItem(name="Support", amount=Decimal("500"), quantity=4),
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
receipt = await client.create_receipt_multi(items)
|
|
230
|
+
# Total: 3000*2 + 10000*1 + 500*4 = 18000 RUB
|
|
231
|
+
print(f"Total: {receipt.total_amount} RUB")
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### With Client Information
|
|
235
|
+
|
|
236
|
+
#### Individual Client (default)
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from moy_nalog import Client, IncomeType
|
|
240
|
+
|
|
241
|
+
client_info = Client(
|
|
242
|
+
income_type=IncomeType.INDIVIDUAL,
|
|
243
|
+
display_name="Ivan Petrov",
|
|
244
|
+
contact_phone="+79001234567"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
receipt = await client.create_receipt(
|
|
248
|
+
name="Service",
|
|
249
|
+
amount=Decimal("1000"),
|
|
250
|
+
client=client_info
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### Legal Entity (Company)
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
company = Client(
|
|
258
|
+
income_type=IncomeType.LEGAL_ENTITY,
|
|
259
|
+
display_name="OOO Romashka",
|
|
260
|
+
inn="7712345678" # 10 digits for companies
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
receipt = await client.create_receipt(
|
|
264
|
+
name="B2B Service",
|
|
265
|
+
amount=Decimal("50000"),
|
|
266
|
+
client=company
|
|
267
|
+
)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Foreign Organization
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
foreign = Client(
|
|
274
|
+
income_type=IncomeType.FOREIGN_AGENCY,
|
|
275
|
+
display_name="Acme Corporation",
|
|
276
|
+
inn="9909123456"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
receipt = await client.create_receipt(
|
|
280
|
+
name="International consulting",
|
|
281
|
+
amount=Decimal("100000"),
|
|
282
|
+
client=foreign
|
|
283
|
+
)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Payment Types
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from moy_nalog import PaymentType
|
|
290
|
+
|
|
291
|
+
# Cash or card payment (default)
|
|
292
|
+
receipt = await client.create_receipt(
|
|
293
|
+
name="Service",
|
|
294
|
+
amount=Decimal("1000"),
|
|
295
|
+
payment_type=PaymentType.CASH
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Bank transfer (requires legal entity client with INN)
|
|
299
|
+
receipt = await client.create_receipt(
|
|
300
|
+
name="Service",
|
|
301
|
+
amount=Decimal("50000"),
|
|
302
|
+
client=company, # Must have INN
|
|
303
|
+
payment_type=PaymentType.WIRE
|
|
304
|
+
)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Canceling Receipts
|
|
308
|
+
|
|
309
|
+
Cancel a receipt within the same tax period:
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from moy_nalog import CancelReason
|
|
313
|
+
|
|
314
|
+
# Client requested refund
|
|
315
|
+
await client.cancel_receipt(
|
|
316
|
+
receipt_uuid="abc123",
|
|
317
|
+
reason=CancelReason.REFUND
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Receipt created by mistake
|
|
321
|
+
await client.cancel_receipt(
|
|
322
|
+
receipt_uuid="abc123",
|
|
323
|
+
reason=CancelReason.MISTAKE
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Viewing Receipts
|
|
328
|
+
|
|
329
|
+
### Get Income List
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from datetime import datetime
|
|
333
|
+
|
|
334
|
+
# Get recent receipts (default: last 50)
|
|
335
|
+
incomes = await client.get_incomes()
|
|
336
|
+
|
|
337
|
+
for receipt in incomes.items:
|
|
338
|
+
status = "CANCELLED" if receipt.is_cancelled else "ACTIVE"
|
|
339
|
+
print(f"{receipt.uuid}: {receipt.total_amount} RUB [{status}]")
|
|
340
|
+
|
|
341
|
+
print(f"Total count: {incomes.total}")
|
|
342
|
+
print(f"Has more: {incomes.has_more}")
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### With Filters and Pagination
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
incomes = await client.get_incomes(
|
|
349
|
+
from_date=datetime(2024, 1, 1),
|
|
350
|
+
to_date=datetime(2024, 12, 31),
|
|
351
|
+
offset=0,
|
|
352
|
+
limit=50
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Load more if needed
|
|
356
|
+
if incomes.has_more:
|
|
357
|
+
more = await client.get_incomes(offset=50, limit=50)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Get Receipt Details
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
# Get full receipt data as dict
|
|
364
|
+
data = await client.get_receipt("receipt_uuid")
|
|
365
|
+
if data:
|
|
366
|
+
print(f"Services: {data['services']}")
|
|
367
|
+
print(f"Payment type: {data['paymentType']}")
|
|
368
|
+
|
|
369
|
+
# Get printable URL
|
|
370
|
+
url = client.get_receipt_print_url("receipt_uuid")
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Error Handling
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
from moy_nalog import (
|
|
377
|
+
MoyNalogError,
|
|
378
|
+
AuthenticationError,
|
|
379
|
+
InvalidCredentialsError,
|
|
380
|
+
TokenExpiredError,
|
|
381
|
+
SMSError,
|
|
382
|
+
SMSRateLimitError,
|
|
383
|
+
InvalidSMSCodeError,
|
|
384
|
+
ReceiptError,
|
|
385
|
+
ValidationError,
|
|
386
|
+
NetworkError,
|
|
387
|
+
RateLimitError,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
await client.auth_by_password(username, password)
|
|
392
|
+
except InvalidCredentialsError:
|
|
393
|
+
print("Wrong username or password")
|
|
394
|
+
except TokenExpiredError:
|
|
395
|
+
print("Session expired, re-authenticate")
|
|
396
|
+
except AuthenticationError as e:
|
|
397
|
+
print(f"Auth failed: {e.message}")
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
await client.request_sms_code(phone)
|
|
401
|
+
except SMSRateLimitError:
|
|
402
|
+
print("Too many SMS requests, wait a minute")
|
|
403
|
+
except SMSError as e:
|
|
404
|
+
print(f"SMS error: {e.message}")
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
await client.create_receipt("Service", Decimal("1000"))
|
|
408
|
+
except ReceiptError as e:
|
|
409
|
+
print(f"Receipt error: {e.message}")
|
|
410
|
+
print(f"Error code: {e.code}")
|
|
411
|
+
print(f"API response: {e.response}")
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
# Network issues are retried automatically
|
|
415
|
+
await client.get_incomes()
|
|
416
|
+
except NetworkError:
|
|
417
|
+
print("Network unavailable after retries")
|
|
418
|
+
except RateLimitError:
|
|
419
|
+
print("API rate limit exceeded")
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Configuration
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
client = MoyNalogClient(
|
|
426
|
+
# Timezone for receipt timestamps (default: Europe/Moscow)
|
|
427
|
+
timezone="Europe/Moscow",
|
|
428
|
+
|
|
429
|
+
# Request timeout in seconds (default: 30)
|
|
430
|
+
timeout=30.0,
|
|
431
|
+
|
|
432
|
+
# Retry attempts for failed requests (default: 3)
|
|
433
|
+
max_retries=3,
|
|
434
|
+
|
|
435
|
+
# Path to session file for persistence (optional)
|
|
436
|
+
session_file="session.json",
|
|
437
|
+
|
|
438
|
+
# Auto-refresh tokens before expiration (default: True)
|
|
439
|
+
auto_refresh_token=True,
|
|
440
|
+
)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## User Profile
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
profile = await client.get_user_profile()
|
|
447
|
+
|
|
448
|
+
print(f"ID: {profile.id}")
|
|
449
|
+
print(f"INN: {profile.inn}")
|
|
450
|
+
print(f"Phone: {profile.phone}")
|
|
451
|
+
print(f"Email: {profile.email}")
|
|
452
|
+
print(f"Name: {profile.display_name}")
|
|
453
|
+
print(f"Full name: {profile.full_name}")
|
|
454
|
+
print(f"Status: {profile.status}")
|
|
455
|
+
print(f"Registration date: {profile.registration_date}")
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Testing
|
|
459
|
+
|
|
460
|
+
### Unit Tests
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# Install dev dependencies
|
|
464
|
+
pip install -e ".[dev]"
|
|
465
|
+
|
|
466
|
+
# Run unit tests
|
|
467
|
+
pytest
|
|
468
|
+
|
|
469
|
+
# Run with coverage
|
|
470
|
+
pytest --cov=moy_nalog
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Integration Test
|
|
474
|
+
|
|
475
|
+
Interactive script for testing all API functionality with a real account.
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
python scripts/integration_test.py
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**What it tests:**
|
|
482
|
+
- Password and SMS authentication
|
|
483
|
+
- Session persistence (save/restore tokens)
|
|
484
|
+
- Simple receipt creation (1 item, cash payment)
|
|
485
|
+
- Multi-item receipt (3 items with quantities)
|
|
486
|
+
- Receipt with individual client info
|
|
487
|
+
- Receipt with legal entity client (INN required)
|
|
488
|
+
- Receipt with bank transfer payment (WIRE)
|
|
489
|
+
- Income list retrieval with pagination
|
|
490
|
+
- Receipt data retrieval by UUID
|
|
491
|
+
- Receipt cancellation
|
|
492
|
+
|
|
493
|
+
**How it works:**
|
|
494
|
+
1. Choose authentication method (password or SMS)
|
|
495
|
+
2. Enter credentials
|
|
496
|
+
3. Script runs all tests sequentially
|
|
497
|
+
4. All created receipts are cancelled automatically
|
|
498
|
+
5. Detailed log and JSON report are saved to `test_output/` directory
|
|
499
|
+
|
|
500
|
+
**Output:**
|
|
501
|
+
- `test_output/<timestamp>/test_log_*.log` - detailed execution log
|
|
502
|
+
- `test_output/<timestamp>/test_report_*.json` - JSON report with results
|
|
503
|
+
- `test_output/<timestamp>/receipts/` - downloaded receipt files (JSON/HTML)
|
|
504
|
+
|
|
505
|
+
## Requirements
|
|
506
|
+
|
|
507
|
+
- Python 3.10+
|
|
508
|
+
- httpx >= 0.25.0
|
|
509
|
+
- pydantic >= 2.0.0
|
|
510
|
+
|
|
511
|
+
## Disclaimer
|
|
512
|
+
|
|
513
|
+
This is an **unofficial** client. The API may change without notice. Use at your own risk. The author is not responsible for any issues with tax authorities.
|
|
514
|
+
|
|
515
|
+
Always verify receipts in your personal cabinet at [lknpd.nalog.ru](https://lknpd.nalog.ru).
|
|
516
|
+
|
|
517
|
+
## Author
|
|
518
|
+
|
|
519
|
+
Kirill Nikulin (c) 2025 [kirodev.eu](https://kirodev.eu)
|
|
520
|
+
|
|
521
|
+
## License
|
|
522
|
+
|
|
523
|
+
MIT License - see [LICENSE](LICENSE) file.
|
|
524
|
+
|
|
525
|
+
## Contributing
|
|
526
|
+
|
|
527
|
+
Contributions are welcome! Please:
|
|
528
|
+
1. Fork the repository
|
|
529
|
+
2. Create a feature branch
|
|
530
|
+
3. Make your changes
|
|
531
|
+
4. Run tests: `pytest`
|
|
532
|
+
5. Run linting: `ruff check .`
|
|
533
|
+
6. Submit a pull request
|