Skip to main content

Unofficial Python wrapper for the ZIMRA FDMS API by Tarmica Chiwara

Project description

# 🇿🇼 zimra **Unofficial Python wrapper for the ZIMRA Fiscal Device Management System (FDMS) API** [![PyPI version](https://img.shields.io/pypi/v/zimra)](https://pypi.org/project/zimra/) [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) *Simplifying fiscal device integration for Zimbabwean businesses* --- ## What is this? `zimra` is a Python library that handles the complexity of communicating with ZIMRA's Fiscal Device Management System. If you're building a POS system, e-commerce platform, or any application that needs to issue fiscal receipts in Zimbabwe — this library does the heavy lifting for you. **What it handles for you:** - 🔐 Device registration & certificate management (mTLS) - 🧮 Automatic VAT calculation (tax-inclusive **and** tax-exclusive) - 📝 Receipt preparation, signing (SHA256 + RSA PKCS#1 v1.5), and submission - 📅 Fiscal day lifecycle (open → submit receipts → close) - 📱 QR code generation for receipt verification - ✅ Support for fiscal invoices, credit notes, and debit notes --- ## Table of Contents - [Installation](#installation) - [Quick Start](#quick-start) - [Step 1 — Register Your Device](#step-1--register-your-device) - [Step 2 — Initialize the Device](#step-2--initialize-the-device) - [Step 3 — Open a Fiscal Day](#step-3--open-a-fiscal-day) - [Step 4 — Prepare & Submit a Receipt](#step-4--prepare--submit-a-receipt) - [Step 5 — Close the Fiscal Day](#step-5--close-the-fiscal-day) - [Tax-Exclusive Receipts](#tax-exclusive-receipts) - [Credit Notes & Debit Notes](#credit-notes--debit-notes) - [Exempt & Mixed Tax Items](#exempt--mixed-tax-items) - [QR Code Generation](#qr-code-generation) - [Utility Functions](#utility-functions) - [API Reference](#api-reference) - [Supported Tax Rates (2026)](#supported-tax-rates-2026) - [Error Handling](#error-handling) - [Contributing](#contributing) - [License](#license) --- ## Installation ### From PyPI (recommended) ```bash pip install zimra ``` ### From source ```bash git clone https://github.com/lordskyzw/zimra.git cd zimra pip install . ``` ### Dependencies The library requires the following packages (installed automatically): | Package | Purpose | |---------|---------| | `requests` | HTTP communication with ZIMRA API | | `pycryptodome` | RSA signing of receipts (PKCS#1 v1.5) | | `cryptography` | Certificate generation (CSR, key pairs) | | `colorama` | Colored logging output | | `requests_toolbelt` | Enhanced HTTP utilities | --- ## Quick Start This walkthrough takes you from a fresh device to submitting your first fiscal receipt. We'll use **test mode** throughout — switch to `test_mode=False` and `prod=True` when you're ready for production. ### Step 1 — Register Your Device Before anything else, you need to register your fiscal device with ZIMRA. This generates an RSA key pair and exchanges a CSR for a signed certificate. ```python from zimra import register_new_device register_new_device( fiscal_device_serial_no="9029D38C011B", # Your device's serial number device_id="10626", # Device ID assigned by ZIMRA activation_key="00398834", # 8-digit activation key from ZIMRA folder_name="certs", # Directory to save certificate & key certificate_filename="my_certificate", # Output: certs/my_certificate.crt private_key_filename="my_private_key", # Output: certs/my_private_key.key prod=False # False = test environment ) ``` > **📁 After running this, you'll have two files:** > - `certs/my_certificate.crt` — your device certificate (issued by ZIMRA) > - `certs/my_private_key.key` — your RSA private key (keep this secure!) ### Step 2 — Initialize the Device Create a `Device` instance using your credentials and certificate files: ```python from zimra import Device device = Device( device_id="10626", serialNo="9029D38C011B", activationKey="00398834", cert_path="certs/my_certificate.crt", private_key_path="certs/my_private_key.key", test_mode=True, # True = sandbox, False = production deviceModelName="Server", # Must match what ZIMRA has on record deviceModelVersion="v1", company_name="MyCompany" ) ``` > ⚠️ **Important:** The `deviceModelName` must exactly match what's registered with ZIMRA. A mismatch will result in `403 Forbidden` errors. You can verify connectivity and check your device configuration: ```python # Check device status (fiscal day state, etc.) status = device.getStatus() print(status) # {'fiscalDayNo': 1, 'fiscalDayStatus': 'FiscalDayClosed', ...} # Get full device configuration (taxpayer info, applicable taxes, etc.) config = device.getConfig() print(config) # {'taxPayerName': 'MY COMPANY', 'taxPayerTIN': '...', 'applicableTaxes': [...], ...} # Simple connectivity test ping = device.ping() print(ping) ``` ### Step 3 — Open a Fiscal Day A fiscal day must be opened before you can submit any receipts. The `fiscalDayNo` is a sequential integer that must be greater than the last closed day. ```python result = device.openDay(fiscalDayNo=1) print(result) # {'fiscalDayNo': 1, 'operationID': '0HN4FDK6T1CNI:00000001'} ``` > The library automatically checks that the current fiscal day is closed before opening a new one. If day 1 is already open, it will return an error message. ### Step 4 — Prepare & Submit a Receipt This is the core workflow. You provide a simplified receipt object, and the library: 1. Validates all mandatory fields 2. Formats receipt lines to ZIMRA's specification 3. Calculates taxes automatically (VAT extraction from inclusive prices) 4. Signs the receipt with your private key 5. Returns a fully prepared payload ready for submission #### Build the receipt data: ```python from datetime import datetime receipt_data = { "receiptType": "FISCALINVOICE", # FISCALINVOICE | CREDITNOTE | DEBITNOTE "receiptCurrency": "USD", # USD | ZWG "receiptCounter": 1, # Sequential counter per receipt type "receiptGlobalNo": 1, # Global sequential number "invoiceNo": "INV-001", # Your unique invoice number "receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'), "receiptLines": [ { "item_name": "Widget A", "tax_percent": 15.5, # Standard VAT rate (2026) "quantity": 2, "unit_price": 25.00 }, { "item_name": "Widget B (Zero-rated)", "tax_percent": 0, # Zero-rated item "quantity": 1, "unit_price": 10.00 } ], "receiptPayments": [{ "moneyTypeCode": 0, # 0 = Cash, 1 = Card "paymentAmount": 60.00 # Total payment amount }] } ``` #### Prepare and submit: ```python # prepareReceipt handles tax calculation, formatting, and signing prepared = device.prepareReceipt(receipt_data) # Submit to ZIMRA response = device.submitReceipt(prepared) print(response) ``` > 💡 **Tip:** The `prepareReceipt` method assumes prices are **tax-inclusive** (prices already include VAT). For tax-exclusive pricing, see [Tax-Exclusive Receipts](#tax-exclusive-receipts). ### Step 5 — Close the Fiscal Day At the end of the business day, close the fiscal day with a summary of the day's counters: ```python result = device.closeDay( fiscalDayNo=1, fiscalDayDate="2026-02-22", lastReceiptCounterValue=5, # Total receipts submitted today fiscalDayCounters=[ # Summary of the day's transactions { "fiscalCounterType": "SaleByTax", "fiscalCounterCurrency": "USD", "fiscalCounterTaxPercent": 15.5, "fiscalCounterTaxID": 515, "fiscalCounterValue": 500.00 }, { "fiscalCounterType": "BalanceByMoneyType", "fiscalCounterCurrency": "USD", "fiscalCounterMoneyType": 0, "fiscalCounterValue": 500.00 } ] ) print(result) ``` > To close an empty day (no receipts), pass `lastReceiptCounterValue=None` and `fiscalDayCounters=[]`. --- ## Tax-Exclusive Receipts If your prices **do not** include VAT, use `prepareReceiptTaxExclusive` instead. The library will calculate VAT on top of the base prices and adjust the receipt total accordingly. ```python receipt_data = { "receiptType": "FISCALINVOICE", "receiptCurrency": "USD", "receiptCounter": 1, "receiptGlobalNo": 2, "invoiceNo": "INV-002", "receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'), "receiptLines": [ { "item_name": "Service A", "tax_percent": 15.5, # VAT will be calculated ON TOP of this price "quantity": 1, "unit_price": 100.00 # Pre-tax price } ], "receiptPayments": [{ "moneyTypeCode": 0, "paymentAmount": 115.50 # base + VAT }] } prepared = device.prepareReceiptTaxExclusive(receipt_data) response = device.submitReceipt(prepared) ``` **Key difference:** | Method | `unit_price` means | VAT calculation | |--------|-------------------|-----------------| | `prepareReceipt` | Price **including** VAT | VAT extracted: `total × rate / (1 + rate)` | | `prepareReceiptTaxExclusive` | Price **excluding** VAT | VAT added: `total × rate` | --- ## Credit Notes & Debit Notes To issue a credit note (refund) or debit note, set `receiptType` accordingly and include the required `creditDebitNote` and `receiptNotes` fields: ```python credit_note = { "receiptType": "CREDITNOTE", "receiptCurrency": "USD", "receiptCounter": 1, "receiptGlobalNo": 3, "invoiceNo": "CN-001", "receiptDate": datetime.now().strftime('%Y-%m-%dT%H:%M:%S'), "receiptNotes": "Refund for damaged goods", "creditDebitNote": { "receiptID": 12345, # Original receipt ID from ZIMRA "deviceID": 10626, "receiptGlobalNo": 1, # Original receipt's global number "fiscalDayNo": 1 }, "receiptLines": [ { "item_name": "Widget A (Returned)", "tax_percent": 15.5, "quantity": 1, "unit_price": 25.00 } ], "receiptPayments": [{ "moneyTypeCode": 0, "paymentAmount": 25.00 }] } prepared = device.prepareReceipt(credit_note) response = device.submitReceipt(prepared) ``` --- ## Exempt & Mixed Tax Items You can mix different tax rates in a single receipt, including exempt items: ```python receipt_data = { # ... standard fields ... "receiptLines": [ { "item_name": "Standard item", "tax_percent": 15.5, # Standard 15.5% VAT "quantity": 1, "unit_price": 50.00 }, { "item_name": "Reduced rate item", "tax_percent": 5, # 5% withholding tax "quantity": 1, "unit_price": 30.00 }, { "item_name": "Zero-rated item", "tax_percent": 0, # Zero-rated "quantity": 1, "unit_price": 20.00 }, { "item_name": "Exempt item", "tax_percent": "exempt", # Use string "exempt" or "E" "quantity": 1, "unit_price": 15.00 } ], # ... } ``` > The library automatically groups items by tax rate, calculates consolidated tax amounts per group, and assigns the correct ZIMRA `taxID` for each. --- ## QR Code Generation After submitting a receipt, generate a QR code URL for receipt verification: ```python # After a successful submitReceipt call: qr_url = device.generate_qr_code( signature=prepared["receiptDeviceSignature"]["signature"], receipt_global_no=1, receipt_date=datetime.now().date() ) print(qr_url) # https://fdmstest.zimra.co.zw/0000010626220220260000000001 ``` This URL can be encoded into a QR code on printed receipts for customers to verify the receipt with ZIMRA. --- ## Utility Functions ### Standalone Tax Calculator Calculate VAT without initializing a `Device`: ```python from zimra import tax_calculator # Extract VAT from a tax-inclusive price # Formula: VAT = total - (total / (1 + rate/100)) vat = tax_calculator(sale_amount=115.50, tax_rate=15.5) print(vat) # 15.5 ``` ### Receipt Preprocessing If you need to sanitize receipt data before preparing it: ```python from zimra import preprocess_receipt, preprocess_tax_exclusivereceipt # Ensures all numeric fields are properly formatted strings # Calculates line_total for each receipt line cleaned = preprocess_receipt(raw_receipt_data) prepared = device.prepareReceipt(cleaned) ``` --- ## API Reference ### `register_new_device()` Registers a new device with ZIMRA and saves the certificate + private key locally. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `fiscal_device_serial_no` | `str` | *required* | Device serial number | | `device_id` | `str` | *required* | ZIMRA-assigned device ID | | `activation_key` | `str` | *required* | 8-digit activation key | | `model_name` | `str` | `'Server'` | Device model name | | `folder_name` | `str` | `'prod'` | Output directory for cert/key files | | `certificate_filename` | `str` | `'certificate'` | Certificate filename (without extension) | | `private_key_filename` | `str` | `'decrypted_key'` | Private key filename (without extension) | | `prod` | `bool` | `False` | `True` = production, `False` = test | ### `Device` Class #### Constructor ```python Device( device_id, serialNo, activationKey, cert_path, private_key_path, test_mode=True, deviceModelName='Server', deviceModelVersion='v1', company_name='NexusClient' ) ``` #### Methods | Method | Description | Returns | |--------|-------------|---------| | `getConfig()` | Fetch device configuration (taxpayer info, applicable taxes) | `dict` | | `getStatus()` | Get current fiscal day status | `dict` | | `ping()` | Connectivity test to ZIMRA server | `dict` | | `openDay(fiscalDayNo)` | Open a new fiscal day | `dict` | | `prepareReceipt(receiptData, ...)` | Prepare a tax-**inclusive** receipt for submission | `OrderedDict` | | `prepareReceiptTaxExclusive(receiptData, ...)` | Prepare a tax-**exclusive** receipt for submission | `OrderedDict` | | `submitReceipt(receiptData)` | Submit a prepared receipt to ZIMRA | `dict` | | `closeDay(fiscalDayNo, fiscalDayDate, lastReceiptCounterValue, fiscalDayCounters)` | Close the current fiscal day | `dict` | | `generate_qr_code(signature, receipt_global_no, receipt_date)` | Generate a QR code verification URL | `str` | | `renewCertificate()` | Renew the device certificate | `str` | --- ## Supported Tax Rates (2026) | Tax Rate | Tax ID | Description | |----------|--------|-------------| | `0` | `2` | Zero-rated (0%) | | `"exempt"` or `"E"` | `3` | Exempt | | `5` | `514` | Non-VAT Withholding Tax (5%) | | `15.5` | `515` | Standard VAT (15.5%) — *current rate* | | `15` | Legacy | Supported for backward compatibility | --- ## Error Handling The library raises clear errors for common issues: ```python from zimra import ZimraServerError try: prepared = device.prepareReceipt(receipt_data) response = device.submitReceipt(prepared) except ValueError as e: # Missing mandatory fields, invalid date format, invalid tax_percent, etc. print(f"Validation error: {e}") except ZimraServerError as e: # ZIMRA server returned an error print(f"Server error {e.status_code}: {e}") except Exception as e: print(f"Unexpected error: {e}") ``` **Common gotchas:** - `deviceModelName` must match exactly what ZIMRA has registered — otherwise you get `403` - Fiscal day numbers must be sequential and the previous day must be closed - `receiptGlobalNo` must be globally unique and sequential - Certificate files must be accessible and valid --- ## Contributing Contributions are welcome! This project is actively maintained and there are many ways to help: - 🐛 **Bug reports** — Open an issue with steps to reproduce - 💡 **Feature requests** — Suggest improvements or new functionality - 🔧 **Pull requests** — Submit code changes ```bash git clone https://github.com/lordskyzw/zimra.git cd zimra pip install -e . # Install in editable mode ``` --- ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. ---
**Built with ❤️ by [Tarmica Chiwara](https://github.com/lordskyzw)** *Alleviating complexity from Zimbabwean businesses*
]]>

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zimra-0.0.5.tar.gz (25.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zimra-0.0.5-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

File details

Details for the file zimra-0.0.5.tar.gz.

File metadata

  • Download URL: zimra-0.0.5.tar.gz
  • Upload date:
  • Size: 25.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.1

File hashes

Hashes for zimra-0.0.5.tar.gz
Algorithm Hash digest
SHA256 cfd559ac87b777802b04f1c1ca6695c67ee19c001dd6ca77ad593eddc89ab2e0
MD5 ce9e1813a9067c21139ca287a37e6285
BLAKE2b-256 456c796d435026db7979aaba4d65d213222c75201e25db68e1365932d9672c76

See more details on using hashes here.

File details

Details for the file zimra-0.0.5-py3-none-any.whl.

File metadata

  • Download URL: zimra-0.0.5-py3-none-any.whl
  • Upload date:
  • Size: 19.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.1

File hashes

Hashes for zimra-0.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 31ac45c3c511c2a1dfdfa4b6da533932cf4ddb5a48dd64a00938b21fe116b347
MD5 d823bac2510ed06176ac0b79a18dda79
BLAKE2b-256 a50137cf12a288e09c34d38d5d7e93e584c863a8ce253c391bc41e7346a60a9e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page