[ADD] l10n_my_ubl_pint: Malaysian Peppol PINT specialization

Add support for the Malaysian Peppol PINT specialization,
allowing to generate the related XML file.

This also adds a few additional features that are in the
specifications:

- SST registration number, needed in the xml and on the
invoice.
- TTx registration number, needed on the invoice.

Both numbers are optional, but if the business is registered
in any of them they are required to display these on the
invoices and they are required to enforce some rules. (cannot
send taxes that are not 0% "Out of scope" if not registered with
SST, ..)

Task id # 3647922

closes odoo/odoo#159407

Signed-off-by: Julien Van Roy (juvr) <juvr@odoo.com>
This commit is contained in:
Nicolas Viseur (vin)
2024-04-10 11:45:53 +00:00
parent 7f947871f0
commit 7b4624b078
11 changed files with 471 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+19
View File
@@ -0,0 +1,19 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Malaysia - UBL PINT',
'countries': ['my'],
'version': '1.0',
'category': 'Accounting/Localizations/EDI',
'icon': '/account/static/description/l10n.png',
'description': """
The UBL PINT e-invoicing format for Malaysia is based on the Peppol International (PINT) model for Billing.
""",
'depends': ['account_edi_ubl_cii'],
'data': [
'views/report_invoice.xml',
'views/res_company_view.xml',
'views/res_partner_view.xml',
],
'installable': True,
'license': 'LGPL-3'
}
@@ -0,0 +1,100 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_my_ubl_pint
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-09 02:43+0000\n"
"PO-Revision-Date: 2024-04-09 02:43+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_my_ubl_pint
#: model:ir.model,name:l10n_my_ubl_pint.model_res_company
msgid "Companies"
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model,name:l10n_my_ubl_pint.model_base_document_layout
msgid "Company Document Layout"
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model,name:l10n_my_ubl_pint.model_res_partner
msgid "Contact"
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_base_document_layout__account_fiscal_country_id
msgid "Fiscal Country"
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_partner__ubl_cii_format
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_users__ubl_cii_format
msgid "Format"
msgstr ""
#. module: l10n_my_ubl_pint
#. odoo-python
#: code:addons/l10n_my_ubl_pint/models/account_edi_xml_pint_my.py:0
#, python-format
msgid ""
"If your business is registered for SST, please provide your registration number in your company details.\n"
"Otherwise, you are not allowed to charge sales or services taxes in the e-Invoice."
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model,name:l10n_my_ubl_pint.model_account_edi_xml_pint_my
msgid ""
"Malaysian implementation of Peppol International (PINT) model for Billing"
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model.fields.selection,name:l10n_my_ubl_pint.selection__res_partner__ubl_cii_format__pint_my
msgid "PINT Malaysia"
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_base_document_layout__sst_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_company__sst_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_partner__sst_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_users__sst_registration_number
msgid "SST"
msgstr ""
#. module: l10n_my_ubl_pint
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_report_invoice_document
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_bold
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_boxed
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_standard
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_striped
msgid "SST:"
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_base_document_layout__ttx_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_company__ttx_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_partner__ttx_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_users__ttx_registration_number
msgid "TTx"
msgstr ""
#. module: l10n_my_ubl_pint
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_bold
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_boxed
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_standard
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_striped
msgid "TTx:"
msgstr ""
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,help:l10n_my_ubl_pint.field_base_document_layout__account_fiscal_country_id
msgid "The country to use the tax reports from for this company"
msgstr ""
+103
View File
@@ -0,0 +1,103 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_my_ubl_pint
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-04-09 02:44+0000\n"
"PO-Revision-Date: 2024-04-09 02:44+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_my_ubl_pint
#: model:ir.model,name:l10n_my_ubl_pint.model_res_company
msgid "Companies"
msgstr "Syarikat"
#. module: l10n_my_ubl_pint
#: model:ir.model,name:l10n_my_ubl_pint.model_base_document_layout
msgid "Company Document Layout"
msgstr "Susun Atur Dokumen Syarikat"
#. module: l10n_my_ubl_pint
#: model:ir.model,name:l10n_my_ubl_pint.model_res_partner
msgid "Contact"
msgstr "Kontak"
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_base_document_layout__account_fiscal_country_id
msgid "Fiscal Country"
msgstr "Negara Fiskal"
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_partner__ubl_cii_format
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_users__ubl_cii_format
msgid "Format"
msgstr "Format"
#. module: l10n_my_ubl_pint
#. odoo-python
#: code:addons/l10n_my_ubl_pint/models/account_edi_xml_pint_my.py:0
#, python-format
msgid ""
"If your business is registered for SST, please provide your registration number in your company details.\n"
"Otherwise, you are not allowed to charge sales or services taxes in the e-Invoice."
msgstr ""
"Jika perniagaan anda didaftarkan untuk SST, sila berikan nombor pendaftaran anda dalam butiran syarikat anda.\n"
"Jika tidak, anda tidak dibenarkan mengenakan cukai jualan atau perkhidmatan dalam e-Invois."
#. module: l10n_my_ubl_pint
#: model:ir.model,name:l10n_my_ubl_pint.model_account_edi_xml_pint_my
msgid ""
"Malaysian implementation of Peppol International (PINT) model for Billing"
msgstr ""
"Pelaksanaan model Peppol International (PINT) Malaysia untuk Pengebilan"
#. module: l10n_my_ubl_pint
#: model:ir.model.fields.selection,name:l10n_my_ubl_pint.selection__res_partner__ubl_cii_format__pint_my
msgid "PINT Malaysia"
msgstr "PINT Malaysia"
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_base_document_layout__sst_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_company__sst_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_partner__sst_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_users__sst_registration_number
msgid "SST"
msgstr "SST"
#. module: l10n_my_ubl_pint
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_report_invoice_document
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_bold
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_boxed
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_standard
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_striped
msgid "SST:"
msgstr "SST:"
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_base_document_layout__ttx_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_company__ttx_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_partner__ttx_registration_number
#: model:ir.model.fields,field_description:l10n_my_ubl_pint.field_res_users__ttx_registration_number
msgid "TTx"
msgstr "TTx"
#. module: l10n_my_ubl_pint
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_bold
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_boxed
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_standard
#: model_terms:ir.ui.view,arch_db:l10n_my_ubl_pint.l10n_my_ubl_pint_external_layout_striped
msgid "TTx:"
msgstr "TTx:"
#. module: l10n_my_ubl_pint
#: model:ir.model.fields,help:l10n_my_ubl_pint.field_base_document_layout__account_fiscal_country_id
msgid "The country to use the tax reports from for this company"
msgstr "Negara untuk menggunakan laporan cukai daripada syarikat ini"
@@ -0,0 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_edi_xml_pint_my
from . import res_company
from . import res_partner
@@ -0,0 +1,107 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
class AccountEdiXmlUBLPINTMY(models.AbstractModel):
_inherit = "account.edi.xml.ubl_bis3"
_name = 'account.edi.xml.pint_my'
_description = "Malaysian implementation of Peppol International (PINT) model for Billing"
"""
* PINT Official documentation: https://docs.peppol.eu/poac/pint/pint/
* PINT MY Official documentation: https://docs.peppol.eu/poac/my/pint-my
"""
def _export_invoice_filename(self, invoice):
# EXTENDS account_edi_ubl_cii
return f"{invoice.name.replace('/', '_')}_pint_my.xml"
def _export_invoice_vals(self, invoice):
# EXTENDS account_edi_ubl_cii
vals = super()._export_invoice_vals(invoice)
vals['vals'].update({
# see https://docs.peppol.eu/poac/my/pint-my/bis/#profiles
'customization_id': 'urn:peppol:pint:billing-1@my-1',
'profile_id': 'urn:peppol:bis:billing',
})
if invoice.currency_id != invoice.company_id.currency_id:
# see https://docs.peppol.eu/poac/my/pint-my/bis/#_tax_in_accounting_currency
vals['vals']['tax_currency_code'] = invoice.company_id.currency_id.name # accounting currency
return vals
def _get_invoice_tax_totals_vals_list(self, invoice, taxes_vals):
# EXTENDS account_edi_ubl_cii
vals_list = super()._get_invoice_tax_totals_vals_list(invoice, taxes_vals)
company_currency = invoice.company_id.currency_id
if invoice.currency_id != company_currency:
# see https://docs.peppol.eu/poac/my/pint-my/bis/#_tax_in_accounting_currency
vals_list.append({
'currency': company_currency,
'currency_dp': company_currency.decimal_places,
'tax_amount': taxes_vals['tax_amount'],
'tax_subtotal_vals': [],
})
return vals_list
def _get_partner_party_tax_scheme_vals_list(self, partner, role):
""" [aligned-ibrp-cl-01-my]-Malaysian invoice tax categories MUST be coded using Malaysian codes. """
# EXTENDS account_edi_ubl_cii
tax_scheme_vals_list = super()._get_partner_party_tax_scheme_vals_list(partner, role)
# See https://docs.peppol.eu/poac/my/pint-my/bis/#_seller_tax_identifier
tax_scheme_vals_list[0]['company_id'] = partner.sst_registration_number or 'NA'
if role == 'supplier':
# TIN
gst_tax_scheme = tax_scheme_vals_list[0].copy()
gst_tax_scheme.update({
'company_id': partner.vat,
'tax_scheme_vals': {'id': 'GST'},
})
tax_scheme_vals_list.append(gst_tax_scheme)
return tax_scheme_vals_list
def _get_tax_unece_codes(self, invoice, tax):
"""
In malaysia, only the following codes can be used: T, E, O
https://docs.peppol.eu/poac/my/pint-my/bis/#_tax_category_code
"""
# OVERRIDE account_edi_ubl_cii
codes = {
'tax_category_code': False,
'tax_exemption_reason_code': False,
'tax_exemption_reason': False,
}
# If a business is not registered for SST and/or TTx, the business is not allowed to charge sales tax,
# service tax or tourism tax in the e-Invoice.
# In this case, the tax category code should be 'O' (Outside scope of tax).
# For now, we do not properly support Tourism tax (TTx) due to a lack of clarity on the subject.
supplier = invoice.company_id.partner_id.commercial_partner_id
if not supplier.sst_registration_number:
codes['tax_category_code'] = 'O'
elif tax.amount != 0:
codes['tax_category_code'] = 'T'
else:
codes['tax_category_code'] = 'E'
return codes
def _export_invoice_constraints(self, invoice, vals):
# EXTENDS account_edi_ubl_cii
constraints = super()._export_invoice_constraints(invoice, vals)
# A tax category "Outside of tax cope" can only have an amount of 0.
for tax_total_val in vals['vals']['tax_total_vals']:
for tax_subtotal_val in tax_total_val['tax_subtotal_vals']:
tax_category_vals = tax_subtotal_val['tax_category_vals']
if tax_category_vals['tax_category_code'] == 'O' and tax_category_vals['percent'] != 0:
constraints['peppol_my_sst_registration'] = _(
"If your business is registered for SST, please provide your registration number in your company details.\n"
"Otherwise, you are not allowed to charge sales or services taxes in the e-Invoice."
)
break
# In malaysia, tax on good is paid at the manufacturer level. It is thus common to invoice without taxes,
# unless invoicing for a service.
constraints.pop('tax_on_line', '')
constraints.pop('cen_en16931_tax_line', '')
return constraints
@@ -0,0 +1,17 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class ResCompany(models.Model):
_inherit = 'res.company'
sst_registration_number = fields.Char(related='partner_id.sst_registration_number', readonly=False)
ttx_registration_number = fields.Char(related='partner_id.ttx_registration_number', readonly=False)
class BaseDocumentLayout(models.TransientModel):
_inherit = 'base.document.layout'
account_fiscal_country_id = fields.Many2one(related="company_id.account_fiscal_country_id")
sst_registration_number = fields.Char(related='company_id.sst_registration_number')
ttx_registration_number = fields.Char(related='company_id.ttx_registration_number')
@@ -0,0 +1,23 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class ResPartner(models.Model):
_inherit = 'res.partner'
ubl_cii_format = fields.Selection(selection_add=[('pint_my', "PINT Malaysia")])
sst_registration_number = fields.Char(string="SST")
ttx_registration_number = fields.Char(string="TTx")
def _get_edi_builder(self):
# EXTENDS 'account_edi_ubl_cii'
if self.ubl_cii_format == 'pint_my':
return self.env['account.edi.xml.pint_my']
return super()._get_edi_builder()
def _compute_ubl_cii_format(self):
# EXTENDS 'account_edi_ubl_cii'
super()._compute_ubl_cii_format()
for partner in self:
if partner.country_code == 'MY':
partner.ubl_cii_format = 'pint_my'
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Add the SST and TTx to the company -->
<template id="l10n_my_ubl_pint_external_layout_standard" inherit_id="web.external_layout_standard">
<xpath expr="//ul[@name='company_address_list']" position="inside">
<li t-if="company.sst_registration_number and company.account_fiscal_country_id.code == 'MY'">
SST: <span t-field="company.sst_registration_number"/>
</li>
<li t-if="company.ttx_registration_number and company.account_fiscal_country_id.code == 'MY'">
TTx: <span t-field="company.ttx_registration_number"/>
</li>
</xpath>
</template>
<template id="l10n_my_ubl_pint_external_layout_bold" inherit_id="web.external_layout_bold">
<xpath expr="//ul[@name='company_address_list']" position="inside">
<li t-if="company.sst_registration_number and company.account_fiscal_country_id.code == 'MY'">
SST: <span t-field="company.sst_registration_number"/>
</li>
<li t-if="company.ttx_registration_number and company.account_fiscal_country_id.code == 'MY'">
TTx: <span t-field="company.ttx_registration_number"/>
</li>
</xpath>
</template>
<template id="l10n_my_ubl_pint_external_layout_boxed" inherit_id="web.external_layout_boxed">
<xpath expr="//ul[@name='company_address_list']" position="inside">
<li t-if="company.sst_registration_number and company.account_fiscal_country_id.code == 'MY'">
SST: <span t-field="company.sst_registration_number"/>
</li>
<li t-if="company.ttx_registration_number and company.account_fiscal_country_id.code == 'MY'">
TTx: <span t-field="company.ttx_registration_number"/>
</li>
</xpath>
</template>
<template id="l10n_my_ubl_pint_external_layout_striped" inherit_id="web.external_layout_striped">
<xpath expr="//ul[@name='company_address_list']" position="inside">
<li t-if="company.sst_registration_number and company.account_fiscal_country_id.code == 'MY'">
SST: <span t-field="company.sst_registration_number"/>
</li>
<li t-if="company.ttx_registration_number and company.account_fiscal_country_id.code == 'MY'">
TTx: <span t-field="company.ttx_registration_number"/>
</li>
</xpath>
</template>
<!-- add the SST to the partner -->
<template id="l10n_my_report_invoice_document" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='partner_vat_address_not_same_as_shipping']" position="after">
<div t-if="o.partner_id.sst_registration_number and o.partner_id.country_code == 'MY'">
SST: <span t-field="o.partner_id.sst_registration_number"/>
</div>
</xpath>
<xpath expr="//div[@id='partner_vat_address_same_as_shipping']" position="after">
<div t-if="o.partner_id.sst_registration_number and o.partner_id.country_code == 'MY'">
SST: <span t-field="o.partner_id.sst_registration_number"/>
</div>
</xpath>
<xpath expr="//div[@id='partner_vat_no_shipping']" position="after">
<div t-if="o.partner_id.sst_registration_number and o.partner_id.country_code == 'MY'">
SST: <span t-field="o.partner_id.sst_registration_number"/>
</div>
</xpath>
</template>
</odoo>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_company_form_inherit_l10n_my_ubl_pint" model="ir.ui.view">
<field name="name">res.company.form.inherit.l10n_my_ubl_pint</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='currency_id']" position="after">
<field name="sst_registration_number" invisible="country_code != 'MY'"/>
<field name="ttx_registration_number" invisible="country_code != 'MY'"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_partner_form_inherit_l10n_my_ubl_pint" model="ir.ui.view">
<field name="name">res.partner.form.inherit.l10n_my_ubl_pint</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="sst_registration_number" invisible="country_code != 'MY'"/>
<field name="ttx_registration_number" invisible="country_code != 'MY'"/>
</xpath>
</field>
</record>
</odoo>