Initial commit: L10N_Me Africa packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:53 +02:00
commit c265268138
611 changed files with 75334 additions and 0 deletions

View file

@ -0,0 +1,48 @@
# Kenya Tremol Device EDI Integration
This module integrates with the Kenyan G03 Tremol control unit device to the KRA through TIMS.
## Installation
```bash
pip install odoo-bringout-oca-ocb-l10n_ke_edi_tremol
```
## Dependencies
This addon depends on:
- l10n_ke
## Manifest Information
- **Name**: Kenya Tremol Device EDI Integration
- **Version**: 1.0
- **Category**: Accounting/Localizations/EDI
- **License**: LGPL-3
- **Installable**: False
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `l10n_ke_edi_tremol`.
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph L10n_ke_edi_tremol Module - l10n_ke_edi_tremol
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for l10n_ke_edi_tremol. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [l10n_ke](../../odoo-bringout-oca-ocb-l10n_ke)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon l10n_ke_edi_tremol or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-ocb-l10n_ke_edi_tremol"
# or
uv pip install odoo-bringout-oca-ocb-l10n_ke_edi_tremol"
```

View file

@ -0,0 +1,17 @@
# Models
Detected core models and extensions in l10n_ke_edi_tremol.
```mermaid
classDiagram
class account_move
class product_product
class product_template
class res_company
class res_config_settings
class res_partner
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: l10n_ke_edi_tremol. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon l10n_ke_edi_tremol
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,8 @@
# Security
This module does not define custom security rules or access controls beyond Odoo defaults.
Default Odoo security applies:
- Base user access through standard groups
- Model access inherited from dependencies
- No custom row-level security rules

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon l10n_ke_edi_tremol
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Kenya Tremol Device EDI Integration",
'summary': """
Kenya Tremol Device EDI Integration
""",
'description': """
This module integrates with the Kenyan G03 Tremol control unit device to the KRA through TIMS.
""",
'author': 'Odoo',
'category': 'Accounting/Localizations/EDI',
'version': '1.0',
'license': 'LGPL-3',
'depends': ['l10n_ke'],
'data': [
'views/account_move_view.xml',
'views/product_view.xml',
'views/report_invoice.xml',
'views/res_config_settings_view.xml',
'views/res_partner_views.xml',
],
'assets': {
'web.assets_backend': [
'l10n_ke_edi_tremol/static/src/js/send_invoice.js',
],
},
}

View file

@ -0,0 +1,3 @@
-- neutralize connection to tremol controle unit
UPDATE res_company
SET l10n_ke_cu_proxy_address = '';

View file

@ -0,0 +1,307 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_ke_edi_tremol
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-08 23:34+0000\n"
"PO-Revision-Date: 2023-01-08 23:34+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_invoice
msgid "<b>Date and Time of Signing: </b><br/>"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_invoice
msgid "<b>Invoice Number: </b><br/>"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_invoice
msgid "<b>Kenyan Fiscal Device Info</b>"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_invoice
msgid "<b>Serial Number: </b><br/>"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.res_config_settings_view_form
msgid ""
"<span class=\"o_form_label\">Tremol Device Settings</span>\n"
" <span class=\"fa fa-lg fa-building-o\" title=\"Values set here are company-specific.\" aria-label=\"Values set here are company-specific.\" groups=\"base.group_multi_company\" role=\"img\"/>"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_invoice
msgid "<strong class=\"text-center\">TIMS URL</strong><br/><br/>"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_bank_statement_line__l10n_ke_cu_invoice_number
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_move__l10n_ke_cu_invoice_number
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_payment__l10n_ke_cu_invoice_number
msgid "CU Invoice Number"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_bank_statement_line__l10n_ke_cu_qrcode
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_move__l10n_ke_cu_qrcode
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_payment__l10n_ke_cu_qrcode
msgid "CU QR Code"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_bank_statement_line__l10n_ke_cu_serial_number
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_move__l10n_ke_cu_serial_number
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_payment__l10n_ke_cu_serial_number
msgid "CU Serial Number"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_bank_statement_line__l10n_ke_cu_datetime
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_move__l10n_ke_cu_datetime
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_account_payment__l10n_ke_cu_datetime
msgid "CU Signing Date and Time"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model,name:l10n_ke_edi_tremol.model_res_company
msgid "Companies"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model,name:l10n_ke_edi_tremol.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model,name:l10n_ke_edi_tremol.model_res_partner
msgid "Contact"
msgstr ""
#. module: l10n_ke_edi_tremol
#. openerp-web
#: code:addons/l10n_ke_edi_tremol/static/src/js/send_invoice.js:0
#, python-format
msgid "Error trying to connect to Odoo. Check your internet connection"
msgstr ""
#. module: l10n_ke_edi_tremol
#. openerp-web
#: code:addons/l10n_ke_edi_tremol/static/src/js/send_invoice.js:0
#, python-format
msgid "Error trying to connect to the middleware. Is the middleware running?"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_res_partner__l10n_ke_exemption_number
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_res_users__l10n_ke_exemption_number
msgid "Exemption Number"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_res_company__l10n_ke_cu_proxy_address
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_res_config_settings__l10n_ke_cu_proxy_address
msgid "Fiscal Device Proxy Address"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_inherit_product_template_form_view
msgid "HS Code"
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"Invalid invoice configuration on %s:\n"
"%s\n"
"\n"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model,name:l10n_ke_edi_tremol.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_product_product__l10n_ke_hsn_code
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_product_template__l10n_ke_hsn_code
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_inherit_product_product_form_view
msgid "KRA Item Code"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_product_product__l10n_ke_hsn_name
#: model:ir.model.fields,field_description:l10n_ke_edi_tremol.field_product_template__l10n_ke_hsn_name
msgid "KRA Item Description"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.res_partner_view_form
msgid "Kenya Accounting Details"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_inherit_account_move_search_view
msgid "Kenya CU Invoice Number"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.res_config_settings_view_form
msgid "Kenya TIMS Integration"
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"On line %s, a product with a HS Code and HS Name must be selected, since the"
" tax is 0%% or exempt."
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid "On line %s, you must select one and only one tax."
msgstr ""
#. module: l10n_ke_edi_tremol
#. openerp-web
#: code:addons/l10n_ke_edi_tremol/static/src/js/send_invoice.js:0
#, python-format
msgid "Posting an invoice has failed, with the message: \n"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model,name:l10n_ke_edi_tremol.model_product_template
msgid "Product Template"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,help:l10n_ke_edi_tremol.field_product_product__l10n_ke_hsn_name
#: model:ir.model.fields,help:l10n_ke_edi_tremol.field_product_template__l10n_ke_hsn_name
msgid "Product code description needed when not 16% VAT rated. "
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,help:l10n_ke_edi_tremol.field_product_product__l10n_ke_hsn_code
#: model:ir.model.fields,help:l10n_ke_edi_tremol.field_product_template__l10n_ke_hsn_code
msgid "Product code needed when not 16% VAT rated. "
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_invoice
msgid "QR Code"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_inherit_account_move_form
msgid "Send To Fiscal Device"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.actions.server,name:l10n_ke_edi_tremol.action_send_invoices_to_device
msgid "Send to fiscal device"
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"Tax '%s' is used, but only taxes of 16%%, 8%%, 0%% or Exempt can be sent. "
"Please reconfigure or change the tax."
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"Tax exempt report line cannot be found, please update the l10n_ke module."
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,help:l10n_ke_edi_tremol.field_res_company__l10n_ke_cu_proxy_address
#: model:ir.model.fields,help:l10n_ke_edi_tremol.field_res_config_settings__l10n_ke_cu_proxy_address
msgid "The address of the proxy server for the fiscal device."
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"The document already has details related to the fiscal device. Please make "
"sure that the invoice has not already been sent."
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid "The document being sent should be an invoice or credit note."
msgstr ""
#. module: l10n_ke_edi_tremol
#: model:ir.model.fields,help:l10n_ke_edi_tremol.field_res_partner__l10n_ke_exemption_number
#: model:ir.model.fields,help:l10n_ke_edi_tremol.field_res_users__l10n_ke_exemption_number
msgid ""
"The exemption number of the partner. Provided by the Kenyan government."
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.res_config_settings_view_form
msgid ""
"The tremol device makes use of a proxy server, which can be running locally on your computer or on an IoT Box.\n"
" The proxy server must be on the same network as the fiscal device."
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"This credit note must reference the previous invoice, and this previous "
"invoice must have already been submitted."
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"This invoice is not a Kenyan invoice and therefore can not be sent to the "
"device."
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"This invoice's company currency is not in Kenyan Shillings, conversion to "
"KES is not possible."
msgstr ""
#. module: l10n_ke_edi_tremol
#: code:addons/l10n_ke_edi_tremol/models/account_move.py:0
#, python-format
msgid ""
"This invoice/credit note has not been posted. Please confirm it to continue."
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_inherit_account_move_search_view
msgid "To Send to TIMS"
msgstr ""
#. module: l10n_ke_edi_tremol
#: model_terms:ir.ui.view,arch_db:l10n_ke_edi_tremol.l10n_ke_inherit_account_move_form
msgid "Tremol GO3 Fiscal Device"
msgstr ""

View file

@ -0,0 +1,5 @@
from . import account_move
from . import product
from . import res_company
from . import res_config_settings
from . import res_partner

View file

@ -0,0 +1,275 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import json
import re
from datetime import datetime
from odoo import models, fields, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_ke_cu_datetime = fields.Datetime(string='CU Signing Date and Time', copy=False)
l10n_ke_cu_serial_number = fields.Char(string='CU Serial Number', copy=False)
l10n_ke_cu_invoice_number = fields.Char(string='CU Invoice Number', copy=False)
l10n_ke_cu_qrcode = fields.Char(string='CU QR Code', copy=False)
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
def _l10n_ke_fmt(self, string, length, ljust=True):
""" Function for common formatting behaviour
:param string: string to be formatted/encoded
:param length: integer length to justify (if enabled), and then truncate the string to
:param ljust: boolean representing whether the string should be justified
:returns: byte-string justified/truncated, with all non-alphanumeric characters removed
"""
if not string:
string = ''
return re.sub('[^A-Za-z0-9 ]+', '', str(string)).encode('cp1251').ljust(length if ljust else 0)[:length]
# -------------------------------------------------------------------------
# CHECKS
# -------------------------------------------------------------------------
def _l10n_ke_validate_move(self):
""" Returns list of errors related to misconfigurations per move
Find misconfigurations on the move, the lines of the move, and the
taxes on those lines that would result in rejection by the KRA.
"""
errors = []
for move in self:
move_errors = []
if move.country_code != 'KE':
move_errors.append(_("This invoice is not a Kenyan invoice and therefore can not be sent to the device."))
if move.company_id.currency_id != self.env.ref('base.KES'):
move_errors.append(_("This invoice's company currency is not in Kenyan Shillings, conversion to KES is not possible."))
if move.state != 'posted':
move_errors.append(_("This invoice/credit note has not been posted. Please confirm it to continue."))
if move.move_type not in ('out_refund', 'out_invoice'):
move_errors.append(_("The document being sent should be an invoice or credit note."))
if any([move.l10n_ke_cu_invoice_number, move.l10n_ke_cu_serial_number, move.l10n_ke_cu_qrcode, move.l10n_ke_cu_datetime]):
move_errors.append(_("The document already has details related to the fiscal device. Please make sure that the invoice has not already been sent."))
# The credit note should refer to the control unit number (receipt number) of the original
# invoice to which it relates.
if move.move_type == 'out_refund' and not move.reversed_entry_id.l10n_ke_cu_invoice_number:
move_errors.append(_("This credit note must reference the previous invoice, and this previous invoice must have already been submitted."))
for line in self.invoice_line_ids.filtered(lambda l: l.display_type == 'product'):
vat_taxes = line.tax_ids.filtered(lambda tax: tax.amount in (16, 8, 0))
if not vat_taxes or len(vat_taxes) > 1:
move_errors.append(_("On line %s, you must select one and only one VAT tax.", line.name))
else:
if vat_taxes[0].amount == 0 and not (line.product_id and line.product_id.l10n_ke_hsn_code and line.product_id.l10n_ke_hsn_name):
move_errors.append(_("On line %s, a product with a HS Code and HS Name must be selected, since the tax is 0%% or exempt.", line.name))
if move_errors:
errors.append((move.name, move_errors))
return errors
# -------------------------------------------------------------------------
# SERIALISERS
# -------------------------------------------------------------------------
def _l10n_ke_cu_open_invoice_message(self):
""" Serialise the required fields for opening an invoice
:returns: a list containing one byte-string representing the <CMD> and
<DATA> of the message sent to the fiscal device.
"""
headquarter_address = (self.commercial_partner_id.street or '') + (self.commercial_partner_id.street2 or '')
customer_address = (self.partner_id.street or '') + (self.partner_id.street2 or '')
postcode_and_city = (self.partner_id.zip or '') + '' + (self.partner_id.city or '')
vat = (self.commercial_partner_id.vat or '').strip() if self.commercial_partner_id.country_id.code == 'KE' else ''
invoice_elements = [
b'1', # Reserved - 1 symbol with value '1'
b' 0', # Reserved - 6 symbols with value 0
b'0', # Reserved - 1 symbol with value '0'
b'1' if self.move_type == 'out_invoice' else b'A', # 1 symbol with value '1' (new invoice), 'A' (credit note), or '@' (debit note)
self._l10n_ke_fmt(self.commercial_partner_id.name, 30), # 30 symbols for Company name
self._l10n_ke_fmt(vat, 14), # 14 Symbols for the client PIN number
self._l10n_ke_fmt(headquarter_address, 30), # 30 Symbols for customer headquarters
self._l10n_ke_fmt(customer_address, 30), # 30 Symbols for the address
self._l10n_ke_fmt(postcode_and_city, 30), # 30 symbols for the customer post code and city
self._l10n_ke_fmt('', 30), # 30 symbols for the exemption number
]
if self.move_type == 'out_refund':
invoice_elements.append(self._l10n_ke_fmt(self.reversed_entry_id.l10n_ke_cu_invoice_number, 19)), # 19 symbols for related invoice number
invoice_elements.append(re.sub('[^A-Za-z0-9 ]+', '', self.name)[-15:].ljust(15).encode('cp1251')) # 15 symbols for trader system invoice number
# Command: Open fiscal record (0x30)
return [b'\x30' + b';'.join(invoice_elements)]
def _l10n_ke_cu_lines_messages(self):
""" Serialise the data of each line on the invoice
This function transforms the lines in order to handle the differences
between the KRA expected data and the lines in odoo.
If a discount line (as a negative line) has been added to the invoice
lines, find a suitable line/lines to distribute the discount accross
:returns: List of byte-strings representing each command <CMD> and the
<DATA> of the line, which will be sent to the fiscal device
in order to add a line to the opened invoice.
"""
def is_discount_line(line):
return line.price_subtotal < 0.0
def is_candidate(discount_line, other_line):
""" If the of one line match those of the discount line, the discount can be distributed accross that line """
discount_taxes = discount_line.tax_ids.flatten_taxes_hierarchy()
other_line_taxes = other_line.tax_ids.flatten_taxes_hierarchy()
return set(discount_taxes.ids) == set(other_line_taxes.ids)
lines = self.invoice_line_ids.filtered(lambda l: l.display_type == 'product' and l.quantity and l.price_total)
# The device expects all monetary values in Kenyan Shillings
if self.currency_id == self.company_id.currency_id:
currency_rate = 1
# In the case of a refund, use the currency rate of the original invoice
elif self.move_type == 'out_refund' and self.reversed_entry_id:
currency_rate = abs(self.reversed_entry_id.amount_total_signed / self.reversed_entry_id.amount_total)
else:
currency_rate = abs(self.amount_total_signed / self.amount_total)
discount_dict = {line.id: line.discount for line in lines if line.price_total > 0}
for line in lines:
if not is_discount_line(line):
continue
# Search for non-discount lines
candidate_vals_list = [l for l in lines if not is_discount_line(l) and is_candidate(l, line)]
candidate_vals_list = sorted(candidate_vals_list, key=lambda x: x.price_unit * x.quantity, reverse=True)
line_to_discount = abs(line.price_unit * line.quantity)
for candidate in candidate_vals_list:
still_to_discount = abs(candidate.price_unit * candidate.quantity * (100.0 - discount_dict[candidate.id]) / 100.0)
if line_to_discount >= still_to_discount:
discount_dict[candidate.id] = 100.0
line_to_discount -= still_to_discount
else:
rest_to_discount = abs((line_to_discount / (candidate.price_unit * candidate.quantity)) * 100.0)
discount_dict[candidate.id] += rest_to_discount
break
vat_class = {16.0: 'A', 8.0: 'B'}
msgs = []
tax_details = self._prepare_invoice_aggregated_taxes()
for line in self.invoice_line_ids.filtered(lambda l: l.display_type == 'product' and l.quantity and l.price_total > 0 and not discount_dict.get(l.id) >= 100):
# Here we use the original discount of the line, since it the distributed discount has not been applied in the price_total
price_total = 0
percentage = 0
for tax in tax_details['tax_details_per_record'][line]['tax_details']:
if tax['tax'].amount in (16, 8, 0): # This should only occur once
line_tax_details = tax_details['tax_details_per_record'][line]['tax_details'][tax]
price_total = abs(line_tax_details['base_amount_currency']) + abs(line_tax_details['tax_amount_currency'])
percentage = tax['tax'].amount
price = round(price_total / abs(line.quantity) * 100 / (100 - line.discount), line.currency_id.decimal_places) * currency_rate
price = ('%.5f' % price).rstrip('0').rstrip('.')
# Letter to classify tax, 0% taxes are handled conditionally, as the tax can be zero-rated or exempt
letter = ''
if percentage in vat_class:
letter = vat_class[percentage]
else:
report_line_ids = line.tax_ids.invoice_repartition_line_ids.tag_ids._get_related_tax_report_expressions().report_line_id.ids
try:
exempt_report_line = self.env.ref('l10n_ke.tax_report_line_exempt_sales')
except ValueError:
raise UserError(_("Tax exempt report line cannot be found, please update the l10n_ke module."))
letter = 'E' if exempt_report_line.id in report_line_ids else 'C'
uom = line.product_uom_id and line.product_uom_id.name or ''
hscode = re.sub('[^0-9.]+', '', line.product_id.l10n_ke_hsn_code)[:10].ljust(10).encode('cp1251') if letter not in ('A', 'B') else b''.ljust(10)
hsname = self._l10n_ke_fmt(line.product_id.l10n_ke_hsn_name, 20) if letter not in ('A', 'B') else b''.ljust(20)
line_data = b';'.join([
self._l10n_ke_fmt(line.name, 36), # 36 symbols for the article's name
self._l10n_ke_fmt(letter, 1), # 1 symbol for article's vat class ('A', 'B', 'C', 'D', or 'E')
price[:15].encode('cp1251'), # 1 to 15 symbols for article's price with up to 5 digits after decimal point
self._l10n_ke_fmt(uom, 3), # 3 symbols for unit of measure
hscode, # 10 symbols for HS code in the format xxxx.xx.xx (can be empty)
hsname, # 20 symbols for the HS name (can be empty)
str(percentage).encode('cp1251')[:5] # up to 5 symbols for vat rate
])
# 1 to 10 symbols for quantity
line_data += b'*' + str(abs(line.quantity)).encode('cp1251')[:10]
if discount_dict.get(line.id):
# 1 to 7 symbols for percentage of discount/addition
discount_sign = b'-' if discount_dict[line.id] > 0 else b'+'
discount = discount_sign + str(abs(discount_dict[line.id])).encode('cp1251')[:6]
line_data += b',' + discount + b'%'
# Command: Sale of article (0x31)
msgs += [b'\x31' + line_data]
return msgs
def _l10n_ke_get_cu_messages(self):
""" Composes a list of all the command and data parts of the messages
required for the fiscal device to open an invoice, add lines and
subsequently close it.
"""
self.ensure_one()
msgs = self._l10n_ke_cu_open_invoice_message()
msgs += self._l10n_ke_cu_lines_messages()
# Command: Close fiscal reciept (0x38)
msgs += [b'\x38']
# Command: Read date and time (0x68)
msgs += [b'\x68']
return msgs
# -------------------------------------------------------------------------
# POST COMMANDS / RECEIVE DATA
# -------------------------------------------------------------------------
def l10n_ke_action_cu_post(self):
""" Returns the client action descriptor dictionary for sending the
invoice(s) to the fiscal device.
"""
# Check the configuration of the invoice
errors = self._l10n_ke_validate_move()
if errors:
error_msg = ""
for move, error_list in errors:
error_list = '\n'.join(error_list)
error_msg += _("Invalid invoice configuration on %s:\n%s\n\n", move, error_list)
raise UserError(error_msg)
return {
'type': 'ir.actions.client',
'tag': 'post_send',
'params': {
'invoices': {
move.id: {
'messages': json.dumps([msg.decode('cp1251') for msg in move._l10n_ke_get_cu_messages()]),
'proxy_address': move.company_id.l10n_ke_cu_proxy_address,
'company_vat': move.company_id.vat
} for move in self
}
}
}
def l10n_ke_cu_response(self, response):
""" Set the fields related to the fiscal device on the invoice.
This is intended to be utilized by an RPC call from the javascript
client action.
"""
move = self.browse(int(response['move_id']))
replies = [msg for msg in response['replies']]
move.update({
'l10n_ke_cu_serial_number': response['serial_number'],
'l10n_ke_cu_invoice_number': replies[-2].split(';')[0],
'l10n_ke_cu_qrcode': replies[-2].split(';')[1].strip(),
'l10n_ke_cu_datetime': datetime.strptime(replies[-1], '%d-%m-%Y %H:%M'),
})

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
l10n_ke_hsn_code = fields.Char(
string='KRA Item Code',
help='Product code needed when not 16% VAT rated. ',
)
l10n_ke_hsn_name = fields.Char(
string='KRA Item Description',
help='Product code description needed when not 16% VAT rated. ',
)
class ProductProduct(models.Model):
_inherit = "product.product"
l10n_ke_hsn_code = fields.Char(
string='HSN code',
related='product_tmpl_id.l10n_ke_hsn_code',
help="Product code needed in case of not 16%. ",
readonly=False,
)
l10n_ke_hsn_name = fields.Char(
string='HSN description',
related='product_tmpl_id.l10n_ke_hsn_name',
help="Product code description needed in case of not 16%. ",
readonly=False,
)

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
l10n_ke_cu_proxy_address = fields.Char(
default="http://localhost:8069",
string='Fiscal Device Proxy Address',
help='The address of the proxy server for the fiscal device.',
)

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
l10n_ke_cu_proxy_address = fields.Char(related='company_id.l10n_ke_cu_proxy_address', readonly=False)

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
l10n_ke_exemption_number = fields.Char(
string='Exemption Number',
help='The exemption number of the partner. Provided by the Kenyan government.',
)
def _commercial_fields(self):
return super()._commercial_fields() + ['l10n_ke_exemption_number']

View file

@ -0,0 +1,49 @@
odoo.define('l10n_ke_edi_tremol.action_post_send_invoice', function (require) {
const core = require('web.core');
const ajax = require('web.ajax');
const Dialog = require('web.Dialog');
var rpc = require('web.rpc');
var _t = core._t;
async function post_send(parent, {params}) {
let refresh = false;
for (let move_id in params.invoices) {
try {
const res = await ajax.post(
params.invoices[move_id].proxy_address + '/hw_proxy/l10n_ke_cu_send', {
messages: params.invoices[move_id].messages,
company_vat: params.invoices[move_id].company_vat
}
);
const res_obj = JSON.parse(res);
if (res_obj.status === "ok") {
try {
await rpc.query({
model: 'account.move',
method: 'l10n_ke_cu_response',
args: [[], {'replies': res_obj.replies, 'serial_number': res_obj.serial_number, 'move_id': move_id}],
});
refresh = true;
} catch (_e) {
Dialog.alert(this, _t("Error trying to connect to Odoo. Check your internet connection"));
break;
}
} else {
Dialog.alert(this, _t("Posting an invoice has failed, with the message: \n") + res_obj.status);
break;
}
} catch(_e) {
Dialog.alert(this, _t("Error trying to connect to the middleware. Is the middleware running?"));
break;
}
}
if (refresh) {
parent.services.action.doAction({
'type': 'ir.actions.client',
'tag': 'reload',
});
}
}
core.action_registry.add('post_send', post_send);
return post_send;
});

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from .import test_move_export

View file

@ -0,0 +1,225 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from freezegun import freeze_time
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestKeMoveExport(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref='l10n_ke.l10nke_chart_template'):
super().setUpClass(chart_template_ref=chart_template_ref)
cls.partner_a.write({
'name': 'Sirius Cybernetics Corporation',
'street': 'Test Street',
'street2': 'Further Test Street',
'city': 'Nairobi',
'zip': '00500',
'country_id': cls.env.ref('base.ke').id,
'vat': 'A000123456F',
})
cls.product_a.write({
'name': 'Infinite Improbability Drive',
'l10n_ke_hsn_code': '0039.11.53',
'l10n_ke_hsn_name': 'Spacecraft including satellites and suborbital and spacecraft launch vehicles'
})
cls.standard_rate_tax = cls.env['account.tax'].create({
'name': '16% tax',
'amount': 16.0,
'amount_type': 'percent',
})
@classmethod
def line_dict_to_bytes(cls, line_dict):
""" Helper method for creating the expected lines """
msg = b'1' + b';'.join([ # 0x31, command to add a line
line_dict.get('name', b''.ljust(36)), # 36 characters for the name
line_dict.get('vat_class', b'A'), # 1 symbol for vat class (a because the tax is 16.0%)
line_dict.get('price', b'1'), # up to 15 symbols for the unit price, tax included (up to 5 decimal places)
line_dict.get('uom', b'Uni'), # 3 symbols for uom
line_dict.get('item_code', b''.ljust(10)), # 10 symbols for item code (only reported when the tax is not 16.0%)
line_dict.get('item_desc', b''.ljust(20)), # item description (only reported when the tex is not 16.0%)
line_dict.get('vat_rate', b'16.0'), # vat rate
])
if line_dict.get('quantity'):
msg += b'*' + line_dict.get('quantity') # 1 to 10 symbols for quantity
if line_dict.get('discount'):
msg += b',' + line_dict.get('discount') # 1 to 7 symbols for discount/addition
return msg
@freeze_time('2023-01-01')
def test_export_simple_invoice(self):
""" The _l10n_ke_get_cu_messages function serialises the data from the invoice as a series
of messages representing commands to the device. The proxy must only wrap these messages
(with the checksum, etc) and send them to the device, and issue a response.
"""
simple_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_line_ids': [
(0, 0, {
'product_id': self.product_a.id,
'quantity': 10,
'price_unit': 1234.56,
'tax_ids': [(6, 0, [self.company_data['company'].account_sale_tax_id.id])],
'discount': 25,
}),
],
})
simple_invoice.action_post()
generated_messages = simple_invoice._l10n_ke_get_cu_messages()
expected_sale_line = self.line_dict_to_bytes({
'name': b'Infinite Improbability Drive ',
'price': b'1432.09', # This is the unit price, tax included
'quantity': b'10.0',
'discount': b'-25.0%',
})
expected_messages = [
# open invoice
b'01; 0;0;1;Sirius Cybernetics Corporation;A000123456F ;Test StreetFurther Test Street;Test StreetFurther Test Street;00500Nairobi ; ;INV202300001 ',
# sale of article
expected_sale_line,
# close invoice
b'8',
# read date / time
b'h',
]
self.assertEqual(generated_messages, expected_messages)
# Next assign the invoice a control unit number, and create a credit note from the invoice
simple_invoice.l10n_ke_cu_invoice_number = '42424200420000004242'
simple_credit_note = simple_invoice._reverse_moves()
simple_credit_note.action_post()
generated_messages = simple_credit_note._l10n_ke_get_cu_messages()
# The credit note of the simple invoice should have the same content, excepting that
expected_credit_note_header = [b''.join([
b'0', b'1;', b' 0;', b'0;',
b'A;', # This reserved 'field' is a capital 'A' instead of a '1'
b'Sirius Cybernetics Corporation;',
b'A000123456F ;',
b'Test StreetFurther Test Street;',
b'Test StreetFurther Test Street;',
b'00500Nairobi ;',
b' ;',
b'4242420042000000424;', # The 'Related invoice number' is the control unit number of the reversed invoice
b'RINV202300001 ', # The invoice number is the number of the credit note
])]
expected_messages = expected_credit_note_header + expected_messages[1:]
self.assertEqual(generated_messages, expected_messages)
@freeze_time('2023-01-01')
def test_export_global_discount_invoice(self):
""" Negative lines can be used as global discounts, the function that serialises the invoice
should recognise these discount lines, and subtract them from positive lines,
representing the subtraction as a discount. Existing discounts on lines should be
handled correctly too.
"""
global_discount_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_line_ids': [
(0, 0, {
'product_id': self.product_a.id,
'quantity': 10,
'price_unit': 10,
'tax_ids': [(6, 0, [self.company_data['company'].account_sale_tax_id.id])],
'discount': 10
}),
(0, 0, {
'name': "don't panic",
'quantity': 1,
'price_unit': -10,
'tax_ids': [(6, 0, [self.company_data['company'].account_sale_tax_id.id])],
}),
],
})
global_discount_invoice.action_post()
generated_messages = global_discount_invoice._l10n_ke_get_cu_messages()
expected_discounted_line = self.line_dict_to_bytes({
'name': b'Infinite Improbability Drive ',
'price': b'11.6',
'quantity': b'10.0',
# The discount is -20%, because there is an existing discount on the line of 10%, and
# another negative line with the amount -10 would be another -10% discount.
'discount': b'-20.0%',
})
expected_messages = [
b'01; 0;0;1;Sirius Cybernetics Corporation;A000123456F ;Test StreetFurther Test Street;Test StreetFurther Test Street;00500Nairobi ; ;INV202300001 ',
expected_discounted_line,
b'8',
b'h'
]
self.assertEqual(generated_messages, expected_messages)
# A copy of the invoice where the positive line is the product of a double negative
# (negative price and negative quantity) should yeild exactly the same representation.
double_negative_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_line_ids': [
(0, 0, {
'product_id': self.product_a.id,
'quantity': -10,
'price_unit': -10,
'tax_ids': [(6, 0, [self.company_data['company'].account_sale_tax_id.id])],
'discount': 10
}),
(0, 0, {
'name': "don't panic",
'quantity': 1,
'price_unit': -10,
'tax_ids': [(6, 0, [self.company_data['company'].account_sale_tax_id.id])],
}),
],
})
double_negative_invoice.action_post()
generated_messages = double_negative_invoice._l10n_ke_get_cu_messages()
# There representation is exactly the same, excepting that the name of the invoice is different
expected_double_negative_header = [b'01; 0;0;1;Sirius Cybernetics Corporation;A000123456F ;Test StreetFurther Test Street;Test StreetFurther Test Street;00500Nairobi ; ;INV202300002 ']
expected_messages = expected_double_negative_header + expected_messages[1:]
self.assertEqual(generated_messages, expected_messages)
def test_export_multi_tax_line_invoice(self):
""" When handling invoices with multiple taxes per line, the export should handle the
reported amounts correctly. Using only the VAT taxes in its calculation and not, for
instance, the 2% tourism levy, or the 4% drinks service charge, or the 10% food service
charge.
"""
tourism_levy = self.env['account.tax'].create({
'name': 'Tourism levy',
'amount': 2,
'company_id': self.company_data['company'].id,
})
multi_tax_line_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_line_ids': [
(0, 0, {
'product_id': self.product_a.id,
'quantity': 10,
'price_unit': 1000,
'tax_ids': [
(6, 0, [
self.company_data['company'].account_sale_tax_id.id,
tourism_levy.id,
]),
],
'discount': 25,
}),
],
})
multi_tax_line_invoice.action_post()
generated_messages = multi_tax_line_invoice._l10n_ke_cu_lines_messages()
expected_sale_line = self.line_dict_to_bytes({
'name': b'Infinite Improbability Drive ',
'price': b'1160', # This is the unit price, tax included, but only the 16% VAT
'quantity': b'10.0',
'discount': b'-25.0%',
})
self.assertEqual(generated_messages, [expected_sale_line])

View file

@ -0,0 +1,73 @@
<odoo>
<data>
<record id="l10n_ke_inherit_account_move_form" model="ir.ui.view">
<field name="name">l10n.ke.inherit.account.move.form</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="priority" eval="40"/>
<field name="arch" type="xml">
<xpath expr="//header/button[@name='action_post']" position="after">
<field name="l10n_ke_cu_qrcode" invisible="1"/>
<button name="l10n_ke_action_cu_post" type="object"
class="oe_highlight"
groups="account.group_account_manager"
string="Send To Fiscal Device"
attrs="{'invisible': ['|', '|', '|', ('country_code', '!=', 'KE'), ('l10n_ke_cu_qrcode', '!=', False), ('state', '!=', 'posted'), ('move_type', 'not in', ['out_invoice', 'out_refund'])]}"/>
</xpath>
<xpath expr="//group[@id='header_right_group']" position="inside">
<field name="l10n_ke_cu_invoice_number" attrs="{'invisible': [('country_code', '!=', 'KE')]}" readonly="1"/>
</xpath>
<notebook position="inside">
<page string="Tremol GO3 Fiscal Device" attrs="{'invisible': [('country_code', '!=', 'KE')]}">
<group>
<group>
<field name="l10n_ke_cu_qrcode" widget="url" readonly="1"/>
<field name="l10n_ke_cu_serial_number" readonly="1"/>
<field name="l10n_ke_cu_datetime" readonly="1"/>
</group>
</group>
</page>
</notebook>
</field>
</record>
<record id="l10n_ke_inherit_account_move_tree_view" model="ir.ui.view">
<field name="name">l10n.ke.inherit.account.move.tree</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_out_invoice_tree" />
<field name="arch" type="xml">
<field name="state" position="after">
<field name="l10n_ke_cu_invoice_number" optional="hide"/>
</field>
</field>
</record>
<record id="action_send_invoices_to_device" model="ir.actions.server">
<field name="name">Send to fiscal device</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="binding_model_id" ref="account.model_account_move"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">
action = records.l10n_ke_action_cu_post()
</field>
</record>
<record id="l10n_ke_inherit_account_move_search_view" model="ir.ui.view">
<field name="name">l10n.ke.inherit.account.move.search</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_account_invoice_filter" />
<field name="arch" type="xml">
<xpath expr="//field[@name='journal_id']" position="after">
<field name="l10n_ke_cu_invoice_number" string="Kenya CU Invoice Number" operator="ilike" />
</xpath>
<xpath expr="//filter[@name='cancel']" position="after">
<separator/>
<filter name="l10n_ke_edi_to_send" string="To Send to TIMS" domain="[('l10n_ke_cu_invoice_number', '=', False), ('state', '=', 'posted'), ('move_type', 'in', ['out_invoice', 'out_refund']), ('country_code', '=', 'KE')]"/>
</xpath>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="l10n_ke_inherit_product_template_form_view" model="ir.ui.view">
<field name="name">l10n.ke.inherit.product.template.form.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="account.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='invoicing']//group[@name='accounting']" position="inside">
<group name="HS Code" string="HS Code" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}">
<field name="l10n_ke_hsn_code"/>
<field name="l10n_ke_hsn_name"/>
</group>
</xpath>
</field>
</record>
<record id="l10n_ke_inherit_product_product_form_view" model="ir.ui.view">
<field name="name">l10n.ke.inherit.product.product.form</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<group>
<group name="KRA Item Code" string="KRA Item Code">
<field name="l10n_ke_hsn_code"/>
<field name="l10n_ke_hsn_name"/>
</group>
</group>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="l10n_ke_invoice" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='qrcode']" position="before">
<div t-if="o.country_code == 'KE'" id="l10n_ke_control_unit_information" style="page-break-inside:avoid;">
<b>Kenyan Fiscal Device Info</b>
<div class="row mt-4 mb-4">
<div class="col-auto col-3 mw-100 mb-2">
<p>
<b>Invoice Number: </b><br></br>
<span t-field="o.l10n_ke_cu_invoice_number"/>
</p>
<p>
<b>Serial Number: </b><br></br>
<span t-field="o.l10n_ke_cu_serial_number"/>
</p>
<p>
<b>Date and Time of Signing: </b><br></br>
<span t-field="o.l10n_ke_cu_datetime"/>
</p>
</div>
<div class="col-auto col-3 mw-100 mb-2">
<p t-if="o.l10n_ke_cu_qrcode">
<strong class="text-center">TIMS URL</strong><br/><br/>
<img style="display:block;" t-att-src="'/report/barcode/?barcode_type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('QR', quote_plus(o.l10n_ke_cu_qrcode), 130, 130)" alt="QR Code"/>
</p>
</div>
</div>
</div>
</xpath>
</template>
</odoo>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">l10n.ke.tremol.inherit.res.config.settings.form</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='account_vendor_bills']" position="after">
<div attrs="{'invisible':[('country_code', '!=', 'KE')]}">
<h2>Kenya TIMS Integration</h2>
<div class="row mt16 o_settings_container" id="l10n_ke_cu_details">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Tremol Device Settings</span>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
The tremol device makes use of a proxy server, which can be running locally on your computer or on an IoT Box.
The proxy server must be on the same network as the fiscal device.
</div>
<div class="content-group">
<div class="row mt8">
<label for="l10n_ke_cu_proxy_address" class="col-lg-5 o_light_label"/>
<field name="l10n_ke_cu_proxy_address"/>
</div>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_partner_view_form" model="ir.ui.view">
<field name="name">l10n.ke.tremol.inherit.res.partner.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml">
<group name="accounting_entries" position="after">
<group string="Kenya Accounting Details" name="l10n_ke_details">
<field name="l10n_ke_exemption_number"/>
</group>
</group>
</field>
</record>
</odoo>

View file

@ -0,0 +1,44 @@
[project]
name = "odoo-bringout-oca-ocb-l10n_ke_edi_tremol"
version = "16.0.0"
description = "Kenya Tremol Device EDI Integration -
Kenya Tremol Device EDI Integration
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-l10n_ke>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["l10n_ke_edi_tremol"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]