diff --git a/addons/hw_drivers/iot_handlers/drivers/L10nKeEDISerialDriver.py b/addons/hw_drivers/iot_handlers/drivers/L10nKeEDISerialDriver.py new file mode 100644 index 0000000000000000000000000000000000000000..6214e6b9b3795bbdcd17959f3bc165666c0cbc73 --- /dev/null +++ b/addons/hw_drivers/iot_handlers/drivers/L10nKeEDISerialDriver.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import serial +import time +import struct +import json +from functools import reduce + +from odoo import http +from odoo.addons.hw_drivers.iot_handlers.drivers.SerialBaseDriver import SerialDriver, SerialProtocol, serial_connection +from odoo.addons.hw_drivers.main import iot_devices + +_logger = logging.getLogger(__name__) + +TremolG03Protocol = SerialProtocol( + name='Tremol G03', + baudrate=115200, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + timeout=3, + writeTimeout=0.2, + measureRegexp=None, + statusRegexp=None, + commandTerminator=b'', + commandDelay=0.4, + measureDelay=0.4, + newMeasureDelay=0.2, + measureCommand=b'', + emptyAnswerValid=False, +) + +STX = 0x02 +ETX = 0x0A +ACK = 0x06 +NACK = 0x15 + +FD_ERRORS = { + 0x30: 'OK', + 0x32: 'Registers overflow', + 0x33: 'Clock failure or incorrect date & time', + 0x34: 'Opened fiscal receipt', + 0x39: 'Incorrect password', + 0x3b: '24 hours block - missing Z report', + 0x3d: 'Interrupt power supply in fiscal receipt (one time until status is read)', + 0x3e: 'Overflow EJ', + 0x3f: 'Insufficient conditions', +} + +COMMAND_ERRORS = { + 0x30: 'OK', + 0x31: 'Invalid command', + 0x32: 'Illegal command', + 0x33: 'Z daily report is not zero', + 0x34: 'Syntax error', + 0x35: 'Input registers orverflow', + 0x36: 'Zero input registers', + 0x37: 'Unavailable transaction for correction', + 0x38: 'Insufficient amount on hand', +} + + +class TremolG03Driver(SerialDriver): + """Driver for the Kenyan Tremol G03 fiscal device.""" + + _protocol = TremolG03Protocol + + def __init__(self, identifier, device): + super().__init__(identifier, device) + self.device_type = 'fiscal_data_module' + self.message_number = 0 + + @classmethod + def get_default_device(cls): + fiscal_devices = list(filter(lambda d: iot_devices[d].device_type == 'fiscal_data_module', iot_devices)) + return len(fiscal_devices) and iot_devices[fiscal_devices[0]] + + @classmethod + def supported(cls, device): + """Checks whether the device, which port info is passed as argument, is supported by the driver. + + :param device: path to the device + :type device: str + :return: whether the device is supported by the driver + :rtype: bool + """ + protocol = cls._protocol + try: + protocol = cls._protocol + with serial_connection(device['identifier'], protocol) as connection: + connection.write(b'\x09') + time.sleep(protocol.commandDelay) + response = connection.read(1) + if response == b'\x40': + return True + + except serial.serialutil.SerialTimeoutException: + pass + except Exception: + _logger.exception('Error while probing %s with protocol %s', device, protocol.name) + + # ---------------- + # HELPERS + # ---------------- + + @staticmethod + def generate_checksum(message): + """ Generate the checksum bytes for the bytes provided. + + :param message: bytes representing the part of the message from which the checksum is calculated + :returns: two checksum bytes calculated from the message + + This checksum is calculated as: + 1) XOR of all bytes of the bytes + 2) Conversion of the one XOR byte into the two bytes of the checksum by + adding 30h to each half-byte of the XOR + + eg. to_check = \x12\x23\x34\x45\x56 + XOR of all bytes in to_check = \x16 + checksum generated as \x16 -> \x31 \x36 + """ + xor = reduce(lambda a, b: a ^ b, message) + return bytes([(xor >> 4) + 0x30, (xor & 0xf) + 0x30]) + + # ---------------- + # COMMUNICATION + # ---------------- + + def send(self, msgs): + """ Send and receive messages to/from the fiscal device over serial connection + + Generate the wrapped message from the msgs and send them to the device. + The wrapping contains the <STX> (starting byte) <LEN> (length byte) + and <NBL> (message number byte) at the start and two <CS> (checksum + bytes), and the <ETX> line-feed byte at the end. + :param msgs: A list of byte strings representing the <CMD> and <DATA> + components of the serial message. + :return: A list of the responses (if any) from the device. If the + response is an ack, it wont be part of this list. + """ + + with self._device_lock: + replies = [] + for msg in msgs: + self.message_number += 1 + core_message = struct.pack('BB%ds' % (len(msg)), len(msg) + 34, self.message_number + 32, msg) + request = struct.pack('B%ds2sB' % (len(core_message)), STX, core_message, self.generate_checksum(core_message), ETX) + time.sleep(self._protocol.commandDelay) + self._connection.write(request) + time.sleep(self._protocol.measureDelay) + response = self._connection.read_all() + if not response: + self.data['status'] = "no response" + _logger.error("Sent request: %s,\n Received no response", request) + self.abort_post() + break + if response[0] == ACK: + # In the case where either byte is not 0x30, there has been an error + if response[2] != 0x30 or response[3] != 0x30: + self.data['status'] = response[2:4].decode('cp1251') + _logger.error( + "Sent request: %s,\n Received fiscal device error: %s \n Received command error: %s", + request, FD_ERRORS.get(response[2], 'Unknown fiscal device error'), + COMMAND_ERRORS.get(response[3], 'Unknown command error'), + ) + self.abort_post() + break + replies.append('') + elif response[0] == NACK: + self.data['status'] = "Received NACK" + _logger.error("Sent request: %s,\n Received NACK \x15", request) + self.abort_post() + break + elif response[0] == 0x02: + self.data['status'] = "ok" + size = response[1] - 35 + reply = response[4:4 + size] + replies.append(reply.decode('cp1251')) + return {'replies': replies, 'status': self.data['status']} + + def abort_post(self): + """ Cancel the posting of the invoice + + In the event of an error, it is better to try to cancel the posting of + the invoice, since the state of the invoice on the device will remain + open otherwise, blocking further invoices being sent. + """ + self.message_number += 1 + abort = struct.pack('BBB', 37, self.message_number + 32, 0x39) + request = struct.pack('B3s2sB', STX, abort, self.generate_checksum(abort), ETX) + time.sleep(self._protocol.commandDelay) + self._connection.write(request) + time.sleep(self._protocol.measureDelay) + response = self._connection.read_all() + if response and response[0] == 0x02: + self.data['status'] += "\n The invoice could not be cancelled." + else: + self.data['status'] += "\n The invoice was successfully cancelled" + + +class TremolG03Controller(http.Controller): + + @http.route('/hw_proxy/l10n_ke_cu_send', type='http', auth='none', cors='*', csrf=False, save_session=False, methods=['POST']) + def l10n_ke_cu_send(self, messages, company_vat): + """ Posts the messages sent to this endpoint to the fiscal device connected to the server + + :param messages: The messages (consisting of <CMD> and <DATA>) to + send to the fiscal device. + :returns: Dictionary containing a list of the responses from + fiscal device and status of the fiscal device. + """ + device = TremolG03Driver.get_default_device() + if device: + # First run the command to get the fiscal device numbers + device_numbers = device.send([b'\x60']) + # If the vat doesn't match, abort + if device_numbers['status'] != 'ok': + return device_numbers + serial_number, device_vat, _dummy = device_numbers['replies'][0].split(';') + if device_vat != company_vat: + return json.dumps({'status': 'The company vat number does not match that of the device'}) + messages = json.loads(messages) + resp = json.dumps({**device.send([msg.encode('cp1251') for msg in messages]), 'serial_number': serial_number}) + return resp + else: + return json.dumps({'status': 'The fiscal device is not connected to the proxy server'}) diff --git a/addons/l10n_ke/__manifest__.py b/addons/l10n_ke/__manifest__.py index 03f11c74ad2d23690374ef12c14cca356aa72543..3484373977d62b5b7e5d178169a89ffbda6dfe40 100644 --- a/addons/l10n_ke/__manifest__.py +++ b/addons/l10n_ke/__manifest__.py @@ -17,6 +17,7 @@ This provides a base chart of accounts and taxes template for use in Odoo. 'data/account.account.template.csv', 'data/l10n_ke_chart_data.xml', 'data/account_tax_group_data.xml', + 'data/account_tax_report_data.xml', 'data/account_tax_template_data.xml', 'data/account_fiscal_position_template.xml', 'data/account_chart_template_configure_data.xml', diff --git a/addons/l10n_ke/data/account_tax_report_data.xml b/addons/l10n_ke/data/account_tax_report_data.xml new file mode 100644 index 0000000000000000000000000000000000000000..94683c37cb19acfd6951e63ccef8854adb3e2170 --- /dev/null +++ b/addons/l10n_ke/data/account_tax_report_data.xml @@ -0,0 +1,206 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="tax_report_ke" model="account.report"> + <field name="name">Tax Report</field> + <field name="root_report_id" ref="account.generic_tax_report"/> + <field name="country_id" ref="base.ke"/> + <field name="filter_fiscal_position" eval="True"/> + <field name="availability_condition">country</field> + <field name="column_ids"> + <record id="tax_report_base_column" model="account.report.column"> + <field name="name">Base</field> + <field name="expression_label">base</field> + </record> + <record id="tax_report_tax_column" model="account.report.column"> + <field name="name">VAT</field> + <field name="expression_label">tax</field> + </record> + </field> + <field name="line_ids"> + <record id="tax_report_line_general_rate_sales" model="account.report.line"> + <field name="name">1. Taxable Sales (General Rate 16%)</field> + <field name="sequence">1</field> + <field name="code">box_1</field> + <field name="expression_ids"> + <record id="tax_report_general_rate_sales_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">tax_tags</field> + <field name="formula">16% Sales Base</field> + </record> + <record id="tax_report_general_rate_sales_tax_tag" model="account.report.expression"> + <field name="label">tax</field> + <field name="engine">tax_tags</field> + <field name="formula">16% Sales Tax</field> + </record> + </field> + </record> + <record id="tax_report_line_other_rate_sales" model="account.report.line"> + <field name="name">2. Taxable Sales (Other Rate 8%)</field> + <field name="sequence">2</field> + <field name="code">box_2</field> + <field name="expression_ids"> + <record id="tax_report_other_rate_sales_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">tax_tags</field> + <field name="formula">8% Sales Base</field> + </record> + <record id="tax_report_other_rate_sales_tax_tag" model="account.report.expression"> + <field name="label">tax</field> + <field name="engine">tax_tags</field> + <field name="formula">8% Sales Tax</field> + </record> + </field> + </record> + <record id="tax_report_line_zero_rated_sales" model="account.report.line"> + <field name="name">3. Sales (Zero Rated 0%)</field> + <field name="sequence">3</field> + <field name="code">box_3</field> + <field name="expression_ids"> + <record id="tax_report_zero_rated_sales_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">tax_tags</field> + <field name="formula">Zero Rated Sales Base</field> + </record> + </field> + </record> + <record id="tax_report_line_exempt_sales" model="account.report.line"> + <field name="name">4. Sales (Exempt)</field> + <field name="sequence">4</field> + <field name="code">box_4</field> + <field name="expression_ids"> + <record id="tax_report_exempt_sales_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">tax_tags</field> + <field name="formula">Exempt Sales Base</field> + </record> + </field> + </record> + <record id="tax_report_line_total_sales" model="account.report.line"> + <field name="name">5. Total Sales</field> + <field name="sequence">5</field> + <field name="code">box_5</field> + <field name="expression_ids"> + <record id="tax_report_total_sales_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">aggregation</field> + <field name="formula">box_1.base + box_2.base + box_3.base + box_4.base</field> + </record> + <record id="tax_report_total_sales_tax_tag" model="account.report.expression"> + <field name="label">tax</field> + <field name="engine">aggregation</field> + <field name="formula">box_1.tax + box_2.tax</field> + </record> + </field> + </record> + <record id="tax_report_line_output_vat" model="account.report.line"> + <field name="name">6. Total Output VAT</field> + <field name="sequence">6</field> + <field name="code">box_6</field> + <field name="expression_ids"> + <record id="tax_report_output_vat_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">aggregation</field> + <field name="formula">box_1.base + box_2.base + box_3.base</field> + </record> + <record id="tax_report_output_vat_tax_tag" model="account.report.expression"> + <field name="label">tax</field> + <field name="engine">aggregation</field> + <field name="formula">box_1.tax + box_2.tax</field> + </record> + </field> + </record> + <record id="tax_report_line_general_rate_purchases" model="account.report.line"> + <field name="name">7. Taxable Purchases (General Rate 16%)</field> + <field name="sequence">7</field> + <field name="code">box_7</field> + <field name="expression_ids"> + <record id="tax_report_general_rate_purchases_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">tax_tags</field> + <field name="formula">16% Purchases Base</field> + </record> + <record id="tax_report_general_rate_purchases_tax_tag" model="account.report.expression"> + <field name="label">tax</field> + <field name="engine">tax_tags</field> + <field name="formula">16% Purchases Tax</field> + </record> + </field> + </record> + <record id="tax_report_line_other_rate_purchases" model="account.report.line"> + <field name="name">8. Taxable Purchases (Other Rate 8%)</field> + <field name="sequence">8</field> + <field name="code">box_8</field> + <field name="expression_ids"> + <record id="tax_report_other_rate_purchases_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">tax_tags</field> + <field name="formula">8% purchases Base</field> + </record> + <record id="tax_report_other_rate_purchases_tax_tag" model="account.report.expression"> + <field name="label">tax</field> + <field name="engine">tax_tags</field> + <field name="formula">8% Purchases Tax</field> + </record> + </field> + </record> + <record id="tax_report_line_zero_rated_purchases" model="account.report.line"> + <field name="name">9. Purchases (Zero Rated 0%)</field> + <field name="sequence">9</field> + <field name="code">box_9</field> + <field name="expression_ids"> + <record id="tax_report_zero_rated_purchases_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">tax_tags</field> + <field name="formula">Zero Rated Purchases Base</field> + </record> + </field> + </record> + <record id="tax_report_line_exempt_purchases" model="account.report.line"> + <field name="name">10. Purchases (Exempt)</field> + <field name="sequence">10</field> + <field name="code">box_10</field> + <field name="expression_ids"> + <record id="tax_report_exempt_purchases_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">tax_tags</field> + <field name="formula">Exempt Purchases Base</field> + </record> + </field> + </record> + <record id="tax_report_line_total_purchases" model="account.report.line"> + <field name="name">11. Total Purchases</field> + <field name="sequence">11</field> + <field name="code">box_11</field> + <field name="expression_ids"> + <record id="tax_report_total_purchases_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">aggregation</field> + <field name="formula">box_7.base + box_8.base + box_9.base + box_10.base</field> + </record> + <record id="tax_report_total_purchases_tax_tag" model="account.report.expression"> + <field name="label">tax</field> + <field name="engine">aggregation</field> + <field name="formula">box_7.tax + box_8.tax</field> + </record> + </field> + </record> + <record id="tax_report_line_input_vat" model="account.report.line"> + <field name="name">12. Total Input VAT</field> + <field name="sequence">12</field> + <field name="code">box_12</field> + <field name="expression_ids"> + <record id="tax_report_input_vat_base_tag" model="account.report.expression"> + <field name="label">base</field> + <field name="engine">aggregation</field> + <field name="formula">box_7.base + box_8.base + box_9.base</field> + </record> + <record id="tax_report_input_vat_tax_tag" model="account.report.expression"> + <field name="label">tax</field> + <field name="engine">aggregation</field> + <field name="formula">box_7.tax + box_8.tax</field> + </record> + </field> + </record> + </field> + </record> +</odoo> diff --git a/addons/l10n_ke/data/account_tax_template_data.xml b/addons/l10n_ke/data/account_tax_template_data.xml index 7f11bf14a74b920e4836742ca514e82b12403658..12e7ae6be559a51c7112fa0ed7092ed517bf9f80 100644 --- a/addons/l10n_ke/data/account_tax_template_data.xml +++ b/addons/l10n_ke/data/account_tax_template_data.xml @@ -9,17 +9,27 @@ <field name="amount">16</field> <field name="tax_group_id" ref="tax_group_16"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), (0,0, { + 'repartition_type': 'base', + 'plus_report_expression_ids': [ref('tax_report_general_rate_sales_base_tag')], + }), + (0,0, { + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('ke2200'), + 'plus_report_expression_ids': [ref('tax_report_general_rate_sales_tax_tag')], }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), (0,0, { + 'repartition_type': 'base', + 'minus_report_expression_ids': [ref('tax_report_general_rate_sales_base_tag')], + }), + (0,0, { + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('ke2200'), + 'minus_report_expression_ids': [ref('tax_report_general_rate_sales_tax_tag')], }), ]"/> </record> @@ -32,17 +42,27 @@ <field name="amount">8</field> <field name="tax_group_id" ref="tax_group_8"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), (0,0, { + 'repartition_type': 'base', + 'plus_report_expression_ids': [ref('tax_report_other_rate_sales_base_tag')], + }), + (0,0, { + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('ke2200'), + 'plus_report_expression_ids': [ref('tax_report_other_rate_sales_tax_tag')], }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), (0,0, { + 'repartition_type': 'base', + 'minus_report_expression_ids': [ref('tax_report_other_rate_sales_base_tag')], + }), + (0,0, { + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('ke2200'), + 'minus_report_expression_ids': [ref('tax_report_other_rate_sales_tax_tag')], }), ]"/> </record> @@ -55,12 +75,24 @@ <field name="amount">0</field> <field name="tax_group_id" ref="tax_group_0"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), - (0,0, {'repartition_type': 'tax'}), + (0,0, { + 'repartition_type': 'base', + 'plus_report_expression_ids': [ref('tax_report_zero_rated_sales_base_tag')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), - (0,0, {'repartition_type': 'tax'}), + (0,0, { + 'repartition_type': 'base', + 'minus_report_expression_ids': [ref('tax_report_zero_rated_sales_base_tag')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), ]"/> </record> <record id="STEX" model="account.tax.template"> @@ -72,12 +104,24 @@ <field name="amount">0</field> <field name="tax_group_id" ref="tax_group_0"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), - (0,0, {'repartition_type': 'tax'}), + (0,0, { + 'repartition_type': 'base', + 'plus_report_expression_ids': [ref('tax_report_exempt_sales_base_tag')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), - (0,0, {'repartition_type': 'tax'}), + (0,0, { + 'repartition_type': 'base', + 'minus_report_expression_ids': [ref('tax_report_exempt_sales_base_tag')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), ]"/> </record> <record id="SWT3" model="account.tax.template"> @@ -273,17 +317,27 @@ <field name="amount">16</field> <field name="tax_group_id" ref="tax_group_16"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), (0,0, { + 'repartition_type': 'base', + 'plus_report_expression_ids': [ref('tax_report_general_rate_purchases_base_tag')], + }), + (0,0, { + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('ke1110'), + 'plus_report_expression_ids': [ref('tax_report_general_rate_purchases_tax_tag')], }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), (0,0, { + 'repartition_type': 'base', + 'minus_report_expression_ids': [ref('tax_report_general_rate_purchases_base_tag')], + }), + (0,0, { + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('ke1110'), + 'minus_report_expression_ids': [ref('tax_report_general_rate_purchases_tax_tag')], }), ]"/> </record> @@ -296,17 +350,27 @@ <field name="amount">8</field> <field name="tax_group_id" ref="tax_group_8"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), (0,0, { + 'repartition_type': 'base', + 'plus_report_expression_ids': [ref('tax_report_other_rate_purchases_base_tag')], + }), + (0,0, { + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('ke1110'), + 'plus_report_expression_ids': [ref('tax_report_other_rate_purchases_tax_tag')], }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), (0,0, { + 'repartition_type': 'base', + 'minus_report_expression_ids': [ref('tax_report_other_rate_purchases_base_tag')], + }), + (0,0, { + 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('ke1110'), + 'minus_report_expression_ids': [ref('tax_report_other_rate_purchases_tax_tag')], }), ]"/> </record> @@ -319,12 +383,24 @@ <field name="amount">0</field> <field name="tax_group_id" ref="tax_group_0"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), - (0,0, {'repartition_type': 'tax'}), + (0,0, { + 'repartition_type': 'base', + 'plus_report_expression_ids': [ref('tax_report_zero_rated_purchases_base_tag')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), - (0,0, {'repartition_type': 'tax'}), + (0,0, { + 'repartition_type': 'base', + 'minus_report_expression_ids': [ref('tax_report_zero_rated_purchases_base_tag')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), ]"/> </record> <record id="PTEX" model="account.tax.template"> @@ -336,12 +412,24 @@ <field name="amount">0</field> <field name="tax_group_id" ref="tax_group_0"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), - (0,0, {'repartition_type': 'tax'}), + (0,0, { + 'repartition_type': 'base', + 'plus_report_expression_ids': [ref('tax_report_exempt_purchases_base_tag')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), - (0,0, {'repartition_type': 'base'}), - (0,0, {'repartition_type': 'tax'}), + (0,0, { + 'repartition_type': 'base', + 'minus_report_expression_ids': [ref('tax_report_exempt_purchases_base_tag')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), ]"/> </record> <record id="PWT3" model="account.tax.template"> diff --git a/addons/l10n_ke_edi_tremol/__init__.py b/addons/l10n_ke_edi_tremol/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0650744f6bc69b9f0b865e8c7174c813a5f5995e --- /dev/null +++ b/addons/l10n_ke_edi_tremol/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/l10n_ke_edi_tremol/__manifest__.py b/addons/l10n_ke_edi_tremol/__manifest__.py new file mode 100644 index 0000000000000000000000000000000000000000..4adae9254a280c76e0e67fa69d2f4fe76eecdb43 --- /dev/null +++ b/addons/l10n_ke_edi_tremol/__manifest__.py @@ -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', + ], + }, +} diff --git a/addons/l10n_ke_edi_tremol/models/__init__.py b/addons/l10n_ke_edi_tremol/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b090c18797a2cf16ddc714f63510c059c8e817fe --- /dev/null +++ b/addons/l10n_ke_edi_tremol/models/__init__.py @@ -0,0 +1,5 @@ +from . import account_move +from . import product +from . import res_company +from . import res_config_settings +from . import res_partner diff --git a/addons/l10n_ke_edi_tremol/models/account_move.py b/addons/l10n_ke_edi_tremol/models/account_move.py new file mode 100644 index 0000000000000000000000000000000000000000..51ac1a5a85e3ce5db95c76f929d05494caa5af94 --- /dev/null +++ b/addons/l10n_ke_edi_tremol/models/account_move.py @@ -0,0 +1,228 @@ +# -*- 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 + + Find misconfigurations on the move, the lines of the move, and the + taxes on those lines that would result in rejection by the KRA. + """ + self.ensure_one() + errors = [] + # The credit note should refer to the control unit number (receipt number) of the original + # invoice to which it relates. + if self.move_type == 'out_refund' and not self.reversed_entry_id.l10n_ke_cu_invoice_number: + 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'): + if not line.tax_ids or len(line.tax_ids) > 1: + errors.append(_("On line %s, you must select one and only one tax.", line.name)) + else: + if line.tax_ids.amount == 0 and not (line.product_id and line.product_id.l10n_ke_hsn_code and line.product_id.l10n_ke_hsn_name): + 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)) + + for tax in self.invoice_line_ids.tax_ids: + if tax.amount not in (16, 8, 0): + errors.append(_("Tax '%s' is used, but only taxes of 16%%, 8%%, 0%% or Exempt can be sent. Please reconfigure or change the tax.", tax.name)) + + 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 '') + 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(self.commercial_partner_id.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_unit < 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 + else: + currency_rate = abs(self.invoice_line_ids[0].balance / self.invoice_line_ids[0].price_subtotal) + + 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 = [] + 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 = round(line.price_total / line.quantity * 100 / (100 - line.discount), 2) * currency_rate + percentage = line.tax_ids[0].amount + + # 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 = re.sub('[^0-9.]+', '', line.product_id.l10n_ke_hsn_name)[:20].ljust(20).encode('cp1251') 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') + str(price)[:13].encode('cp1251'), # 1 to 13 symbols for article's price + 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(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): + 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): + self.ensure_one() + # Check the configuration of the invoice + errors = self._l10n_ke_validate_move() + if errors: + raise UserError(_("Invalid invoice configuration:\n\n%s") % '\n'.join(errors)) + return { + 'type': 'ir.actions.client', + 'tag': 'post_send', + 'params': { + 'messages': json.dumps([m.decode('cp1251') for m in self._l10n_ke_get_cu_messages()]), + 'move_id': self.id, + 'proxy_address': self.company_id.l10n_ke_cu_proxy_address, + 'company_vat': self.company_id.vat, + } + } + + def l10n_ke_cu_response(self, response): + move = self.browse(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'), + }) diff --git a/addons/l10n_ke_edi_tremol/models/product.py b/addons/l10n_ke_edi_tremol/models/product.py new file mode 100644 index 0000000000000000000000000000000000000000..7a59d27ae83d22b932ad3dedad52c9ed8e18b6ea --- /dev/null +++ b/addons/l10n_ke_edi_tremol/models/product.py @@ -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='HSN code', + help="Product code needed in case of not 16%. ", + ) + l10n_ke_hsn_name = fields.Char( + string='HSN description', + help="Product code description needed in case of not 16%. ", + ) + +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, + ) diff --git a/addons/l10n_ke_edi_tremol/models/res_company.py b/addons/l10n_ke_edi_tremol/models/res_company.py new file mode 100644 index 0000000000000000000000000000000000000000..69c502ac18b60ad9dc0ea79bbbe558b5518c3fe6 --- /dev/null +++ b/addons/l10n_ke_edi_tremol/models/res_company.py @@ -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='Control Unit Proxy Address', + help='The address of the proxy server for the control unit.', + ) diff --git a/addons/l10n_ke_edi_tremol/models/res_config_settings.py b/addons/l10n_ke_edi_tremol/models/res_config_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..3614cae3e150e51b2502f8657e08edaa52557f1b --- /dev/null +++ b/addons/l10n_ke_edi_tremol/models/res_config_settings.py @@ -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) diff --git a/addons/l10n_ke_edi_tremol/models/res_partner.py b/addons/l10n_ke_edi_tremol/models/res_partner.py new file mode 100644 index 0000000000000000000000000000000000000000..9a1c9788800c92e5354fb6aa30589b1727bc8440 --- /dev/null +++ b/addons/l10n_ke_edi_tremol/models/res_partner.py @@ -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'] diff --git a/addons/l10n_ke_edi_tremol/static/src/js/send_invoice.js b/addons/l10n_ke_edi_tremol/static/src/js/send_invoice.js new file mode 100644 index 0000000000000000000000000000000000000000..8d88d67ac260f032e1f35ca1d7779a33d3b80a45 --- /dev/null +++ b/addons/l10n_ke_edi_tremol/static/src/js/send_invoice.js @@ -0,0 +1,34 @@ +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}) { + const move_id = params.move_id; + await ajax.post(params.proxy_address + '/hw_proxy/l10n_ke_cu_send', params).then(function (res) { + const res_obj = JSON.parse(res); + if (res_obj.status != "ok") { + Dialog.alert(this, "Posting the invoice has failed, with the message: \n" + res_obj.status); + } else { + 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}], + }).then(function () { + parent.services.action.doAction({ + 'type': 'ir.actions.client', + 'tag': 'reload', + }); + }, function () { + Dialog.alert(this, _t("Error trying to connect to Odoo. Check your internet connection")); + }) + } + }, function () { + Dialog.alert(this, _t("Error trying to connect to the middleware. Is the middleware running?")); + }) + } + core.action_registry.add('post_send', post_send); + return post_send; +}); diff --git a/addons/l10n_ke_edi_tremol/views/account_move_view.xml b/addons/l10n_ke_edi_tremol/views/account_move_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..7c4ae0096b2c2cf8a6fa98708fa3cee901c4e3fe --- /dev/null +++ b/addons/l10n_ke_edi_tremol/views/account_move_view.xml @@ -0,0 +1,58 @@ +<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 Invoice To 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 Control Unit" 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="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" filter_domain="[('l10n_ke_cu_invoice_number', 'ilike', self')]" /> + </xpath> + </field> + </record> + </data> +</odoo> + + diff --git a/addons/l10n_ke_edi_tremol/views/product_view.xml b/addons/l10n_ke_edi_tremol/views/product_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..5829232e1e82d37ee2e8c8489876490b442351f8 --- /dev/null +++ b/addons/l10n_ke_edi_tremol/views/product_view.xml @@ -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="HS Code" string="HS Code"> + <field name="l10n_ke_hsn_code"/> + <field name="l10n_ke_hsn_name"/> + </group> + </group> + </xpath> + </field> + </record> +</odoo> diff --git a/addons/l10n_ke_edi_tremol/views/report_invoice.xml b/addons/l10n_ke_edi_tremol/views/report_invoice.xml new file mode 100644 index 0000000000000000000000000000000000000000..c7491ec277978edefc2978b3356cd9abf0f0f10e --- /dev/null +++ b/addons/l10n_ke_edi_tremol/views/report_invoice.xml @@ -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 Control Unit 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> diff --git a/addons/l10n_ke_edi_tremol/views/res_config_settings_view.xml b/addons/l10n_ke_edi_tremol/views/res_config_settings_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..7b7c899f3972d926234bc72b74f208a4136dbd94 --- /dev/null +++ b/addons/l10n_ke_edi_tremol/views/res_config_settings_view.xml @@ -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> diff --git a/addons/l10n_ke_edi_tremol/views/res_partner_views.xml b/addons/l10n_ke_edi_tremol/views/res_partner_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..31c65cf33f38523a8620bfc62bb62449a0288873 --- /dev/null +++ b/addons/l10n_ke_edi_tremol/views/res_partner_views.xml @@ -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>