Skip to content
Snippets Groups Projects
Commit e5dd9dd2 authored by Mehdi Bendali Hacine's avatar Mehdi Bendali Hacine
Browse files

[ADD] l10n_sa_edi: Implement Saudi ZATCA invoicing standards

The ZATCA edi has an onboarding process which happens per journal.
A private key is generated for the company and with the data from
the company and journal a CSR is made to get a certificate to sign the
invoices.  In that process that contains multiple steps (see account_journal.py),
we also have to send the compliance files which are some example (simplified or not)
invoices/debit and credit notes.  We have a separate folder with those files
and also use them in the tests.  For the onboarding, they still need to be signed however.

For the sending of the customer invoices themselves, UBL 2.1 is used, but
with ZATCA style adaptations.  That is why we inherit from that class
to be able to add those specific adaptations.  This UBL needs to be
signed XadeS and in this case we need to do the hash with the sign information
present but with empty tags.

For ZATCA invoices, as is the case with Ticketbai, the previous hash is needed
for the next invoice and is stored per journal.  (so we have a chain of hashes)

Tests written by Simon (smdc)

Part-of: odoo/odoo#124901
parent 65c564b9
No related branches found
No related tags found
No related merge requests found
Showing
with 2426 additions and 0 deletions
# -*- coding: utf-8 -*-
from . import models, wizard
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Saudi Arabia - E-invoicing',
'icon': '/l10n_sa/static/description/icon.png',
'version': '0.1',
'depends': [
'account_edi_ubl_cii',
'account_debit_note',
'l10n_sa',
'base_vat'
],
'author': 'Odoo',
'summary': """
E-Invoicing, Universal Business Language
""",
'description': """
E-invoice implementation for the Kingdom of Saudi Arabia
""",
'category': 'Accounting/Localizations/EDI',
'license': 'LGPL-3',
'data': [
'security/ir.model.access.csv',
'data/account_edi_format.xml',
'data/ubl_21_zatca.xml',
'data/res_country_data.xml',
'wizard/l10n_sa_edi_otp_wizard.xml',
'views/account_tax_views.xml',
'views/account_journal_views.xml',
'views/res_partner_views.xml',
'views/res_company_views.xml',
'views/res_config_settings_view.xml',
'views/report_invoice.xml',
],
'demo': [
'demo/demo_company.xml',
],
'assets': {
'web.assets_backend': [
'l10n_sa_edi/static/src/scss/form_view.scss',
]
}
}
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="edi_sa_zatca" model="account.edi.format">
<field name="name">ZATCA (Saudi Arabia)</field>
<field name="code">sa_zatca</field>
</record>
</data>
</odoo>
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2"
exclude-result-prefixes="xs"
version="2.0">
<xsl:output omit-xml-declaration="yes" indent="no"/>
<xsl:template match="node() | @*">
<xsl:copy>
<xsl:apply-templates select="node() | @*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="//*[local-name()='Invoice']//*[local-name()='UBLExtensions']"></xsl:template>
<xsl:template match="//*[local-name()='AdditionalDocumentReference'][cbc:ID[normalize-space(text()) = 'QR']]"></xsl:template>
<xsl:template match="//*[local-name()='Invoice']//*[local-name()='Signature']"></xsl:template>
</xsl:stylesheet>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sa_partner_address_form" model="ir.ui.view">
<field name="name">sa.partner.form.address</field>
<field name="model">res.partner</field>
<field name="priority" eval="900"/>
<field name="arch" type="xml">
<form>
<div class="o_address_format">
<field name="parent_id" invisible="1"/>
<field name="type" invisible="1"/>
<field name="street" placeholder="Street" class="o_address_street"
attrs="{'readonly': [('type', '=', 'contact'),('parent_id', '!=', False)]}"/>
<field name="street2" placeholder="Neighborhood" class="o_address_street"
attrs="{'readonly': [('type', '=', 'contact'),('parent_id', '!=', False)]}"/>
<field name="city" placeholder="City" class="o_address_city"
attrs="{'readonly': [('type', '=', 'contact'),('parent_id', '!=', False)]}"/>
<field name="state_id" class="o_address_state" placeholder="State..." options='{"no_open": True}'
attrs="{'readonly': [('type', '=', 'contact'),('parent_id', '!=', False)]}"/>
<field name="zip" placeholder="ZIP" class="o_address_zip"
attrs="{'readonly': [('type', '=', 'contact'),('parent_id', '!=', False)]}"/>
<field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'
attrs="{'readonly': [('type', '=', 'contact'),('parent_id', '!=', False)]}"/>
<field name="l10n_sa_edi_building_number" placeholder="Building Number"
class="o_address_building_number" options='{"no_open": True, "no_create": True}'
attrs="{'readonly': [('type', '=', 'contact'),('parent_id', '!=', False)]}"/>
<field name="l10n_sa_edi_plot_identification" placeholder="Plot Identification"
class="o_address_plot_identification" options='{"no_open": True, "no_create": True}'
attrs="{'readonly': [('type', '=', 'contact'),('parent_id', '!=', False)]}"/>
</div>
</form>
</field>
</record>
<record id="base.sa" model="res.country">
<field name="address_view_id" ref="sa_partner_address_form" />
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(l10n_sa_edi_building_number)s %(l10n_sa_edi_plot_identification)s\n%(country_name)s'"/>
</record>
</odoo>
This diff is collapsed.
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="l10n_sa.partner_demo_company_sa" model="res.partner">
<field name="vat">310175397400003</field>
<field name="state_id" ref="base.state_sa_70"/>
<field name="street2">Somewhere close to Mecca</field>
<field name="l10n_sa_edi_building_number">1234</field>
<field name="l10n_sa_edi_plot_identification">1234</field>
<field name="l10n_sa_additional_identification_scheme">OTH</field>
<field name="l10n_sa_additional_identification_number">3101753974</field>
</record>
<record id="partner_demo_simplified" model="res.partner">
<field name="name">Mohammed Maamour</field>
<field name="street">Al Amir Mohammed Bin Abdul Aziz Street</field>
<field name="city">المدينة المنورة</field>
<field name="country_id" ref="base.sa"/>
<field name="state_id" ref="base.state_sa_70"/>
<field name="zip">42318</field>
<field name="phone">+966 55 777 8888</field>
<field name="email">info@company.saexample.com</field>
<field name="website">www.saexample.com</field>
<field name="l10n_sa_additional_identification_number">123456789</field>
</record>
<record id="partner_demo_standard" model="res.partner">
<field name="name">ARAMCO Medinah Branch</field>
<field name="street">Al Amir Mohammed Bin Abdul Aziz Street</field>
<field name="street2">Ammi Saysi</field>
<field name="city">المدينة المنورة</field>
<field name="country_id" ref="base.sa"/>
<field name="state_id" ref="base.state_sa_70"/>
<field name="zip">42317</field>
<field name="vat">311112111111113</field>
<field name="company_type">company</field>
<field name="phone">+966 55 999 1010</field>
<field name="email">info@company.saexample.com</field>
<field name="website">www.saexample.com</field>
<field name="l10n_sa_edi_building_number">1234</field>
<field name="l10n_sa_edi_plot_identification">1234</field>
<field name="l10n_sa_additional_identification_number">123456789</field>
</record>
<function model="account.journal" name="_l10n_sa_load_edi_demo_data">
<value model="account.journal"
eval="obj().search([
('type', '=', 'sale'),
('company_id', '=', ref('l10n_sa.demo_company_sa'))], limit=1).ids"/>
</function>
</odoo>
# -*- coding: utf-8 -*-
from . import account_edi_format
from . import account_journal
from . import account_move
from . import account_tax
from . import res_partner
from . import res_company
from . import res_config_settings
from . import account_edi_xml_ubl_21_zatca
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountEdiDocument(models.Model):
_inherit = 'account.edi.document'
def _prepare_jobs(self):
"""
Override to achieve the following:
If there is a job to process that may already be part of the chain (posted invoice that timed out),
Moves it at the beginning of the list.
"""
jobs = super()._prepare_jobs()
if len(jobs) > 1:
move_first_index = 0
for index, job in enumerate(jobs):
documents = job['documents']
if any(d.edi_format_id.code == 'sa_zatca' and d.state == 'to_send' and d.move_id.l10n_sa_chain_index for d in documents):
move_first_index = index
break
jobs = [jobs[move_first_index]] + jobs[:move_first_index] + jobs[move_first_index + 1:]
return jobs
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import uuid
import json
from markupsafe import Markup
from odoo import _, fields, models, api
from odoo.tools import float_repr
from datetime import datetime
from base64 import b64decode, b64encode
from lxml import etree
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.backends import default_backend
from cryptography.x509 import load_der_x509_certificate
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_sa_uuid = fields.Char(string='Document UUID (SA)', copy=False, help="Universally unique identifier of the Invoice")
l10n_sa_invoice_signature = fields.Char("Unsigned XML Signature", copy=False)
l10n_sa_chain_index = fields.Integer(
string="ZATCA chain index", copy=False, readonly=True,
help="Invoice index in chain, set if and only if an in-chain XML was submitted and did not error",
)
def _l10n_sa_is_simplified(self):
"""
Returns True if the customer is an individual, i.e: The invoice is B2C
:return:
"""
self.ensure_one()
return self.partner_id.company_type == 'person'
@api.depends('amount_total_signed', 'amount_tax_signed', 'l10n_sa_confirmation_datetime', 'company_id',
'company_id.vat', 'journal_id', 'journal_id.l10n_sa_production_csid_json',
'l10n_sa_invoice_signature', 'l10n_sa_chain_index')
def _compute_qr_code_str(self):
""" Override to update QR code generation in accordance with ZATCA Phase 2"""
for move in self:
move.l10n_sa_qr_code_str = ''
if move.country_code == 'SA' and move.move_type in ('out_invoice', 'out_refund') and move.l10n_sa_chain_index:
edi_format = self.env.ref('l10n_sa_edi.edi_sa_zatca')
zatca_document = move.edi_document_ids.filtered(lambda d: d.edi_format_id == edi_format)
if move._l10n_sa_is_simplified():
x509_cert = json.loads(move.journal_id.l10n_sa_production_csid_json)['binarySecurityToken']
xml_content = self.env.ref('l10n_sa_edi.edi_sa_zatca')._l10n_sa_generate_zatca_template(move)
qr_code_str = move._l10n_sa_get_qr_code(move.journal_id, xml_content, b64decode(x509_cert), move.l10n_sa_invoice_signature, move._l10n_sa_is_simplified())
move.l10n_sa_qr_code_str = b64encode(qr_code_str).decode()
elif zatca_document.state == 'sent' and zatca_document.attachment_id.datas:
document_xml = zatca_document.attachment_id.datas.decode()
root = etree.fromstring(b64decode(document_xml))
qr_node = root.xpath('//*[local-name()="ID"][text()="QR"]/following-sibling::*/*')[0]
move.l10n_sa_qr_code_str = qr_node.text
def _l10n_sa_get_qr_code_encoding(self, tag, field, int_length=1):
"""
Helper function to encode strings for the QR code generation according to ZATCA specs
"""
company_name_tag_encoding = tag.to_bytes(length=1, byteorder='big')
company_name_length_encoding = len(field).to_bytes(length=int_length, byteorder='big')
return company_name_tag_encoding + company_name_length_encoding + field
def _l10n_sa_check_refund_reason(self):
"""
Make sure credit/debit notes have a valid reason and reversal reference
"""
self.ensure_one()
return self.reversed_entry_id and self.ref
@api.model
def _l10n_sa_get_qr_code(self, journal_id, unsigned_xml, x509_cert, signature, is_b2c=False):
"""
Generate QR code string based on XML content of the Invoice UBL file, X509 Production Certificate
and company info.
:return b64 encoded QR code string
"""
def xpath_ns(expr):
return root.xpath(expr, namespaces=edi_format._l10n_sa_get_namespaces())[0].text.strip()
qr_code_str = ''
root = etree.fromstring(unsigned_xml)
edi_format = self.env['account.edi.xml.ubl_21.zatca']
# Indent XML content to avoid indentation mismatches
etree.indent(root, space=' ')
invoice_date = xpath_ns('//cbc:IssueDate')
invoice_time = xpath_ns('//cbc:IssueTime')
invoice_datetime = datetime.strptime(invoice_date + ' ' + invoice_time, '%Y-%m-%d %H:%M:%S')
if invoice_datetime and journal_id.company_id.vat and x509_cert:
prehash_content = etree.tostring(root)
invoice_hash = edi_format._l10n_sa_generate_invoice_xml_hash(prehash_content, 'digest')
amount_total = float(xpath_ns('//cbc:TaxInclusiveAmount'))
amount_tax = float(xpath_ns('//cac:TaxTotal/cbc:TaxAmount'))
x509_certificate = load_der_x509_certificate(b64decode(x509_cert), default_backend())
seller_name_enc = self._l10n_sa_get_qr_code_encoding(1, journal_id.company_id.display_name.encode())
seller_vat_enc = self._l10n_sa_get_qr_code_encoding(2, journal_id.company_id.vat.encode())
timestamp_enc = self._l10n_sa_get_qr_code_encoding(3,
invoice_datetime.strftime("%Y-%m-%dT%H:%M:%SZ").encode())
amount_total_enc = self._l10n_sa_get_qr_code_encoding(4, float_repr(abs(amount_total), 2).encode())
amount_tax_enc = self._l10n_sa_get_qr_code_encoding(5, float_repr(abs(amount_tax), 2).encode())
invoice_hash_enc = self._l10n_sa_get_qr_code_encoding(6, invoice_hash)
signature_enc = self._l10n_sa_get_qr_code_encoding(7, signature.encode())
public_key_enc = self._l10n_sa_get_qr_code_encoding(8,
x509_certificate.public_key().public_bytes(Encoding.DER,
PublicFormat.SubjectPublicKeyInfo))
qr_code_str = (seller_name_enc + seller_vat_enc + timestamp_enc + amount_total_enc +
amount_tax_enc + invoice_hash_enc + signature_enc + public_key_enc)
if is_b2c:
qr_code_str += self._l10n_sa_get_qr_code_encoding(9, x509_certificate.signature)
return qr_code_str
@api.depends('state', 'edi_document_ids.state')
def _compute_edi_show_cancel_button(self):
"""
Override to hide the EDI Cancellation button at all times for ZATCA Invoices
"""
super()._compute_edi_show_cancel_button()
for move in self.filtered(lambda m: m.is_invoice() and m.country_code == 'SA'):
move.edi_show_cancel_button = False
@api.depends('state', 'edi_document_ids.state')
def _compute_show_reset_to_draft_button(self):
"""
Override to hide the Reset to Draft button for ZATCA Invoices that have been successfully submitted
"""
super()._compute_show_reset_to_draft_button()
for move in self:
# An invoice should only have an index chain if it was successfully submitted without rejection,
# or if the submission timed out. In both cases, a user should not be able to reset it to draft.
if move.l10n_sa_chain_index:
move.show_reset_to_draft_button = False
def _l10n_sa_generate_unsigned_data(self):
"""
Generate UUID and digital signature to be used during both Signing and QR code generation.
It is necessary to save the signature as it changes everytime it is generated and both the signing and the
QR code expect to have the same, identical signature.
"""
self.ensure_one()
edi_format = self.env.ref('l10n_sa_edi.edi_sa_zatca')
# Build the dict of values to be used for generating the Invoice XML content
# Set Invoice field values required for generating the XML content, hash and signature
self.l10n_sa_uuid = uuid.uuid4()
# We generate the XML content
xml_content = edi_format._l10n_sa_generate_zatca_template(self)
# Once the required values are generated, we hash the invoice, then use it to generate a Signature
invoice_hash_hex = self.env['account.edi.xml.ubl_21.zatca']._l10n_sa_generate_invoice_xml_hash(xml_content).decode()
self.l10n_sa_invoice_signature = edi_format._l10n_sa_get_digital_signature(self.journal_id.company_id,
invoice_hash_hex).decode()
return xml_content
def _l10n_sa_log_results(self, xml_content, response_data=None, error=False):
"""
Save submitted invoice XML hash in case of either Rejection or Acceptance.
"""
self.ensure_one()
self.journal_id.l10n_sa_latest_submission_hash = self.env['account.edi.xml.ubl_21.zatca']._l10n_sa_generate_invoice_xml_hash(
xml_content)
bootstrap_cls, title, content = ("success", _("Invoice Successfully Submitted to ZATCA"),
"" if (not error or not response_data) else response_data)
if error:
bootstrap_cls, title = ("danger", _("Invoice was rejected by ZATCA"))
content = Markup("""
<p class='mb-0'>
%s
</p>
<hr>
<p class='mb-0'>
%s
</p>
""") % (_('The invoice was rejected by ZATCA. Please, check the response below:'), response_data)
if response_data and response_data.get('validationResults', {}).get('warningMessages'):
bootstrap_cls, title = ("warning", _("Invoice was Accepted by ZATCA (with Warnings)"))
content = Markup("""
<p class='mb-0'>
%s
</p>
<hr>
<p class='mb-0'>
%s
</p>
""") % (_('The invoice was accepted by ZATCA, but returned warnings. Please, check the response below:'), "<br/>".join([Markup("<b>%s</b> : %s") % (m['code'], m['message']) for m in response_data['validationResults']['warningMessages']]))
self.message_post(body=Markup("""
<div role='alert' class='alert alert-%s'>
<h4 class='alert-heading'>%s</h4>%s
</div>
""") % (bootstrap_cls, title, content))
def _l10n_sa_is_in_chain(self):
"""
If the invoice was successfully posted and confirmed by the government, then this would return True.
If the invoice timed out, then its edi_document should still be in the 'to_send' state.
"""
zatca_doc_ids = self.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'sa_zatca')
return len(zatca_doc_ids) > 0 and not any(zatca_doc_ids.filtered(lambda d: d.state == 'to_send'))
from odoo import fields, models, api, _
from odoo.exceptions import UserError
EXEMPTION_REASON_CODES = [
('VATEX-SA-29', 'VATEX-SA-29 Financial services mentioned in Article 29 of the VAT Regulations.'),
('VATEX-SA-29-7', 'VATEX-SA-29-7 Life insurance services mentioned in Article 29 of the VAT.'),
('VATEX-SA-30', 'VATEX-SA-30 Real estate transactions mentioned in Article 30 of the VAT Regulations.'),
('VATEX-SA-32', 'VATEX-SA-32 Export of goods.'),
('VATEX-SA-33', 'VATEX-SA-33 Export of Services.'),
('VATEX-SA-34-1', 'VATEX-SA-34-1 The international transport of Goods.'),
('VATEX-SA-34-2', 'VATEX-SA-34-1 The international transport of Passengers.'),
('VATEX-SA-34-3', 'VATEX-SA-34-3 Services directly connected and incidental to a Supply of international passenger transport.'),
('VATEX-SA-34-4', 'VATEX-SA-34-4 Supply of a qualifying means of transport.'),
('VATEX-SA-34-5', 'VATEX-SA-34-5 Any services relating to Goods or passenger transportation, as defined in article twenty five of these Regulations.'),
('VATEX-SA-35', 'VATEX-SA-35 Medicines and medical equipment.'),
('VATEX-SA-36', 'VATEX-SA-36 Qualifying metals.'),
('VATEX-SA-EDU', 'VATEX-SA-EDU Private education to citizen.'),
('VATEX-SA-HEA', 'VATEX-SA-HEA Private healthcare to citizen.')
]
class AccountTax(models.Model):
_inherit = 'account.tax'
l10n_sa_is_retention = fields.Boolean("Is Retention", default=False,
help="Determines whether or not a tax counts as a Withholding Tax")
l10n_sa_exemption_reason_code = fields.Selection(string="Exemption Reason Code",
selection=EXEMPTION_REASON_CODES, help="Tax Exemption Reason Code (ZATCA)")
@api.onchange('amount')
def onchange_amount(self):
super().onchange_amount()
self.l10n_sa_is_retention = False
@api.constrains("l10n_sa_is_retention", "amount", "type_tax_use")
def _l10n_sa_constrain_is_retention(self):
for tax in self:
if tax.amount >= 0 and tax.l10n_sa_is_retention and tax.type_tax_use == 'sale':
raise UserError(_("Cannot set a tax to Retention if the amount is greater than or equal 0"))
class AccountTaxTemplate(models.Model):
_inherit = 'account.tax.template'
l10n_sa_is_retention = fields.Boolean("Is Retention", default=False,
help="Determines whether or not a tax counts as a Withholding Tax")
l10n_sa_exemption_reason_code = fields.Selection(string="Exemption Reason Code",
selection=EXEMPTION_REASON_CODES, help="Tax Exemption Reason Code (ZATCA)")
def _get_tax_vals(self, company, tax_template_to_tax):
# OVERRIDE
res = super()._get_tax_vals(company, tax_template_to_tax)
res['l10n_sa_is_retention'] = self.l10n_sa_is_retention
res['l10n_sa_exemption_reason_code'] = self.l10n_sa_exemption_reason_code
return res
import re
from odoo import models, fields
from odoo.exceptions import UserError
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
class ResCompany(models.Model):
_inherit = "res.company"
def _l10n_sa_generate_private_key(self):
"""
Compute a private key for each company that will be used to generate certificate signing requests (CSR)
in order to receive X509 certificates from the ZATCA APIs and sign EDI documents
- public_exponent=65537 is a default value that should be used most of the time, as per the documentation
of cryptography.
- key_size=2048 is considered a reasonable default key size, as per the documentation of cryptography.
See https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
"""
private_key = ec.generate_private_key(ec.SECP256K1, default_backend())
return private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption())
l10n_sa_private_key = fields.Binary("ZATCA Private key", attachment=False, groups="base.group_system", copy=False,
help="The private key used to generate the CSR and obtain certificates",)
l10n_sa_api_mode = fields.Selection(
[('sandbox', 'Sandbox'), ('preprod', 'Simulation (Pre-Production)'), ('prod', 'Production')],
help="Specifies which API the system should use", required=True,
default='sandbox', copy=False)
l10n_sa_edi_building_number = fields.Char(compute='_compute_address',
inverse='_l10n_sa_edi_inverse_building_number')
l10n_sa_edi_plot_identification = fields.Char(compute='_compute_address',
inverse='_l10n_sa_edi_inverse_plot_identification')
l10n_sa_additional_identification_scheme = fields.Selection(
related='partner_id.l10n_sa_additional_identification_scheme', readonly=False)
l10n_sa_additional_identification_number = fields.Char(
related='partner_id.l10n_sa_additional_identification_number', readonly=False)
def write(self, vals):
for company in self:
if 'l10n_sa_api_mode' in vals:
if company.l10n_sa_api_mode == 'prod' and vals['l10n_sa_api_mode'] != 'prod':
raise UserError("You cannot change the ZATCA Submission Mode once it has been set to Production")
journals = self.env['account.journal'].search([('company_id', '=', company.id)])
journals._l10n_sa_reset_certificates()
journals.l10n_sa_latest_submission_hash = False
return super().write(vals)
def _get_company_address_field_names(self):
""" Override to add ZATCA specific address fields """
return super()._get_company_address_field_names() + \
['l10n_sa_edi_building_number', 'l10n_sa_edi_plot_identification']
def _l10n_sa_edi_inverse_building_number(self):
for company in self:
company.partner_id.l10n_sa_edi_building_number = company.l10n_sa_edi_building_number
def _l10n_sa_edi_inverse_plot_identification(self):
for company in self:
company.partner_id.l10n_sa_edi_plot_identification = company.l10n_sa_edi_plot_identification
def _l10n_sa_get_csr_invoice_type(self):
"""
Return the Invoice Type flag used in the CSR. 4-digit numerical input using 0 & 1 mapped to “TSCZ” where:
- 0: False/Not supported, 1: True/Supported
- T: Tax Invoice (Standard), S: Simplified Invoice, C & Z will be used in the future and should
always be 0
For example: 1100 would mean the Solution will be generating Standard and Simplified invoices.
We can assume Odoo-powered EGS solutions will always generate both Standard & Simplified invoices
:return:
"""
return '1100'
def _l10n_sa_check_organization_unit(self):
"""
Check company Organization Unit according to ZATCA specifications
Standards:
BR-KSA-39
BR-KSA-40
See https://zatca.gov.sa/ar/RulesRegulations/Taxes/Documents/20210528_ZATCA_Electronic_Invoice_XML_Implementation_Standard_vShared.pdf
"""
self.ensure_one()
if not self.vat:
return False
return len(self.vat) == 15 and bool(re.match(r'^3\d{13}3$', self.vat))
from odoo import models, fields, api, _
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
l10n_sa_api_mode = fields.Selection(related='company_id.l10n_sa_api_mode', readonly=False)
@api.depends('company_id')
def _compute_company_informations(self):
super()._compute_company_informations()
for record in self:
if self.company_id.country_code == 'SA':
record.company_informations += _('\nBuilding Number: %s, Plot Identification: %s \nNeighborhood: %s') % (self.company_id.l10n_sa_edi_building_number, self.company_id.l10n_sa_edi_plot_identification, self.company_id.street2)
from odoo import fields, models, api
class ResPartner(models.Model):
_inherit = 'res.partner'
l10n_sa_edi_building_number = fields.Char("Building Number")
l10n_sa_edi_plot_identification = fields.Char("Plot Identification")
l10n_sa_additional_identification_scheme = fields.Selection([
('TIN', 'Tax Identification Number'),
('CRN', 'Commercial Registration Number'),
('MOM', 'Momra License'),
('MLS', 'MLSD License'),
('700', '700 Number'),
('SAG', 'Sagia License'),
('NAT', 'National ID'),
('GCC', 'GCC ID'),
('IQA', 'Iqama Number'),
('PAS', 'Passport ID'),
('OTH', 'Other ID')
], default="OTH", string="Identification Scheme", help="Additional Identification scheme for Seller/Buyer")
l10n_sa_additional_identification_number = fields.Char("Identification Number (SA)",
help="Additional Identification Number for Seller/Buyer")
@api.model
def _commercial_fields(self):
return super()._commercial_fields() + ['l10n_sa_edi_building_number',
'l10n_sa_edi_plot_identification',
'l10n_sa_additional_identification_scheme',
'l10n_sa_additional_identification_number']
def _address_fields(self):
return super()._address_fields() + ['l10n_sa_edi_building_number',
'l10n_sa_edi_plot_identification']
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
l10n_sa_edi_otp_wizard,l10n_sa_edi_otp_wizard,model_l10n_sa_edi_otp_wizard,account.group_account_invoice,1,1,1,0
.o_form_view {
.o_address_format {
.o_address_building_number,
.o_address_plot_identification {
margin-right: 2%;
}
}
&.o_form_editable .o_address_format {
.o_address_building_number {
width: 48%;
}
.o_address_plot_identification {
width: 48%;
margin-right: 0;
}
}
}
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_edi_zatca
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment