kra-etims-sdk 0.1.2__tar.gz → 0.1.3__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.
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/PKG-INFO +117 -250
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/README.md +116 -249
- kra_etims_sdk-0.1.3/kra_etims_sdk/base_client.py +149 -0
- kra_etims_sdk-0.1.3/kra_etims_sdk/client.py +95 -0
- kra_etims_sdk-0.1.3/kra_etims_sdk/schemas.py +308 -0
- kra_etims_sdk-0.1.3/kra_etims_sdk/validator.py +21 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/PKG-INFO +117 -250
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/pyproject.toml +1 -1
- kra_etims_sdk-0.1.3/tests/test_etims.py +543 -0
- kra_etims_sdk-0.1.2/kra_etims_sdk/base_client.py +0 -67
- kra_etims_sdk-0.1.2/kra_etims_sdk/client.py +0 -106
- kra_etims_sdk-0.1.2/kra_etims_sdk/schemas.py +0 -212
- kra_etims_sdk-0.1.2/kra_etims_sdk/validator.py +0 -47
- kra_etims_sdk-0.1.2/tests/test_etims.py +0 -222
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/LICENSE +0 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk/__init__.py +0 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk/auth.py +0 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk/exceptions.py +0 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/SOURCES.txt +0 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/dependency_links.txt +0 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/requires.txt +0 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/top_level.txt +0 -0
- {kra_etims_sdk-0.1.2 → kra_etims_sdk-0.1.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kra-etims-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Python SDK for KRA eTIMS OSCU API
|
|
5
5
|
Author: Emmanuel Bartile
|
|
6
6
|
License: MIT
|
|
@@ -39,7 +39,7 @@ Dynamic: license-file
|
|
|
39
39
|

|
|
40
40
|

|
|
41
41
|
|
|
42
|
-
A production-ready **Python SDK** for integrating with the Kenya Revenue Authority (KRA) **eTIMS OSCU** (Online Sales Control Unit) API. Built to match the official Postman collection specifications with strict header compliance, token management, and comprehensive
|
|
42
|
+
A production-ready **Python SDK** for integrating with the Kenya Revenue Authority (KRA) **eTIMS OSCU** (Online Sales Control Unit) API. Built to match the official Postman collection specifications with strict header compliance, token management, and comprehensive payload validation.
|
|
43
43
|
|
|
44
44
|
> ⚠️ **Critical Note**: This SDK implements the **new OSCU specification** (KRA-hosted), *not* the VSCU eTIMS API. OSCU requires device registration, headers, and `cmcKey` lifecycle management.
|
|
45
45
|
|
|
@@ -79,17 +79,6 @@ KRA's **Electronic Tax Invoice Management System (eTIMS)** uses **OSCU** (Online
|
|
|
79
79
|
- Communication key (`cmcKey`) lifecycle management
|
|
80
80
|
- Strict payload schema compliance per KRA specifications
|
|
81
81
|
|
|
82
|
-
### OSCU vs VSCU eTIMS
|
|
83
|
-
|
|
84
|
-
| Feature | OSCU (This SDK) | VSCU eTIMS |
|
|
85
|
-
|---------|-----------------|--------------|
|
|
86
|
-
| **Hosting** | KRA-hosted (cloud) | Self-hosted (on-premise) |
|
|
87
|
-
| **Device Registration** | Mandatory pre-registration | Not required |
|
|
88
|
-
| **Authentication** | Bearer token | Basic auth only |
|
|
89
|
-
| **Communication Key** | `cmcKey` required after init | Not applicable |
|
|
90
|
-
| **API Base URL** | `sbx.kra.go.ke/etims-oscu/api/v1` | `etims-api-sbx.kra.go.ke` |
|
|
91
|
-
| **Header Requirements** | Strict 6-header compliance | Minimal headers |
|
|
92
|
-
|
|
93
82
|
### Receipt Types & Labels Matrix
|
|
94
83
|
|
|
95
84
|
Each receipt is formed from a combination of receipt type and transaction type:
|
|
@@ -200,24 +189,23 @@ Before integration, you **MUST** complete these prerequisites:
|
|
|
200
189
|
```python
|
|
201
190
|
# 1. Initialize FIRST (returns cmcKey)
|
|
202
191
|
response = etims.select_init_osdc_info({
|
|
203
|
-
"tin": config
|
|
204
|
-
"bhfId": config
|
|
205
|
-
"dvcSrlNo": config
|
|
192
|
+
"tin": config["oscu"]["tin"],
|
|
193
|
+
"bhfId": config["oscu"]["bhf_id"],
|
|
194
|
+
"dvcSrlNo": config["oscu"]["device_serial"], # KRA-approved serial
|
|
206
195
|
})
|
|
207
196
|
|
|
208
|
-
# 2. Extract cmcKey
|
|
209
|
-
cmc_key = response.get("cmcKey")
|
|
197
|
+
# 2. Extract cmcKey
|
|
198
|
+
cmc_key = response.get("cmcKey")
|
|
210
199
|
|
|
211
200
|
# 3. Update config IMMEDIATELY
|
|
212
|
-
config
|
|
201
|
+
config["oscu"]["cmc_key"] = cmc_key
|
|
213
202
|
|
|
214
203
|
# 4. Recreate client with updated config (critical!)
|
|
215
204
|
etims = EtimsClient(config, auth)
|
|
216
|
-
|
|
217
|
-
# 5. ALL subsequent requests require cmcKey in headers
|
|
218
|
-
etims.select_code_list(...)
|
|
219
205
|
```
|
|
220
206
|
|
|
207
|
+
> 🔔 Note: `cmcKey` is only required for certain write operations (branch/user/insurance), not all endpoints.
|
|
208
|
+
|
|
221
209
|
### 3. Invoice Numbering Rules
|
|
222
210
|
- **MUST be sequential integers** (1, 2, 3...) – **NOT strings** (`INV001`)
|
|
223
211
|
- Must be unique per branch office (`bhfId`)
|
|
@@ -269,94 +257,52 @@ etims.select_code_list(...)
|
|
|
269
257
|
|
|
270
258
|
```bash
|
|
271
259
|
pip install kra-etims-sdk
|
|
272
|
-
# OR with dev dependencies
|
|
273
|
-
pip install "kra-etims-sdk[dev]"
|
|
274
260
|
```
|
|
275
261
|
|
|
276
262
|
### Requirements
|
|
277
263
|
- Python 3.9+
|
|
278
264
|
- `requests` (≥2.31)
|
|
279
265
|
- `pydantic` (≥2.0)
|
|
280
|
-
|
|
266
|
+
|
|
267
|
+
> 💡 The SDK uses plain dictionaries for configuration — no custom config class required.
|
|
281
268
|
|
|
282
269
|
---
|
|
283
270
|
|
|
284
271
|
## Configuration
|
|
285
272
|
|
|
273
|
+
Define your config as a **plain Python dictionary**:
|
|
274
|
+
|
|
286
275
|
```python
|
|
287
|
-
from kra_etims_sdk import KraEtimsConfig
|
|
288
276
|
import os
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
auth
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
},
|
|
300
|
-
"production": {
|
|
301
|
-
"token_url": "https://kra.go.ke/v1/token/generate".strip(),
|
|
302
|
-
"consumer_key": os.environ["KRA_PROD_CONSUMER_KEY"],
|
|
303
|
-
"consumer_secret": os.environ["KRA_PROD_CONSUMER_SECRET"],
|
|
277
|
+
import tempfile
|
|
278
|
+
|
|
279
|
+
config = {
|
|
280
|
+
'env': 'sbx', # 'sbx' = sandbox, 'prod' = production
|
|
281
|
+
'cache_file': os.path.join(tempfile.gettempdir(), 'kra_etims_token.json'),
|
|
282
|
+
'auth': {
|
|
283
|
+
'sbx': {
|
|
284
|
+
'token_url': 'https://sbx.kra.go.ke/v1/token/generate',
|
|
285
|
+
'consumer_key': os.getenv('KRA_CONSUMER_KEY'),
|
|
286
|
+
'consumer_secret': os.getenv('KRA_CONSUMER_SECRET'),
|
|
304
287
|
}
|
|
305
288
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
"sandbox": {"base_url": "https://etims-api-sbx.kra.go.ke/etims-api".strip()},
|
|
309
|
-
"production": {"base_url": "https://etims-api.kra.go.ke/etims-api".strip()}
|
|
310
|
-
},
|
|
311
|
-
|
|
312
|
-
oscu={
|
|
313
|
-
"tin": os.environ["KRA_TIN"],
|
|
314
|
-
"bhf_id": os.environ["KRA_BHF_ID"],
|
|
315
|
-
"cmc_key": os.environ["CMC_KEY"] # Set AFTER initialization
|
|
316
|
-
},
|
|
317
|
-
|
|
318
|
-
endpoints={
|
|
319
|
-
# INITIALIZATION (ONLY endpoint without tin/bhfId/cmcKey headers)
|
|
320
|
-
"selectInitOsdcInfo": "/selectInitOsdcInfo",
|
|
321
|
-
|
|
322
|
-
# DATA MANAGEMENT
|
|
323
|
-
"selectCodeList": "/selectCodeList",
|
|
324
|
-
"selectItemClsList": "/selectItemClass",
|
|
325
|
-
"selectBhfList": "/branchList",
|
|
326
|
-
"selectTaxpayerInfo": "/selectTaxpayerInfo",
|
|
327
|
-
"selectCustomerList": "/selectCustomerList",
|
|
328
|
-
"selectNoticeList": "/selectNoticeList",
|
|
329
|
-
|
|
330
|
-
# BRANCH MANAGEMENT
|
|
331
|
-
"branchInsuranceInfo": "/branchInsuranceInfo",
|
|
332
|
-
"branchUserAccount": "/branchUserAccount",
|
|
333
|
-
"branchSendCustomerInfo": "/branchSendCustomerInfo",
|
|
334
|
-
|
|
335
|
-
# ITEM MANAGEMENT
|
|
336
|
-
"saveItem": "/saveItem",
|
|
337
|
-
"itemInfo": "/itemInfo",
|
|
338
|
-
|
|
339
|
-
# PURCHASE MANAGEMENT
|
|
340
|
-
"selectPurchaseTrns": "/getPurchaseTransactionInfo",
|
|
341
|
-
"sendPurchaseTransactionInfo": "/sendPurchaseTransactionInfo",
|
|
342
|
-
|
|
343
|
-
# SALES MANAGEMENT
|
|
344
|
-
"sendSalesTransaction": "/sendSalesTransaction",
|
|
345
|
-
"selectSalesTrns": "/selectSalesTransactions",
|
|
346
|
-
"selectInvoiceDetail": "/selectInvoiceDetail",
|
|
347
|
-
|
|
348
|
-
# STOCK MANAGEMENT (NESTED PATHS - CRITICAL)
|
|
349
|
-
"insertStockIO": "/insert/stockIO", # ← slash in path
|
|
350
|
-
"saveStockMaster": "/save/stockMaster", # ← slash in path
|
|
351
|
-
"selectMoveList": "/selectStockMoveLists",
|
|
289
|
+
'api': {
|
|
290
|
+
'sbx': {'base_url': 'https://etims-api-sbx.kra.go.ke/etims-api'}
|
|
352
291
|
},
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
)
|
|
292
|
+
'http': {'timeout': 30},
|
|
293
|
+
'oscu': {
|
|
294
|
+
'tin': os.getenv('KRA_TIN'),
|
|
295
|
+
'bhf_id': os.getenv('KRA_BHF_ID') or '01',
|
|
296
|
+
'device_serial': os.getenv('DEVICE_SERIAL'),
|
|
297
|
+
'cmc_key': '', # populated after initialization
|
|
298
|
+
}
|
|
299
|
+
}
|
|
356
300
|
```
|
|
357
301
|
|
|
358
|
-
>
|
|
359
|
-
>
|
|
302
|
+
> ✅ **Required environment variables**:
|
|
303
|
+
> `KRA_CONSUMER_KEY`, `KRA_CONSUMER_SECRET`, `KRA_TIN`, `DEVICE_SERIAL`
|
|
304
|
+
>
|
|
305
|
+
> ⚠️ **Never include trailing spaces in URLs** — they cause silent connection failures.
|
|
360
306
|
|
|
361
307
|
---
|
|
362
308
|
|
|
@@ -364,164 +310,109 @@ config = KraEtimsConfig(
|
|
|
364
310
|
|
|
365
311
|
### Step 1: Initialize SDK
|
|
366
312
|
```python
|
|
367
|
-
from kra_etims_sdk import AuthClient
|
|
368
|
-
from kra_etims_sdk.
|
|
313
|
+
from kra_etims_sdk.auth import AuthClient
|
|
314
|
+
from kra_etims_sdk.client import EtimsClient
|
|
369
315
|
|
|
370
316
|
auth = AuthClient(config)
|
|
371
317
|
etims = EtimsClient(config, auth)
|
|
372
318
|
```
|
|
373
319
|
|
|
374
|
-
### Step 2: Authenticate
|
|
320
|
+
### Step 2: Authenticate
|
|
375
321
|
```python
|
|
376
322
|
try:
|
|
377
|
-
#
|
|
378
|
-
token = auth.token(
|
|
379
|
-
print(f"✅ Token
|
|
380
|
-
except
|
|
323
|
+
auth.forget_token() # Clear cached token
|
|
324
|
+
token = auth.token(force=True)
|
|
325
|
+
print(f"✅ Token OK: {token[:25]}...")
|
|
326
|
+
except Exception as e:
|
|
381
327
|
print(f"❌ Authentication failed: {e}")
|
|
382
328
|
exit(1)
|
|
383
329
|
```
|
|
384
330
|
|
|
385
|
-
### Step 3: OSCU Initialization (
|
|
331
|
+
### Step 3: OSCU Initialization (If Needed)
|
|
332
|
+
> Only required for `cmcKey`-dependent operations (e.g., saving branch users or insurance).
|
|
333
|
+
|
|
386
334
|
```python
|
|
387
335
|
try:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
"tin": config.oscu["tin"],
|
|
393
|
-
"bhfId": config.oscu["bhf_id"],
|
|
394
|
-
"dvcSrlNo": config.oscu["device_serial"],
|
|
336
|
+
init_resp = etims.select_init_osdc_info({
|
|
337
|
+
'tin': config['oscu']['tin'],
|
|
338
|
+
'bhfId': config['oscu']['bhf_id'],
|
|
339
|
+
'dvcSrlNo': config['oscu']['device_serial'],
|
|
395
340
|
})
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
cmc_key = response.get("cmcKey") or response.get("data", {}).get("cmcKey")
|
|
341
|
+
|
|
342
|
+
cmc_key = init_resp.get('cmcKey')
|
|
399
343
|
if not cmc_key:
|
|
400
344
|
raise RuntimeError("cmcKey not found in response")
|
|
401
345
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
# Recreate client with updated config (critical!)
|
|
406
|
-
etims = EtimsClient(config, auth)
|
|
407
|
-
|
|
346
|
+
config['oscu']['cmc_key'] = cmc_key
|
|
347
|
+
etims = EtimsClient(config, auth) # Reinitialize to inject cmcKey
|
|
408
348
|
print(f"✅ OSCU initialized. cmcKey: {cmc_key[:15]}...")
|
|
409
349
|
|
|
410
|
-
except
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
print(" → Device serial not registered with KRA")
|
|
414
|
-
print(" → Contact timsupport@kra.go.ke for approved serial")
|
|
415
|
-
print(" → Common sandbox test value: 'dvcv1130' (may work if pre-provisioned)")
|
|
416
|
-
exit(1)
|
|
417
|
-
raise
|
|
350
|
+
except Exception as e:
|
|
351
|
+
print(f"❌ OSCU Init failed: {e}")
|
|
352
|
+
exit(1)
|
|
418
353
|
```
|
|
419
354
|
|
|
420
|
-
### Step 4: Business Operations
|
|
355
|
+
### Step 4: Business Operations
|
|
421
356
|
```python
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
print(f"❌ Code list fetch failed: {e}")
|
|
439
|
-
|
|
440
|
-
# Send sales transaction (FULL Postman payload structure)
|
|
441
|
-
try:
|
|
442
|
-
response = etims.send_sales_transaction({
|
|
443
|
-
"invcNo": 1, # INTEGER (sequential) - NOT string!
|
|
444
|
-
"custTin": "A123456789Z",
|
|
445
|
-
"custNm": "Test Customer",
|
|
446
|
-
"salesTyCd": "N", # N=Normal, R=Return
|
|
447
|
-
"rcptTyCd": "R", # R=Receipt
|
|
448
|
-
"pmtTyCd": "01", # 01=Cash
|
|
449
|
-
"salesSttsCd": "01", # 01=Completed
|
|
450
|
-
"cfmDt": kra_date(), # YYYYMMDDHHmmss
|
|
451
|
-
"salesDt": kra_date()[:8], # YYYYMMDD (NO time)
|
|
452
|
-
"totItemCnt": 1,
|
|
453
|
-
# TAX BREAKDOWN (ALL 15 FIELDS REQUIRED)
|
|
454
|
-
"taxblAmtA": 0.00, "taxblAmtB": 0.00, "taxblAmtC": 81000.00,
|
|
455
|
-
"taxblAmtD": 0.00, "taxblAmtE": 0.00,
|
|
456
|
-
"taxRtA": 0.00, "taxRtB": 0.00, "taxRtC": 0.00,
|
|
457
|
-
"taxRtD": 0.00, "taxRtE": 0.00,
|
|
458
|
-
"taxAmtA": 0.00, "taxAmtB": 0.00, "taxAmtC": 0.00,
|
|
459
|
-
"taxAmtD": 0.00, "taxAmtE": 0.00,
|
|
460
|
-
"totTaxblAmt": 81000.00,
|
|
461
|
-
"totTaxAmt": 0.00,
|
|
462
|
-
"totAmt": 81000.00,
|
|
463
|
-
"regrId": "Admin", "regrNm": "Admin",
|
|
464
|
-
"modrId": "Admin", "modrNm": "Admin",
|
|
465
|
-
"itemList": [{
|
|
466
|
-
"itemSeq": 1,
|
|
467
|
-
"itemCd": "KE2NTBA00000001", # Must exist in KRA system
|
|
468
|
-
"itemClsCd": "1000000000",
|
|
469
|
-
"itemNm": "Brand A",
|
|
470
|
-
"barCd": "", # Nullable but REQUIRED field
|
|
471
|
-
"pkgUnitCd": "NT",
|
|
472
|
-
"pkg": 1, # Package quantity
|
|
473
|
-
"qtyUnitCd": "BA",
|
|
474
|
-
"qty": 90.0,
|
|
475
|
-
"prc": 1000.00,
|
|
476
|
-
"splyAmt": 81000.00,
|
|
477
|
-
"dcRt": 10.0, # Discount rate %
|
|
478
|
-
"dcAmt": 9000.00, # Discount amount
|
|
479
|
-
"taxTyCd": "C", # C = Zero-rated/Exempt
|
|
480
|
-
"taxblAmt": 81000.00,
|
|
481
|
-
"taxAmt": 0.00,
|
|
482
|
-
"totAmt": 81000.00, # splyAmt - dcAmt + taxAmt
|
|
483
|
-
}],
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
print(f"✅ Sales transaction sent (resultCd: {response['resultCd']})")
|
|
487
|
-
print(f"Receipt Signature: {response['data']['rcptSign']}")
|
|
488
|
-
|
|
489
|
-
except ValidationException as e:
|
|
490
|
-
print("❌ Validation failed:")
|
|
491
|
-
for err in e.errors:
|
|
492
|
-
field = err.get('loc', [])[0] if err.get('loc') else 'unknown'
|
|
493
|
-
print(f" • {field}: {err.get('msg', 'validation error')}")
|
|
357
|
+
# Fetch code list
|
|
358
|
+
codes = etims.select_code_list({'lastReqDt': '20260101000000'})
|
|
359
|
+
|
|
360
|
+
# Save an item
|
|
361
|
+
item_resp = etims.save_item({
|
|
362
|
+
'itemCd': 'KE1NTXU0000006',
|
|
363
|
+
'itemClsCd': '5059690800',
|
|
364
|
+
'itemNm': 'Test Material',
|
|
365
|
+
'pkgUnitCd': 'NT',
|
|
366
|
+
'qtyUnitCd': 'U',
|
|
367
|
+
'taxTyCd': 'B',
|
|
368
|
+
'dftPrc': 3500,
|
|
369
|
+
'useYn': 'Y',
|
|
370
|
+
'regrId': 'Test', 'regrNm': 'Test',
|
|
371
|
+
'modrId': 'Test', 'modrNm': 'Test',
|
|
372
|
+
})
|
|
494
373
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
374
|
+
# Save purchase transaction (full tax breakdown required)
|
|
375
|
+
purchase_resp = etims.save_purchase({
|
|
376
|
+
'invcNo': 1,
|
|
377
|
+
'spplrTin': 'A123456789Z',
|
|
378
|
+
'pchsTyCd': 'N',
|
|
379
|
+
'rcptTyCd': 'P',
|
|
380
|
+
'pmtTyCd': '01',
|
|
381
|
+
'pchsSttsCd': '02',
|
|
382
|
+
'cfmDt': '20260206120000',
|
|
383
|
+
'pchsDt': '20260206',
|
|
384
|
+
'totItemCnt': 1,
|
|
385
|
+
'taxblAmtA': 0, 'taxblAmtB': 10500, 'taxblAmtC': 0, 'taxblAmtD': 0, 'taxblAmtE': 0,
|
|
386
|
+
'taxRtA': 0, 'taxRtB': 18, 'taxRtC': 0, 'taxRtD': 0, 'taxRtE': 0,
|
|
387
|
+
'taxAmtA': 0, 'taxAmtB': 1890, 'taxAmtC': 0, 'taxAmtD': 0, 'taxAmtE': 0,
|
|
388
|
+
'totTaxblAmt': 10500,
|
|
389
|
+
'totTaxAmt': 1890,
|
|
390
|
+
'totAmt': 10500,
|
|
391
|
+
'regrId': 'Test', 'regrNm': 'Test',
|
|
392
|
+
'modrId': 'Test', 'modrNm': 'Test',
|
|
393
|
+
'itemList': [/* ... */],
|
|
394
|
+
})
|
|
499
395
|
```
|
|
500
396
|
|
|
397
|
+
> ✅ All payloads are validated using internal Pydantic schemas before sending.
|
|
398
|
+
|
|
501
399
|
---
|
|
502
400
|
|
|
503
401
|
## API Reference
|
|
504
402
|
|
|
505
|
-
### Functional Categories
|
|
403
|
+
### Functional Categories & Methods
|
|
506
404
|
|
|
507
|
-
| Category |
|
|
508
|
-
|
|
509
|
-
| **Initialization** |
|
|
510
|
-
| **Data Management** |
|
|
511
|
-
| **Branch Management** |
|
|
512
|
-
| **Item Management** |
|
|
513
|
-
| **Purchase Management** |
|
|
514
|
-
| **Sales Management** |
|
|
515
|
-
| **Stock Management** |
|
|
405
|
+
| Category | Methods |
|
|
406
|
+
|--------|--------|
|
|
407
|
+
| **Initialization** | `select_init_osdc_info()` |
|
|
408
|
+
| **Data Management** | `select_code_list()`, `select_customer()`, `select_notice_list()`, `select_branches()`, `select_item_classes()`, `select_items()` |
|
|
409
|
+
| **Branch Management** | `save_branch_customer()`, `save_branch_user()`, `save_branch_insurance()` |
|
|
410
|
+
| **Item Management** | `save_item()`, `save_item_composition()` |
|
|
411
|
+
| **Purchase Management** | `save_purchase()`, `select_purchases()` |
|
|
412
|
+
| **Sales Management** | `save_sales_transaction()` |
|
|
413
|
+
| **Stock Management** | `save_stock_io()`, `save_stock_master()`, `select_stock_movement()` |
|
|
516
414
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
| Class | Purpose |
|
|
520
|
-
|-------|---------|
|
|
521
|
-
| `AuthClient` | Token generation, caching (60s buffer), and refresh management |
|
|
522
|
-
| `BaseClient` | HTTP request handling, header management, error unwrapping |
|
|
523
|
-
| `EtimsClient` | Business endpoint methods (all 8 functional categories) |
|
|
524
|
-
| `Validator` | Payload validation against KRA schemas (Pydantic v2) |
|
|
415
|
+
> 🔍 Each method maps to a KRA endpoint alias defined internally (e.g., `save_purchase` → `insertTrnsPurchase`).
|
|
525
416
|
|
|
526
417
|
---
|
|
527
418
|
|
|
@@ -555,38 +446,20 @@ except ApiException as e:
|
|
|
555
446
|
### Handling Pattern
|
|
556
447
|
```python
|
|
557
448
|
try:
|
|
558
|
-
response = etims.
|
|
449
|
+
response = etims.save_purchase(payload)
|
|
559
450
|
|
|
560
451
|
except ValidationException as e:
|
|
561
452
|
print("❌ Validation failed:")
|
|
562
|
-
for
|
|
563
|
-
|
|
564
|
-
print(f" • {field}: {error.get('msg', 'validation error')}")
|
|
453
|
+
for field, msg in e.details.items():
|
|
454
|
+
print(f" • {field}: {msg}")
|
|
565
455
|
|
|
566
456
|
except ApiException as e:
|
|
567
457
|
print(f"❌ KRA API Error ({e.error_code}): {e}")
|
|
568
|
-
|
|
569
|
-
# Get full KRA response for debugging
|
|
570
458
|
if e.details and 'resultMsg' in e.details:
|
|
571
459
|
print(f"KRA Message: {e.details['resultMsg']}")
|
|
572
|
-
|
|
573
|
-
# Handle specific error codes
|
|
574
|
-
if e.error_code == "901":
|
|
575
|
-
print("→ Device serial not registered with KRA")
|
|
576
|
-
elif e.error_code == "902":
|
|
577
|
-
print("→ cmcKey expired - reinitialize OSCU")
|
|
578
|
-
elif e.error_code == "500":
|
|
579
|
-
print("→ Invalid payload - check date formats/tax fields")
|
|
580
460
|
|
|
581
461
|
except AuthenticationException as e:
|
|
582
462
|
print(f"❌ Authentication failed: {e}")
|
|
583
|
-
|
|
584
|
-
# Attempt token refresh
|
|
585
|
-
try:
|
|
586
|
-
auth.token(force_refresh=True)
|
|
587
|
-
# Retry operation...
|
|
588
|
-
except Exception as ex:
|
|
589
|
-
print(f"Token refresh failed: {ex}")
|
|
590
463
|
```
|
|
591
464
|
|
|
592
465
|
### Comprehensive KRA Error Codes
|
|
@@ -623,18 +496,14 @@ except AuthenticationException as e:
|
|
|
623
496
|
**Cause**: cmcKey expired or not set in config
|
|
624
497
|
**Solution**:
|
|
625
498
|
```python
|
|
626
|
-
|
|
627
|
-
config.oscu["cmc_key"] = extracted_cmc_key
|
|
499
|
+
config['oscu']['cmc_key'] = extracted_cmc_key
|
|
628
500
|
etims = EtimsClient(config, auth) # MUST recreate client
|
|
629
501
|
```
|
|
630
502
|
|
|
631
503
|
### ❌ Trailing spaces in URLs
|
|
632
504
|
|
|
633
505
|
**Cause**: Copy-paste errors from documentation
|
|
634
|
-
**Solution**: Always
|
|
635
|
-
```python
|
|
636
|
-
"token_url": "https://sbx.kra.go.ke/v1/token/generate ".strip(),
|
|
637
|
-
```
|
|
506
|
+
**Solution**: Always verify URLs have no trailing whitespace.
|
|
638
507
|
|
|
639
508
|
### ❌ Invoice number rejected
|
|
640
509
|
|
|
@@ -642,7 +511,6 @@ etims = EtimsClient(config, auth) # MUST recreate client
|
|
|
642
511
|
**Solution**: Use sequential integers starting from 1:
|
|
643
512
|
```python
|
|
644
513
|
"invcNo": 1, # ✅ Correct
|
|
645
|
-
# NOT "INV001" ❌
|
|
646
514
|
```
|
|
647
515
|
|
|
648
516
|
---
|
|
@@ -741,7 +609,7 @@ KRA mandates successful completion of automated tests before verification:
|
|
|
741
609
|
- **Email**: ebartile@gmail.com (for integration guidance)
|
|
742
610
|
- **Emergency Hotline**: +254757807150 (business hours only)
|
|
743
611
|
|
|
744
|
-
> ℹ️ **Disclaimer**: This SDK is not
|
|
612
|
+
> ℹ️ **Disclaimer**: This SDK is independently developed by Paybill Kenya and is **not affiliated with or endorsed by the Kenya Revenue Authority (KRA)**. Always verify integration requirements with KRA before production deployment. KRA may update API specifications without notice – monitor [GavaConnect Portal](https://developer.go.ke) for updates.
|
|
745
613
|
|
|
746
614
|
---
|
|
747
615
|
|
|
@@ -749,7 +617,7 @@ KRA mandates successful completion of automated tests before verification:
|
|
|
749
617
|
|
|
750
618
|
MIT License
|
|
751
619
|
|
|
752
|
-
Copyright © 2024
|
|
620
|
+
Copyright © 2024–2026 Bartile Emmanuel / Paybill Kenya
|
|
753
621
|
|
|
754
622
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
755
623
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -776,4 +644,3 @@ SOFTWARE.
|
|
|
776
644
|
This SDK was developed by **Bartile Emmanuel** for Paybill Kenya to simplify KRA eTIMS OSCU integration for Kenyan businesses. Special thanks to KRA for providing comprehensive API documentation and Postman collections.
|
|
777
645
|
|
|
778
646
|
> 🇰🇪 **Proudly Made in Kenya** – Supporting digital tax compliance for East Africa's largest economy.
|
|
779
|
-
> *Tested on KRA Sandbox • Built with Python • Pydantic v2 Validation • Production Ready*
|