kra-etims-sdk 0.1.1__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.1 → kra_etims_sdk-0.1.3}/PKG-INFO +120 -252
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/README.md +119 -251
- 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.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/PKG-INFO +120 -252
- {kra_etims_sdk-0.1.1 → 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.1/kra_etims_sdk/base_client.py +0 -67
- kra_etims_sdk-0.1.1/kra_etims_sdk/client.py +0 -106
- kra_etims_sdk-0.1.1/kra_etims_sdk/schemas.py +0 -212
- kra_etims_sdk-0.1.1/kra_etims_sdk/validator.py +0 -47
- kra_etims_sdk-0.1.1/tests/test_etims.py +0 -222
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/LICENSE +0 -0
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk/__init__.py +0 -0
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk/auth.py +0 -0
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk/exceptions.py +0 -0
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/SOURCES.txt +0 -0
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/dependency_links.txt +0 -0
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/requires.txt +0 -0
- {kra_etims_sdk-0.1.1 → kra_etims_sdk-0.1.3}/kra_etims_sdk.egg-info/top_level.txt +0 -0
- {kra_etims_sdk-0.1.1 → 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:
|
|
@@ -191,7 +180,8 @@ flowchart TD
|
|
|
191
180
|
Before integration, you **MUST** complete these prerequisites:
|
|
192
181
|
|
|
193
182
|
### 1. Device Registration (MANDATORY)
|
|
194
|
-
- Register OSCU device via [eTIMS Taxpayer
|
|
183
|
+
- Register OSCU device via [eTIMS Taxpayer Production Portal](https://etims.kra.go.ke)
|
|
184
|
+
- Register OSCU device via [eTIMS Taxpayer Sandbox Portal](https://etims-sbx.kra.go.ke)
|
|
195
185
|
- Obtain **approved device serial number** (`dvcSrlNo`)
|
|
196
186
|
- ⚠️ **Dynamic/unregistered device serials fail with `resultCd: 901`** ("It is not valid device")
|
|
197
187
|
|
|
@@ -199,24 +189,23 @@ Before integration, you **MUST** complete these prerequisites:
|
|
|
199
189
|
```python
|
|
200
190
|
# 1. Initialize FIRST (returns cmcKey)
|
|
201
191
|
response = etims.select_init_osdc_info({
|
|
202
|
-
"tin": config
|
|
203
|
-
"bhfId": config
|
|
204
|
-
"dvcSrlNo": "
|
|
192
|
+
"tin": config["oscu"]["tin"],
|
|
193
|
+
"bhfId": config["oscu"]["bhf_id"],
|
|
194
|
+
"dvcSrlNo": config["oscu"]["device_serial"], # KRA-approved serial
|
|
205
195
|
})
|
|
206
196
|
|
|
207
|
-
# 2. Extract cmcKey
|
|
208
|
-
cmc_key = response.get("cmcKey")
|
|
197
|
+
# 2. Extract cmcKey
|
|
198
|
+
cmc_key = response.get("cmcKey")
|
|
209
199
|
|
|
210
200
|
# 3. Update config IMMEDIATELY
|
|
211
|
-
config
|
|
201
|
+
config["oscu"]["cmc_key"] = cmc_key
|
|
212
202
|
|
|
213
203
|
# 4. Recreate client with updated config (critical!)
|
|
214
204
|
etims = EtimsClient(config, auth)
|
|
215
|
-
|
|
216
|
-
# 5. ALL subsequent requests require cmcKey in headers
|
|
217
|
-
etims.select_code_list(...)
|
|
218
205
|
```
|
|
219
206
|
|
|
207
|
+
> 🔔 Note: `cmcKey` is only required for certain write operations (branch/user/insurance), not all endpoints.
|
|
208
|
+
|
|
220
209
|
### 3. Invoice Numbering Rules
|
|
221
210
|
- **MUST be sequential integers** (1, 2, 3...) – **NOT strings** (`INV001`)
|
|
222
211
|
- Must be unique per branch office (`bhfId`)
|
|
@@ -268,94 +257,52 @@ etims.select_code_list(...)
|
|
|
268
257
|
|
|
269
258
|
```bash
|
|
270
259
|
pip install kra-etims-sdk
|
|
271
|
-
# OR with dev dependencies
|
|
272
|
-
pip install "kra-etims-sdk[dev]"
|
|
273
260
|
```
|
|
274
261
|
|
|
275
262
|
### Requirements
|
|
276
263
|
- Python 3.9+
|
|
277
264
|
- `requests` (≥2.31)
|
|
278
265
|
- `pydantic` (≥2.0)
|
|
279
|
-
|
|
266
|
+
|
|
267
|
+
> 💡 The SDK uses plain dictionaries for configuration — no custom config class required.
|
|
280
268
|
|
|
281
269
|
---
|
|
282
270
|
|
|
283
271
|
## Configuration
|
|
284
272
|
|
|
273
|
+
Define your config as a **plain Python dictionary**:
|
|
274
|
+
|
|
285
275
|
```python
|
|
286
|
-
from kra_etims_sdk import KraEtimsConfig
|
|
287
276
|
import os
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
auth
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
},
|
|
299
|
-
"production": {
|
|
300
|
-
"token_url": "https://kra.go.ke/v1/token/generate".strip(),
|
|
301
|
-
"consumer_key": os.environ["KRA_PROD_CONSUMER_KEY"],
|
|
302
|
-
"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'),
|
|
303
287
|
}
|
|
304
288
|
},
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"sandbox": {"base_url": "https://sbx.kra.go.ke/etims-oscu/api/v1".strip()},
|
|
308
|
-
"production": {"base_url": "https://api.developer.go.ke/etims-oscu/api/v1".strip()}
|
|
309
|
-
},
|
|
310
|
-
|
|
311
|
-
oscu={
|
|
312
|
-
"tin": os.environ["KRA_TIN"],
|
|
313
|
-
"bhf_id": os.environ["KRA_BHF_ID"],
|
|
314
|
-
"cmc_key": "", # Set AFTER initialization
|
|
315
|
-
},
|
|
316
|
-
|
|
317
|
-
endpoints={
|
|
318
|
-
# INITIALIZATION (ONLY endpoint without tin/bhfId/cmcKey headers)
|
|
319
|
-
"selectInitOsdcInfo": "/selectInitOsdcInfo",
|
|
320
|
-
|
|
321
|
-
# DATA MANAGEMENT
|
|
322
|
-
"selectCodeList": "/selectCodeList",
|
|
323
|
-
"selectItemClsList": "/selectItemClass",
|
|
324
|
-
"selectBhfList": "/branchList",
|
|
325
|
-
"selectTaxpayerInfo": "/selectTaxpayerInfo",
|
|
326
|
-
"selectCustomerList": "/selectCustomerList",
|
|
327
|
-
"selectNoticeList": "/selectNoticeList",
|
|
328
|
-
|
|
329
|
-
# BRANCH MANAGEMENT
|
|
330
|
-
"branchInsuranceInfo": "/branchInsuranceInfo",
|
|
331
|
-
"branchUserAccount": "/branchUserAccount",
|
|
332
|
-
"branchSendCustomerInfo": "/branchSendCustomerInfo",
|
|
333
|
-
|
|
334
|
-
# ITEM MANAGEMENT
|
|
335
|
-
"saveItem": "/saveItem",
|
|
336
|
-
"itemInfo": "/itemInfo",
|
|
337
|
-
|
|
338
|
-
# PURCHASE MANAGEMENT
|
|
339
|
-
"selectPurchaseTrns": "/getPurchaseTransactionInfo",
|
|
340
|
-
"sendPurchaseTransactionInfo": "/sendPurchaseTransactionInfo",
|
|
341
|
-
|
|
342
|
-
# SALES MANAGEMENT
|
|
343
|
-
"sendSalesTransaction": "/sendSalesTransaction",
|
|
344
|
-
"selectSalesTrns": "/selectSalesTransactions",
|
|
345
|
-
"selectInvoiceDetail": "/selectInvoiceDetail",
|
|
346
|
-
|
|
347
|
-
# STOCK MANAGEMENT (NESTED PATHS - CRITICAL)
|
|
348
|
-
"insertStockIO": "/insert/stockIO", # ← slash in path
|
|
349
|
-
"saveStockMaster": "/save/stockMaster", # ← slash in path
|
|
350
|
-
"selectMoveList": "/selectStockMoveLists",
|
|
289
|
+
'api': {
|
|
290
|
+
'sbx': {'base_url': 'https://etims-api-sbx.kra.go.ke/etims-api'}
|
|
351
291
|
},
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
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
|
+
}
|
|
355
300
|
```
|
|
356
301
|
|
|
357
|
-
>
|
|
358
|
-
>
|
|
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.
|
|
359
306
|
|
|
360
307
|
---
|
|
361
308
|
|
|
@@ -363,164 +310,109 @@ config = KraEtimsConfig(
|
|
|
363
310
|
|
|
364
311
|
### Step 1: Initialize SDK
|
|
365
312
|
```python
|
|
366
|
-
from kra_etims_sdk import AuthClient
|
|
367
|
-
from kra_etims_sdk.
|
|
313
|
+
from kra_etims_sdk.auth import AuthClient
|
|
314
|
+
from kra_etims_sdk.client import EtimsClient
|
|
368
315
|
|
|
369
316
|
auth = AuthClient(config)
|
|
370
317
|
etims = EtimsClient(config, auth)
|
|
371
318
|
```
|
|
372
319
|
|
|
373
|
-
### Step 2: Authenticate
|
|
320
|
+
### Step 2: Authenticate
|
|
374
321
|
```python
|
|
375
322
|
try:
|
|
376
|
-
#
|
|
377
|
-
token = auth.token(
|
|
378
|
-
print(f"✅ Token
|
|
379
|
-
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:
|
|
380
327
|
print(f"❌ Authentication failed: {e}")
|
|
381
328
|
exit(1)
|
|
382
329
|
```
|
|
383
330
|
|
|
384
|
-
### 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
|
+
|
|
385
334
|
```python
|
|
386
335
|
try:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
"tin": config.oscu["tin"],
|
|
392
|
-
"bhfId": config.oscu["bhf_id"],
|
|
393
|
-
"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'],
|
|
394
340
|
})
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
cmc_key = response.get("cmcKey") or response.get("data", {}).get("cmcKey")
|
|
341
|
+
|
|
342
|
+
cmc_key = init_resp.get('cmcKey')
|
|
398
343
|
if not cmc_key:
|
|
399
344
|
raise RuntimeError("cmcKey not found in response")
|
|
400
345
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
# Recreate client with updated config (critical!)
|
|
405
|
-
etims = EtimsClient(config, auth)
|
|
406
|
-
|
|
346
|
+
config['oscu']['cmc_key'] = cmc_key
|
|
347
|
+
etims = EtimsClient(config, auth) # Reinitialize to inject cmcKey
|
|
407
348
|
print(f"✅ OSCU initialized. cmcKey: {cmc_key[:15]}...")
|
|
408
349
|
|
|
409
|
-
except
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
print(" → Device serial not registered with KRA")
|
|
413
|
-
print(" → Contact timsupport@kra.go.ke for approved serial")
|
|
414
|
-
print(" → Common sandbox test value: 'dvcv1130' (may work if pre-provisioned)")
|
|
415
|
-
exit(1)
|
|
416
|
-
raise
|
|
350
|
+
except Exception as e:
|
|
351
|
+
print(f"❌ OSCU Init failed: {e}")
|
|
352
|
+
exit(1)
|
|
417
353
|
```
|
|
418
354
|
|
|
419
|
-
### Step 4: Business Operations
|
|
355
|
+
### Step 4: Business Operations
|
|
420
356
|
```python
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
print(f"❌ Code list fetch failed: {e}")
|
|
438
|
-
|
|
439
|
-
# Send sales transaction (FULL Postman payload structure)
|
|
440
|
-
try:
|
|
441
|
-
response = etims.send_sales_transaction({
|
|
442
|
-
"invcNo": 1, # INTEGER (sequential) - NOT string!
|
|
443
|
-
"custTin": "A123456789Z",
|
|
444
|
-
"custNm": "Test Customer",
|
|
445
|
-
"salesTyCd": "N", # N=Normal, R=Return
|
|
446
|
-
"rcptTyCd": "R", # R=Receipt
|
|
447
|
-
"pmtTyCd": "01", # 01=Cash
|
|
448
|
-
"salesSttsCd": "01", # 01=Completed
|
|
449
|
-
"cfmDt": kra_date(), # YYYYMMDDHHmmss
|
|
450
|
-
"salesDt": kra_date()[:8], # YYYYMMDD (NO time)
|
|
451
|
-
"totItemCnt": 1,
|
|
452
|
-
# TAX BREAKDOWN (ALL 15 FIELDS REQUIRED)
|
|
453
|
-
"taxblAmtA": 0.00, "taxblAmtB": 0.00, "taxblAmtC": 81000.00,
|
|
454
|
-
"taxblAmtD": 0.00, "taxblAmtE": 0.00,
|
|
455
|
-
"taxRtA": 0.00, "taxRtB": 0.00, "taxRtC": 0.00,
|
|
456
|
-
"taxRtD": 0.00, "taxRtE": 0.00,
|
|
457
|
-
"taxAmtA": 0.00, "taxAmtB": 0.00, "taxAmtC": 0.00,
|
|
458
|
-
"taxAmtD": 0.00, "taxAmtE": 0.00,
|
|
459
|
-
"totTaxblAmt": 81000.00,
|
|
460
|
-
"totTaxAmt": 0.00,
|
|
461
|
-
"totAmt": 81000.00,
|
|
462
|
-
"regrId": "Admin", "regrNm": "Admin",
|
|
463
|
-
"modrId": "Admin", "modrNm": "Admin",
|
|
464
|
-
"itemList": [{
|
|
465
|
-
"itemSeq": 1,
|
|
466
|
-
"itemCd": "KE2NTBA00000001", # Must exist in KRA system
|
|
467
|
-
"itemClsCd": "1000000000",
|
|
468
|
-
"itemNm": "Brand A",
|
|
469
|
-
"barCd": "", # Nullable but REQUIRED field
|
|
470
|
-
"pkgUnitCd": "NT",
|
|
471
|
-
"pkg": 1, # Package quantity
|
|
472
|
-
"qtyUnitCd": "BA",
|
|
473
|
-
"qty": 90.0,
|
|
474
|
-
"prc": 1000.00,
|
|
475
|
-
"splyAmt": 81000.00,
|
|
476
|
-
"dcRt": 10.0, # Discount rate %
|
|
477
|
-
"dcAmt": 9000.00, # Discount amount
|
|
478
|
-
"taxTyCd": "C", # C = Zero-rated/Exempt
|
|
479
|
-
"taxblAmt": 81000.00,
|
|
480
|
-
"taxAmt": 0.00,
|
|
481
|
-
"totAmt": 81000.00, # splyAmt - dcAmt + taxAmt
|
|
482
|
-
}],
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
print(f"✅ Sales transaction sent (resultCd: {response['resultCd']})")
|
|
486
|
-
print(f"Receipt Signature: {response['data']['rcptSign']}")
|
|
487
|
-
|
|
488
|
-
except ValidationException as e:
|
|
489
|
-
print("❌ Validation failed:")
|
|
490
|
-
for err in e.errors:
|
|
491
|
-
field = err.get('loc', [])[0] if err.get('loc') else 'unknown'
|
|
492
|
-
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
|
+
})
|
|
493
373
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
})
|
|
498
395
|
```
|
|
499
396
|
|
|
397
|
+
> ✅ All payloads are validated using internal Pydantic schemas before sending.
|
|
398
|
+
|
|
500
399
|
---
|
|
501
400
|
|
|
502
401
|
## API Reference
|
|
503
402
|
|
|
504
|
-
### Functional Categories
|
|
403
|
+
### Functional Categories & Methods
|
|
505
404
|
|
|
506
|
-
| Category |
|
|
507
|
-
|
|
508
|
-
| **Initialization** |
|
|
509
|
-
| **Data Management** |
|
|
510
|
-
| **Branch Management** |
|
|
511
|
-
| **Item Management** |
|
|
512
|
-
| **Purchase Management** |
|
|
513
|
-
| **Sales Management** |
|
|
514
|
-
| **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()` |
|
|
515
414
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
| Class | Purpose |
|
|
519
|
-
|-------|---------|
|
|
520
|
-
| `AuthClient` | Token generation, caching (60s buffer), and refresh management |
|
|
521
|
-
| `BaseClient` | HTTP request handling, header management, error unwrapping |
|
|
522
|
-
| `EtimsClient` | Business endpoint methods (all 8 functional categories) |
|
|
523
|
-
| `Validator` | Payload validation against KRA schemas (Pydantic v2) |
|
|
415
|
+
> 🔍 Each method maps to a KRA endpoint alias defined internally (e.g., `save_purchase` → `insertTrnsPurchase`).
|
|
524
416
|
|
|
525
417
|
---
|
|
526
418
|
|
|
@@ -554,38 +446,20 @@ except ApiException as e:
|
|
|
554
446
|
### Handling Pattern
|
|
555
447
|
```python
|
|
556
448
|
try:
|
|
557
|
-
response = etims.
|
|
449
|
+
response = etims.save_purchase(payload)
|
|
558
450
|
|
|
559
451
|
except ValidationException as e:
|
|
560
452
|
print("❌ Validation failed:")
|
|
561
|
-
for
|
|
562
|
-
|
|
563
|
-
print(f" • {field}: {error.get('msg', 'validation error')}")
|
|
453
|
+
for field, msg in e.details.items():
|
|
454
|
+
print(f" • {field}: {msg}")
|
|
564
455
|
|
|
565
456
|
except ApiException as e:
|
|
566
457
|
print(f"❌ KRA API Error ({e.error_code}): {e}")
|
|
567
|
-
|
|
568
|
-
# Get full KRA response for debugging
|
|
569
458
|
if e.details and 'resultMsg' in e.details:
|
|
570
459
|
print(f"KRA Message: {e.details['resultMsg']}")
|
|
571
|
-
|
|
572
|
-
# Handle specific error codes
|
|
573
|
-
if e.error_code == "901":
|
|
574
|
-
print("→ Device serial not registered with KRA")
|
|
575
|
-
elif e.error_code == "902":
|
|
576
|
-
print("→ cmcKey expired - reinitialize OSCU")
|
|
577
|
-
elif e.error_code == "500":
|
|
578
|
-
print("→ Invalid payload - check date formats/tax fields")
|
|
579
460
|
|
|
580
461
|
except AuthenticationException as e:
|
|
581
462
|
print(f"❌ Authentication failed: {e}")
|
|
582
|
-
|
|
583
|
-
# Attempt token refresh
|
|
584
|
-
try:
|
|
585
|
-
auth.token(force_refresh=True)
|
|
586
|
-
# Retry operation...
|
|
587
|
-
except Exception as ex:
|
|
588
|
-
print(f"Token refresh failed: {ex}")
|
|
589
463
|
```
|
|
590
464
|
|
|
591
465
|
### Comprehensive KRA Error Codes
|
|
@@ -622,18 +496,14 @@ except AuthenticationException as e:
|
|
|
622
496
|
**Cause**: cmcKey expired or not set in config
|
|
623
497
|
**Solution**:
|
|
624
498
|
```python
|
|
625
|
-
|
|
626
|
-
config.oscu["cmc_key"] = extracted_cmc_key
|
|
499
|
+
config['oscu']['cmc_key'] = extracted_cmc_key
|
|
627
500
|
etims = EtimsClient(config, auth) # MUST recreate client
|
|
628
501
|
```
|
|
629
502
|
|
|
630
503
|
### ❌ Trailing spaces in URLs
|
|
631
504
|
|
|
632
505
|
**Cause**: Copy-paste errors from documentation
|
|
633
|
-
**Solution**: Always
|
|
634
|
-
```python
|
|
635
|
-
"token_url": "https://sbx.kra.go.ke/v1/token/generate ".strip(),
|
|
636
|
-
```
|
|
506
|
+
**Solution**: Always verify URLs have no trailing whitespace.
|
|
637
507
|
|
|
638
508
|
### ❌ Invoice number rejected
|
|
639
509
|
|
|
@@ -641,7 +511,6 @@ etims = EtimsClient(config, auth) # MUST recreate client
|
|
|
641
511
|
**Solution**: Use sequential integers starting from 1:
|
|
642
512
|
```python
|
|
643
513
|
"invcNo": 1, # ✅ Correct
|
|
644
|
-
# NOT "INV001" ❌
|
|
645
514
|
```
|
|
646
515
|
|
|
647
516
|
---
|
|
@@ -719,7 +588,7 @@ KRA mandates successful completion of automated tests before verification:
|
|
|
719
588
|
3. Deploy directly to production environment
|
|
720
589
|
4. No SLA execution required
|
|
721
590
|
|
|
722
|
-
> 💡 **Production URL**: `https://api.
|
|
591
|
+
> 💡 **Production URL**: `https://etims-api.kra.go.ke/etims-api`
|
|
723
592
|
> ⚠️ **Never use sandbox credentials in production** – KRA monitors environment separation strictly
|
|
724
593
|
|
|
725
594
|
---
|
|
@@ -740,7 +609,7 @@ KRA mandates successful completion of automated tests before verification:
|
|
|
740
609
|
- **Email**: ebartile@gmail.com (for integration guidance)
|
|
741
610
|
- **Emergency Hotline**: +254757807150 (business hours only)
|
|
742
611
|
|
|
743
|
-
> ℹ️ **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.
|
|
744
613
|
|
|
745
614
|
---
|
|
746
615
|
|
|
@@ -748,7 +617,7 @@ KRA mandates successful completion of automated tests before verification:
|
|
|
748
617
|
|
|
749
618
|
MIT License
|
|
750
619
|
|
|
751
|
-
Copyright © 2024
|
|
620
|
+
Copyright © 2024–2026 Bartile Emmanuel / Paybill Kenya
|
|
752
621
|
|
|
753
622
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
754
623
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -775,4 +644,3 @@ SOFTWARE.
|
|
|
775
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.
|
|
776
645
|
|
|
777
646
|
> 🇰🇪 **Proudly Made in Kenya** – Supporting digital tax compliance for East Africa's largest economy.
|
|
778
|
-
> *Tested on KRA Sandbox • Built with Python • Pydantic v2 Validation • Production Ready*
|