mirror of
https://github.com/bringout/oca-ocb-l10n_me-africa.git
synced 2026-04-27 14:42:03 +02:00
Initial commit: L10N_Me Africa packages
This commit is contained in:
commit
c265268138
611 changed files with 75334 additions and 0 deletions
48
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/README.md
Normal file
48
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/README.md
Normal 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
|
||||
32
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/ARCHITECTURE.md
Normal 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.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for l10n_ke_edi_tremol. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [l10n_ke](../../odoo-bringout-oca-ocb-l10n_ke)
|
||||
4
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/FAQ.md
Normal 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.
|
||||
7
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/INSTALL.md
Normal 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"
|
||||
```
|
||||
17
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/MODELS.md
Normal file
17
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/MODELS.md
Normal 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.
|
||||
6
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/OVERVIEW.md
Normal 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
|
||||
3
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
8
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/SECURITY.md
Normal file
8
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/SECURITY.md
Normal 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
|
||||
|
|
@ -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.
|
||||
7
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/USAGE.md
Normal 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
|
||||
```
|
||||
3
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/WIZARDS.md
Normal file
3
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- neutralize connection to tremol controle unit
|
||||
UPDATE res_company
|
||||
SET l10n_ke_cu_proxy_address = '';
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from . import account_move
|
||||
from . import product
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import res_partner
|
||||
|
|
@ -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'),
|
||||
})
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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.',
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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']
|
||||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from .import test_move_export
|
||||
|
|
@ -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])
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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&value=%s&width=%s&height=%s' % ('QR', quote_plus(o.l10n_ke_cu_qrcode), 130, 130)" alt="QR Code"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
44
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/pyproject.toml
Normal file
44
odoo-bringout-oca-ocb-l10n_ke_edi_tremol/pyproject.toml
Normal 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",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue