From e5dd9dd2380dd020762d01a36b728110a148fd3f Mon Sep 17 00:00:00 2001
From: Mehdi Bendali Hacine <mebe@odoo.com>
Date: Tue, 22 Nov 2022 06:08:58 +0000
Subject: [PATCH] [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
---
 addons/l10n_sa_edi/__init__.py                |   2 +
 addons/l10n_sa_edi/__manifest__.py            |  44 ++
 .../l10n_sa_edi/data/account_edi_format.xml   |  11 +
 addons/l10n_sa_edi/data/pre-hash_invoice.xsl  |  19 +
 addons/l10n_sa_edi/data/res_country_data.xml  |  38 ++
 addons/l10n_sa_edi/data/ubl_21_zatca.xml      | 285 ++++++++
 addons/l10n_sa_edi/demo/demo_company.xml      |  52 ++
 addons/l10n_sa_edi/models/__init__.py         |   9 +
 .../models/account_edi_document.py            |  28 +
 .../l10n_sa_edi/models/account_edi_format.py  | 471 +++++++++++++
 .../models/account_edi_xml_ubl_21_zatca.py    | 410 ++++++++++++
 addons/l10n_sa_edi/models/account_journal.py  | 624 ++++++++++++++++++
 addons/l10n_sa_edi/models/account_move.py     | 204 ++++++
 addons/l10n_sa_edi/models/account_tax.py      |  58 ++
 addons/l10n_sa_edi/models/res_company.py      |  93 +++
 .../l10n_sa_edi/models/res_config_settings.py |  14 +
 addons/l10n_sa_edi/models/res_partner.py      |  36 +
 .../l10n_sa_edi/security/ir.model.access.csv  |   2 +
 .../static/src/scss/form_view.scss            |  21 +
 addons/l10n_sa_edi/tests/__init__.py          |   5 +
 addons/l10n_sa_edi/tests/common.py            | 245 +++++++
 .../tests/compliance/simplified/credit.xml    | 225 +++++++
 .../tests/compliance/simplified/debit.xml     | 226 +++++++
 .../tests/compliance/simplified/invoice.xml   | 218 ++++++
 .../tests/compliance/standard/credit.xml      | 222 +++++++
 .../tests/compliance/standard/debit.xml       | 223 +++++++
 .../tests/compliance/standard/invoice.xml     | 213 ++++++
 addons/l10n_sa_edi/tests/test_edi_zatca.py    | 124 ++++
 .../views/account_journal_views.xml           |  94 +++
 .../l10n_sa_edi/views/account_tax_views.xml   |  16 +
 addons/l10n_sa_edi/views/report_invoice.xml   |  66 ++
 .../l10n_sa_edi/views/res_company_views.xml   |  24 +
 .../views/res_config_settings_view.xml        |  43 ++
 .../l10n_sa_edi/views/res_partner_views.xml   |  18 +
 addons/l10n_sa_edi/wizard/__init__.py         |   3 +
 .../l10n_sa_edi/wizard/account_debit_note.py  |  15 +
 .../wizard/account_move_reversal.py           |  15 +
 .../wizard/l10n_sa_edi_otp_wizard.py          |  28 +
 .../wizard/l10n_sa_edi_otp_wizard.xml         |  30 +
 39 files changed, 4474 insertions(+)
 create mode 100644 addons/l10n_sa_edi/__init__.py
 create mode 100644 addons/l10n_sa_edi/__manifest__.py
 create mode 100644 addons/l10n_sa_edi/data/account_edi_format.xml
 create mode 100644 addons/l10n_sa_edi/data/pre-hash_invoice.xsl
 create mode 100644 addons/l10n_sa_edi/data/res_country_data.xml
 create mode 100644 addons/l10n_sa_edi/data/ubl_21_zatca.xml
 create mode 100644 addons/l10n_sa_edi/demo/demo_company.xml
 create mode 100644 addons/l10n_sa_edi/models/__init__.py
 create mode 100644 addons/l10n_sa_edi/models/account_edi_document.py
 create mode 100644 addons/l10n_sa_edi/models/account_edi_format.py
 create mode 100644 addons/l10n_sa_edi/models/account_edi_xml_ubl_21_zatca.py
 create mode 100644 addons/l10n_sa_edi/models/account_journal.py
 create mode 100644 addons/l10n_sa_edi/models/account_move.py
 create mode 100644 addons/l10n_sa_edi/models/account_tax.py
 create mode 100644 addons/l10n_sa_edi/models/res_company.py
 create mode 100644 addons/l10n_sa_edi/models/res_config_settings.py
 create mode 100644 addons/l10n_sa_edi/models/res_partner.py
 create mode 100644 addons/l10n_sa_edi/security/ir.model.access.csv
 create mode 100644 addons/l10n_sa_edi/static/src/scss/form_view.scss
 create mode 100644 addons/l10n_sa_edi/tests/__init__.py
 create mode 100644 addons/l10n_sa_edi/tests/common.py
 create mode 100644 addons/l10n_sa_edi/tests/compliance/simplified/credit.xml
 create mode 100644 addons/l10n_sa_edi/tests/compliance/simplified/debit.xml
 create mode 100644 addons/l10n_sa_edi/tests/compliance/simplified/invoice.xml
 create mode 100644 addons/l10n_sa_edi/tests/compliance/standard/credit.xml
 create mode 100644 addons/l10n_sa_edi/tests/compliance/standard/debit.xml
 create mode 100644 addons/l10n_sa_edi/tests/compliance/standard/invoice.xml
 create mode 100644 addons/l10n_sa_edi/tests/test_edi_zatca.py
 create mode 100644 addons/l10n_sa_edi/views/account_journal_views.xml
 create mode 100644 addons/l10n_sa_edi/views/account_tax_views.xml
 create mode 100644 addons/l10n_sa_edi/views/report_invoice.xml
 create mode 100644 addons/l10n_sa_edi/views/res_company_views.xml
 create mode 100644 addons/l10n_sa_edi/views/res_config_settings_view.xml
 create mode 100644 addons/l10n_sa_edi/views/res_partner_views.xml
 create mode 100644 addons/l10n_sa_edi/wizard/__init__.py
 create mode 100644 addons/l10n_sa_edi/wizard/account_debit_note.py
 create mode 100644 addons/l10n_sa_edi/wizard/account_move_reversal.py
 create mode 100644 addons/l10n_sa_edi/wizard/l10n_sa_edi_otp_wizard.py
 create mode 100644 addons/l10n_sa_edi/wizard/l10n_sa_edi_otp_wizard.xml

diff --git a/addons/l10n_sa_edi/__init__.py b/addons/l10n_sa_edi/__init__.py
new file mode 100644
index 000000000000..f0111a2faf4a
--- /dev/null
+++ b/addons/l10n_sa_edi/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import models, wizard
diff --git a/addons/l10n_sa_edi/__manifest__.py b/addons/l10n_sa_edi/__manifest__.py
new file mode 100644
index 000000000000..204db76c416b
--- /dev/null
+++ b/addons/l10n_sa_edi/__manifest__.py
@@ -0,0 +1,44 @@
+# -*- 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',
+        ]
+    }
+}
diff --git a/addons/l10n_sa_edi/data/account_edi_format.xml b/addons/l10n_sa_edi/data/account_edi_format.xml
new file mode 100644
index 000000000000..d340e6ccc0d8
--- /dev/null
+++ b/addons/l10n_sa_edi/data/account_edi_format.xml
@@ -0,0 +1,11 @@
+<?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>
diff --git a/addons/l10n_sa_edi/data/pre-hash_invoice.xsl b/addons/l10n_sa_edi/data/pre-hash_invoice.xsl
new file mode 100644
index 000000000000..6c69d566615d
--- /dev/null
+++ b/addons/l10n_sa_edi/data/pre-hash_invoice.xsl
@@ -0,0 +1,19 @@
+<?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>
diff --git a/addons/l10n_sa_edi/data/res_country_data.xml b/addons/l10n_sa_edi/data/res_country_data.xml
new file mode 100644
index 000000000000..3e0b47a6dbf4
--- /dev/null
+++ b/addons/l10n_sa_edi/data/res_country_data.xml
@@ -0,0 +1,38 @@
+<?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>
diff --git a/addons/l10n_sa_edi/data/ubl_21_zatca.xml b/addons/l10n_sa_edi/data/ubl_21_zatca.xml
new file mode 100644
index 000000000000..2ab7ef4076b4
--- /dev/null
+++ b/addons/l10n_sa_edi/data/ubl_21_zatca.xml
@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+
+        <template id="ubl_21_PaymentMeansType_zatca" inherit_id="account_edi_ubl_cii.ubl_20_PaymentMeansType" primary="True">
+            <xpath expr="//*[local-name()='InstructionID']" position="after">
+                <cbc:InstructionNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+                                     xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+                                     t-out="vals['adjustment_reason']"/>
+            </xpath>
+        </template>
+
+        <template id="export_sa_zatca_ubl_extensions">
+            <ext:UBLExtensions xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+                               xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+                               xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2">
+                <ext:UBLExtension>
+                    <ext:ExtensionURI>urn:oasis:names:specification:ubl:dsig:enveloped:xades</ext:ExtensionURI>
+                    <ext:ExtensionContent>
+                        <sig:UBLDocumentSignatures
+                                xmlns:sac="urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2"
+                                xmlns:sbc="urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2"
+                                xmlns:sig="urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2">
+                            <sac:SignatureInformation>
+                                <cbc:ID>urn:oasis:names:specification:ubl:signature:1</cbc:ID>
+                                <sbc:ReferencedSignatureID>urn:oasis:names:specification:ubl:signature:Invoice
+                                </sbc:ReferencedSignatureID>
+                                <ds:Signature Id="signature" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                                    <ds:SignedInfo>
+                                        <ds:CanonicalizationMethod
+                                                Algorithm="http://www.w3.org/2006/12/xml-c14n11"/>
+                                        <ds:SignatureMethod
+                                                Algorithm="http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"/>
+                                        <ds:Reference Id="invoiceSignedData" URI="">
+                                            <ds:Transforms>
+                                                <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
+                                                    <ds:XPath>not(//ancestor-or-self::ext:UBLExtensions)</ds:XPath>
+                                                </ds:Transform>
+                                                <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
+                                                    <ds:XPath>not(//ancestor-or-self::cac:Signature)</ds:XPath>
+                                                </ds:Transform>
+                                                <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
+                                                    <ds:XPath>
+                                                        not(//ancestor-or-self::cac:AdditionalDocumentReference[cbc:ID='QR'])
+                                                    </ds:XPath>
+                                                </ds:Transform>
+                                                <ds:Transform Algorithm="http://www.w3.org/2006/12/xml-c14n11"/>
+                                            </ds:Transforms>
+                                            <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                                            <!-- b64encoded SHA256 digest of document -->
+                                            <ds:DigestValue/>
+                                        </ds:Reference>
+                                        <ds:Reference Type="http://www.w3.org/2000/09/xmldsig#SignatureProperties"
+                                                      URI="#xadesSignedProperties">
+                                            <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                                            <ds:DigestValue/>
+                                        </ds:Reference>
+                                    </ds:SignedInfo>
+                                    <ds:SignatureValue/>
+                                    <ds:KeyInfo>
+                                        <ds:X509Data>
+                                            <ds:X509Certificate/>
+                                        </ds:X509Data>
+                                    </ds:KeyInfo>
+                                    <ds:Object>
+                                        <xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"
+                                                                    Target="signature">
+                                            <xades:SignedProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"
+                                                                    Id="xadesSignedProperties">
+                                                <xades:SignedSignatureProperties>
+                                                    <xades:SigningTime/>
+                                                    <xades:SigningCertificate>
+                                                        <xades:Cert>
+                                                            <xades:CertDigest>
+                                                                <ds:DigestMethod
+                                                                        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+                                                                        Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                                                                <ds:DigestValue
+                                                                        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>
+                                                            </xades:CertDigest>
+                                                            <xades:IssuerSerial>
+                                                                <ds:X509IssuerName
+                                                                        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>
+                                                                <ds:X509SerialNumber
+                                                                        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>
+                                                            </xades:IssuerSerial>
+                                                        </xades:Cert>
+                                                    </xades:SigningCertificate>
+                                                </xades:SignedSignatureProperties>
+                                            </xades:SignedProperties>
+                                        </xades:QualifyingProperties>
+                                    </ds:Object>
+                                </ds:Signature>
+                            </sac:SignatureInformation>
+                        </sig:UBLDocumentSignatures>
+                    </ext:ExtensionContent>
+                </ext:UBLExtension>
+            </ext:UBLExtensions>
+        </template>
+
+        <template id="export_sa_zatca_ubl_signed_properties">
+            <xades:SignedProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Id="xadesSignedProperties">
+                <xades:SignedSignatureProperties>
+                    <xades:SigningTime t-out="signing_time"/>
+                    <xades:SigningCertificate>
+                        <xades:Cert>
+                            <xades:CertDigest>
+                                <ds:DigestMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+                                                 Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                                <ds:DigestValue xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+                                                t-out="public_key_hashing"/>
+                            </xades:CertDigest>
+                            <xades:IssuerSerial>
+                                <ds:X509IssuerName xmlns:ds="http://www.w3.org/2000/09/xmldsig#" t-out="issuer_name"/>
+                                <ds:X509SerialNumber xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+                                                     t-out="serial_number"/>
+                            </xades:IssuerSerial>
+                        </xades:Cert>
+                    </xades:SigningCertificate>
+                </xades:SignedSignatureProperties>
+            </xades:SignedProperties>
+        </template>
+
+        <template id="ubl_21_InvoiceLineType_zatca" inherit_id="account_edi_ubl_cii.ubl_20_InvoiceLineType" primary="True">
+            <!-- Remove SellersItemIdentification if None -->
+            <xpath expr="//*[local-name()='SellersItemIdentification']" position="replace">
+                <cac:SellersItemIdentification t-if="item_vals['sellers_item_identification_vals']['id']"
+                                               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">
+                    <cbc:ID t-out="item_vals['sellers_item_identification_vals']['id']"/>
+                </cac:SellersItemIdentification>
+            </xpath>
+            <xpath expr="//*[local-name()='CreditedQuantity']" position="replace"/>
+            <xpath expr="//*[local-name()='InvoicedQuantity']" position="replace">
+                <cbc:InvoicedQuantity
+                        xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+                        xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+                        t-att="vals.get('invoiced_quantity_attrs', {})"
+                        t-out="vals['invoiced_quantity']"/>
+            </xpath>
+            <xpath expr="//*[local-name()='LineExtensionAmount']" position="after">
+                <cac:DocumentReference
+                        t-if="vals.get('prepayment_vals', {})"
+                        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">
+                    <cbc:ID t-out="vals['prepayment_vals']['prepayment_id']"/>
+                    <cbc:IssueDate t-esc="vals['prepayment_vals']['issue_date'].strftime('%Y-%m-%d')"/>
+                    <cbc:IssueTime t-esc="vals['prepayment_vals']['issue_date'].strftime('%H:%M:%S')"/>
+                    <cbc:DocumentTypeCode t-out="vals['prepayment_vals']['document_type_code']"/>
+                </cac:DocumentReference>
+            </xpath>
+        </template>
+
+        <template id="ubl_21_TaxTotalType_zatca" inherit_id="account_edi_ubl_cii.ubl_20_TaxTotalType" primary="True">
+            <xpath expr="//*[local-name()='TaxAmount']" position="after">
+                <cbc:RoundingAmount xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+                                    xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+                                    t-att-currencyID="vals['currency'].name"
+                                    t-esc="format_float(vals.get('total_amount_sa'), vals['currency_dp'])"/>
+            </xpath>
+        </template>
+
+        <template id="ubl_21_PartyType_zatca" inherit_id="account_edi_ubl_cii.ubl_20_PartyType" primary="True">
+            <xpath expr="//*[local-name()='PartyIdentification']" position="replace">
+                <cac:PartyIdentification t-if="party_vals.get('id')"
+                                         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">
+                    <cbc:ID t-att="party_vals.get('id_attrs', {})" t-out="party_vals['id']"/>
+                </cac:PartyIdentification>
+            </xpath>
+        </template>
+
+        <template id="ubl_21_AddressType_zatca" inherit_id="account_edi_ubl_cii.ubl_20_AddressType" primary="True">
+            <!-- AdditionalStreetName causes the Validation SDK to crash, so it has to be removed -->
+            <xpath expr="//*[local-name()='AdditionalStreetName']" position="replace"/>
+            <xpath expr="//*[local-name()='StreetName']" position="after">
+                <t 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">
+                    <!--  Add Building number in compliance with rules KSA-17 (seller)/ KSA-18 (customer)  -->
+                    <cbc:BuildingNumber t-out="vals.get('building_number')"/>
+                    <!--  Add Plot identification in compliance with rules KSA-23 (seller)/ KSA-19 (customer)  -->
+                    <cbc:PlotIdentification t-out="vals.get('plot_identification')"/>
+                    <!--  Add Neighborhood in compliance with rules KSA-3 (seller)/ KSA-4 (customer)  -->
+                    <cbc:CitySubdivisionName t-out="vals.get('neighborhood')"/>
+                </t>
+            </xpath>
+        </template>
+
+        <template id="ubl_21_InvoiceType_zatca" inherit_id="account_edi_ubl_cii.ubl_21_InvoiceType" primary="True">
+
+            <!--    For ZATCA, we do not use CreditNoteTypeCode or DebitNoteTypeCode tags. We always use InvoiceTypeCode. -->
+            <xpath expr="//*[local-name()='CreditNoteTypeCode']" position="replace"/>
+            <xpath expr="//*[local-name()='InvoiceTypeCode']" position="replace">
+                <cbc:InvoiceTypeCode 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"
+                                     t-att="vals['invoice_type_code_attrs']"
+                                     t-out="vals['invoice_type_code']"/>
+            </xpath>
+
+            <!--    For ZATCA, we do not use CreditNoteLine or DebitNoteLine tags. We always use InvoiceLine. -->
+            <xpath expr="//*[local-name()='CreditNoteLine']" position="replace"/>
+            <xpath expr="//*[local-name()='InvoiceLine']" position="replace">
+                <cac:InvoiceLine 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">
+                    <t t-call="{{InvoiceLineType_template}}">
+                        <t t-set="vals" t-value="foreach_vals"/>
+                    </t>
+                </cac:InvoiceLine>
+            </xpath>
+
+            <!-- Remove Order Reference if None -->
+            <xpath expr="//*[local-name()='OrderReference']" position="replace">
+                <cac:OrderReference t-if="vals.get('order_reference')"
+                                    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">
+                    <cbc:ID t-out="vals['order_reference']"/>
+                </cac:OrderReference>
+            </xpath>
+
+            <!-- Add Invoice UUID in compliance with rule BR-KSA-03 -->
+            <xpath expr="//*[local-name()='ID']" position="after">
+                <cbc:UUID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+                          xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+                          t-esc="invoice.l10n_sa_uuid"/>
+            </xpath>
+
+            <!-- Add Invoice Issue Time in compliance with rule KSA-25 -->
+            <xpath expr="//*[local-name()='IssueDate']" position="replace">
+                <t xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+                   xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
+                    <cbc:IssueDate t-esc="vals['issue_date'].strftime('%Y-%m-%d')"/>
+                    <cbc:IssueTime t-esc="vals['issue_date'].strftime('%H:%M:%S')"/>
+                </t>
+            </xpath>
+
+            <!-- Add Tax Currency Code in compliance with rules BR-KSA-EN16931-02 and BR-KSA-68 -->
+            <xpath expr="//*[local-name()='DocumentCurrencyCode']" position="after">
+                <cbc:TaxCurrencyCode xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+                                     xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+                                     t-esc="invoice.company_currency_id.name"/>
+            </xpath>
+
+            <!-- Add Previous Invoice Hash & Invoice Counter Value -->
+            <xpath expr="//*[local-name()='BillingReference']" position="after">
+                <t 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">
+                    <!-- Add QR Code in compliance with rule BR-KSA-27 -->
+                    <cac:AdditionalDocumentReference t-if="invoice._l10n_sa_is_simplified()">
+                        <cbc:ID>QR</cbc:ID>
+                        <cac:Attachment>
+                            <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">N/A</cbc:EmbeddedDocumentBinaryObject>
+                        </cac:Attachment>
+                    </cac:AdditionalDocumentReference>
+                    <!-- Add Previous Invoice Hash in compliance with rule BR-KSA-61 -->
+                    <cac:AdditionalDocumentReference>
+                        <cbc:ID>PIH</cbc:ID>
+                        <cac:Attachment>
+                            <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain"
+                                                              t-out="vals['previous_invoice_hash']"/>
+                        </cac:Attachment>
+                    </cac:AdditionalDocumentReference>
+                    <!-- Add Invoice Counter Value in compliance with rules BR-KSA-33 and BR-KSA-34 -->
+                    <cac:AdditionalDocumentReference>
+                        <cbc:ID>ICV</cbc:ID>
+                        <cbc:UUID t-out="invoice.l10n_sa_chain_index"/>
+                    </cac:AdditionalDocumentReference>
+                    <!-- Add Signature references in compliance with rules BR-KSA-29 and BR-KSA-30 -->
+                    <cac:Signature t-if="invoice._l10n_sa_is_simplified()">
+                        <cbc:ID>urn:oasis:names:specification:ubl:signature:Invoice</cbc:ID>
+                        <cbc:SignatureMethod>urn:oasis:names:specification:ubl:dsig:enveloped:xades</cbc:SignatureMethod>
+                    </cac:Signature>
+                </t>
+            </xpath>
+        </template>
+
+    </data>
+</odoo>
diff --git a/addons/l10n_sa_edi/demo/demo_company.xml b/addons/l10n_sa_edi/demo/demo_company.xml
new file mode 100644
index 000000000000..cf8cb4c14c01
--- /dev/null
+++ b/addons/l10n_sa_edi/demo/demo_company.xml
@@ -0,0 +1,52 @@
+<?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>
diff --git a/addons/l10n_sa_edi/models/__init__.py b/addons/l10n_sa_edi/models/__init__.py
new file mode 100644
index 000000000000..47185ed0ba3a
--- /dev/null
+++ b/addons/l10n_sa_edi/models/__init__.py
@@ -0,0 +1,9 @@
+# -*- 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
diff --git a/addons/l10n_sa_edi/models/account_edi_document.py b/addons/l10n_sa_edi/models/account_edi_document.py
new file mode 100644
index 000000000000..9858f394861c
--- /dev/null
+++ b/addons/l10n_sa_edi/models/account_edi_document.py
@@ -0,0 +1,28 @@
+# -*- 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
diff --git a/addons/l10n_sa_edi/models/account_edi_format.py b/addons/l10n_sa_edi/models/account_edi_format.py
new file mode 100644
index 000000000000..4b666e016a38
--- /dev/null
+++ b/addons/l10n_sa_edi/models/account_edi_format.py
@@ -0,0 +1,471 @@
+import json
+from hashlib import sha256
+from base64 import b64decode, b64encode
+from lxml import etree
+from datetime import date, datetime
+from odoo import models, fields, _, api
+from odoo.exceptions import UserError
+from cryptography.hazmat.primitives.serialization import load_pem_private_key
+from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.backends import default_backend
+from cryptography.x509 import load_der_x509_certificate
+
+
+class AccountEdiFormat(models.Model):
+    _inherit = 'account.edi.format'
+
+    """
+        Once the journal has been successfully onboarded, we can clear/report invoices through the ZATCA API:
+            A) STANDARD Invoice:
+                Make a call to the Clearance API '/invoices/clearance/single'.
+                This will validate the invoice, sign it and apply a QR code then return the result.
+            B) SIMPLIFIED Invoice:
+                Make a call to the Reporting API '/invoices/reporting/single'.
+                This will validate the invoice then return the result.
+        The X509 Certificate and password from the PCSID API need to be provided in the request headers.
+    """
+
+    # ====== Helper Functions =======
+
+    def _l10n_sa_get_zatca_datetime(self, timestamp):
+        return fields.Datetime.context_timestamp(self.with_context(tz='Asia/Riyadh'), timestamp)
+
+    def _l10n_sa_xml_node_content(self, root, xpath, namespaces=None):
+        namespaces = namespaces or self.env['account.edi.xml.ubl_21.zatca']._l10n_sa_get_namespaces()
+        return etree.tostring(root.xpath(xpath, namespaces=namespaces)[0], with_tail=False,
+                              encoding='utf-8', method='xml')
+
+    # ====== Xades Signing =======
+
+    @api.model
+    def _l10n_sa_get_digital_signature(self, company_id, invoice_hash):
+        """
+            Generate an ECDSA SHA256 digital signature for the XML eInvoice
+        """
+        decoded_hash = b64decode(invoice_hash).decode()
+        private_key = load_pem_private_key(company_id.sudo().l10n_sa_private_key, password=None, backend=default_backend())
+        signature = private_key.sign(decoded_hash.encode(), ECDSA(hashes.SHA256()))
+        return b64encode(signature)
+
+    def _l10n_sa_calculate_signed_properties_hash(self, issuer_name, serial_number, signing_time, public_key):
+        """
+            Calculate the SHA256 value of the SignedProperties XML node. The algorithm used by ZATCA expects the indentation
+            of the nodes to start with 40 spaces, except for the root SignedProperties node.
+        """
+        signed_properties = etree.fromstring(self.env['ir.qweb']._render('l10n_sa_edi.export_sa_zatca_ubl_signed_properties', {
+            'issuer_name': issuer_name,
+            'serial_number': serial_number,
+            'signing_time': signing_time,
+            'public_key_hashing': public_key,
+        }))
+        etree.indent(signed_properties, space='    ')
+        signed_properties_split = etree.tostring(signed_properties).decode().split('\n')
+        signed_properties_final = ""
+        for index, line in enumerate(signed_properties_split):
+            if index == 0:
+                signed_properties_final += line
+            else:
+                signed_properties_final += (' ' * 36) + line
+            if index != len(signed_properties_final) - 1:
+                signed_properties_final += '\n'
+        signed_properties_final = etree.tostring(etree.fromstring(signed_properties_final))
+        return b64encode(sha256(signed_properties_final).hexdigest().encode()).decode()
+
+    def _l10n_sa_sign_xml(self, xml_content, certificate_str, signature):
+        """
+            Function that signs XML content of a UBL document with a provided B64 encoded X509 certificate
+        """
+        root = etree.fromstring(xml_content)
+        etree.indent(root, space='    ')
+
+        def _set_content(xpath, content):
+            node = root.xpath(xpath)[0]
+            node.text = content
+
+        b64_decoded_cert = b64decode(certificate_str)
+        x509_certificate = load_der_x509_certificate(b64decode(b64_decoded_cert.decode()), default_backend())
+
+        issuer_name = ', '.join([s.rfc4514_string() for s in x509_certificate.issuer.rdns[::-1]])
+        serial_number = str(x509_certificate.serial_number)
+        signing_time = self._l10n_sa_get_zatca_datetime(datetime.now()).strftime('%Y-%m-%dT%H:%M:%SZ')
+        public_key_hashing = b64encode(sha256(b64_decoded_cert).hexdigest().encode()).decode()
+
+        signed_properties_hash = self._l10n_sa_calculate_signed_properties_hash(issuer_name, serial_number,
+                                                                                signing_time, public_key_hashing)
+
+        _set_content("//*[local-name()='X509IssuerName']", issuer_name)
+        _set_content("//*[local-name()='X509SerialNumber']", serial_number)
+        _set_content("//*[local-name()='SignedSignatureProperties']/*[local-name()='SigningTime']", signing_time)
+        _set_content("//*[local-name()='SignedSignatureProperties']//*[local-name()='DigestValue']", public_key_hashing)
+
+        prehash_content = etree.tostring(root)
+        invoice_hash = self.env['account.edi.xml.ubl_21.zatca']._l10n_sa_generate_invoice_xml_hash(prehash_content,
+                                                                                                   'digest')
+
+        _set_content("//*[local-name()='SignatureValue']", signature)
+        _set_content("//*[local-name()='X509Certificate']", b64_decoded_cert.decode())
+        _set_content("//*[local-name()='SignatureInformation']//*[local-name()='DigestValue']", invoice_hash)
+        _set_content("//*[@URI='#xadesSignedProperties']/*[local-name()='DigestValue']", signed_properties_hash)
+
+        return etree.tostring(root, with_tail=False)
+
+    def _l10n_sa_assert_clearance_status(self, invoice, clearance_data):
+        """
+            Assert Clearance status. To be overridden in case there are any other cases to be accounted for
+        """
+        mode = 'reporting' if invoice._l10n_sa_is_simplified() else 'clearance'
+        if mode == 'clearance' and clearance_data.get('clearanceStatus', '') != 'CLEARED':
+            return {'error': _("Invoice could not be cleared: \r\n %s ") % clearance_data, 'blocking_level': 'error'}
+        elif mode == 'reporting' and clearance_data.get('reportingStatus', '') != 'REPORTED':
+            return {'error': _("Invoice could not be reported: \r\n %s ") % clearance_data, 'blocking_level': 'error'}
+        return clearance_data
+
+    # ====== UBL Document Rendering & Submission =======
+
+    def _l10n_sa_postprocess_zatca_template(self, xml_content):
+        """
+            Post-process xml content generated according to the ZATCA UBL specifications. Specifically, this entails:
+                -   Force the xmlns:ext namespace on the root element (Invoice). This is required, since, by default
+                    the generated UBL file does not have any ext namespaced element, so the namespace is removed
+                    since it is unused.
+        """
+
+        # Append UBLExtensions to the XML content
+        ubl_extensions = etree.fromstring(self.env['ir.qweb']._render('l10n_sa_edi.export_sa_zatca_ubl_extensions'))
+        root = etree.fromstring(xml_content)
+        root.insert(0, ubl_extensions)
+
+        # Force xmlns:ext namespace on UBl file
+        ns_map = {'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2'}
+        etree.cleanup_namespaces(root, top_nsmap=ns_map, keep_ns_prefixes=['ext'])
+
+        return etree.tostring(root, with_tail=False).decode()
+
+    def _l10n_sa_generate_zatca_template(self, invoice):
+        """
+            Render the ZATCA UBL file
+        """
+        xml_content, errors = self.env['account.edi.xml.ubl_21.zatca']._export_invoice(invoice)
+        if errors:
+            return {
+                'error': _("Could not generate Invoice UBL content: %s") % ", \n".join(errors),
+                'blocking_level': 'error'
+            }
+        return self._l10n_sa_postprocess_zatca_template(xml_content)
+
+    def _l10n_sa_submit_einvoice(self, invoice, signed_xml, PCSID_data):
+        """
+            Submit a generated Invoice UBL file by making calls to the following APIs:
+                -   A. Clearance API: Submit a standard Invoice to ZATCA for validation, returns signed UBL
+                -   B. Reporting API: Submit a simplified Invoice to ZATCA for validation
+        """
+        clearance_data = invoice.journal_id._l10n_sa_api_clearance(invoice, signed_xml.decode(), PCSID_data)
+        if clearance_data.get('json_errors'):
+            errors = [json.loads(j).get('validationResults', {}) for j in clearance_data['json_errors']]
+            error_msg = ''
+            is_warning = True
+            for error in errors:
+                validation_results = error.get('validationResults', {})
+                for err in validation_results.get('warningMessages', []):
+                    error_msg += '\n - %s | %s' % (err['code'], err['message'])
+                for err in validation_results.get('errorMessages', []):
+                    is_warning = False
+                    error_msg += '\n - %s | %s' % (err['code'], err['message'])
+            return {
+                'error': error_msg,
+                'rejected': not is_warning,
+                'response': signed_xml.decode(),
+                'blocking_level': 'warning' if is_warning else 'error'
+            }
+        if not clearance_data.get('error'):
+            return self._l10n_sa_assert_clearance_status(invoice, clearance_data)
+        return clearance_data
+
+    def _l10n_sa_postprocess_einvoice_submission(self, invoice, signed_xml, clearance_data):
+        """
+            Once an invoice has been successfully submitted, it is returned as a Cleared invoice, on which data
+            from ZATCA was applied. To be overridden to account for other cases, such as Reporting.
+        """
+        if invoice._l10n_sa_is_simplified():
+            # if invoice is B2C, it is a SIMPLIFIED invoice, and thus it is only reported and returns
+            # no signed invoice. In this case, we just return the original content
+            return signed_xml.decode()
+        return b64decode(clearance_data['clearedInvoice']).decode()
+
+    def _l10n_sa_apply_qr_code(self, invoice, xml_content):
+        """
+            Apply QR code on Invoice UBL content
+        """
+        root = etree.fromstring(xml_content)
+        qr_code = invoice.l10n_sa_qr_code_str
+        qr_node = root.xpath('//*[local-name()="ID"][text()="QR"]/following-sibling::*/*')[0]
+        qr_node.text = qr_code
+        return etree.tostring(root, with_tail=False)
+
+    def _l10n_sa_get_signed_xml(self, invoice, unsigned_xml, x509_cert):
+        """
+            Helper method to sign the provided XML, apply the QR code in the case if Simplified invoices (B2C), then
+            return the signed XML
+        """
+        signed_xml = self._l10n_sa_sign_xml(unsigned_xml, x509_cert, invoice.l10n_sa_invoice_signature)
+        if invoice._l10n_sa_is_simplified():
+            return self._l10n_sa_apply_qr_code(invoice, signed_xml)
+        return signed_xml
+
+    def _l10n_sa_export_zatca_invoice(self, invoice, xml_content=None):
+        """
+            Generate a ZATCA compliant UBL file, make API calls to authenticate, sign and include QR Code and
+            Cryptographic Stamp, then create an attachment with the final contents of the UBL file
+        """
+        self.ensure_one()
+
+        # Prepare UBL invoice values and render XML file
+        unsigned_xml = xml_content or self._l10n_sa_generate_zatca_template(invoice)
+
+        # Load PCISD data and X509 certificate
+        try:
+            PCSID_data = invoice.journal_id._l10n_sa_api_get_pcsid()
+        except UserError as e:
+            return {'error': _("Could not generate PCSID values: \n") + e.args[0], 'blocking_level': 'error'}
+        x509_cert = PCSID_data['binarySecurityToken']
+
+        # Apply Signature/QR code on the generated XML document
+        try:
+            signed_xml = self._l10n_sa_get_signed_xml(invoice, unsigned_xml, x509_cert)
+        except UserError as e:
+            return {
+                'error': _("Could not generate signed XML values: \n") + e.args[0],
+                'blocking_level': 'error',
+                'response': unsigned_xml
+            }
+
+        # Once the XML content has been generated and signed, we submit it to ZATCA
+        return self._l10n_sa_submit_einvoice(invoice, signed_xml, PCSID_data), signed_xml
+
+    def _l10n_sa_check_partner_missing_info(self, partner_id, fields_to_check):
+        """
+            Helper function to check if ZATCA mandated partner fields are missing for a specified partner record
+        """
+        missing = []
+        for field in fields_to_check:
+            field_value = partner_id[field[0]]
+            if not field_value or (len(field) == 3 and not field[2](partner_id, field_value)):
+                missing.append(field[1])
+        return missing
+
+    def _l10n_sa_check_seller_missing_info(self, invoice):
+        """
+            Helper function to check if ZATCA mandated partner fields are missing for the seller
+        """
+        partner_id = invoice.company_id.partner_id.commercial_partner_id
+        fields_to_check = [
+            ('l10n_sa_edi_building_number', _('Building Number for the Buyer is required on Standard Invoices')),
+            ('street2', _('Neighborhood for the Seller is required on Standard Invoices')),
+            ('l10n_sa_additional_identification_scheme',
+             _('Additional Identification Scheme is required for the Seller, and must be one of CRN, MOM, MLS, SAG or OTH'),
+             lambda p, v: v in ('CRN', 'MOM', 'MLS', 'SAG', 'OTH')
+             ),
+            ('vat',
+             _('VAT is required when Identification Scheme is set to Tax Identification Number'),
+             lambda p, v: p.l10n_sa_additional_identification_scheme != 'TIN'
+             ),
+            ('state_id', _('State / Country subdivision'))
+        ]
+        return self._l10n_sa_check_partner_missing_info(partner_id, fields_to_check)
+
+    def _l10n_sa_check_buyer_missing_info(self, invoice):
+        """
+            Helper function to check if ZATCA mandated partner fields are missing for the buyer
+        """
+        fields_to_check = []
+        if any(tax.l10n_sa_exemption_reason_code in ('VATEX-SA-HEA', 'VATEX-SA-EDU') for tax in
+               invoice.invoice_line_ids.filtered(
+                   lambda line: not line.display_type).tax_ids):
+            fields_to_check += [
+                ('l10n_sa_additional_identification_scheme',
+                 _('Additional Identification Scheme is required for the Buyer if tax exemption reason is either '
+                   'VATEX-SA-HEA or VATEX-SA-EDU, and its value must be NAT'), lambda p, v: v == 'NAT'),
+                ('l10n_sa_additional_identification_number',
+                 _('Additional Identification Number is required for commercial partners'),
+                 lambda p, v: p.l10n_sa_additional_identification_scheme != 'TIN'
+                 ),
+            ]
+        elif invoice.commercial_partner_id.l10n_sa_additional_identification_scheme == 'TIN':
+            fields_to_check += [
+                ('vat', _('VAT is required when Identification Scheme is set to Tax Identification Number'))
+            ]
+        if not invoice._l10n_sa_is_simplified() and invoice.partner_id.country_id.code == 'SA':
+            # If the invoice is a non-foreign, Standard (B2B), the Building Number and Neighborhood are required
+            fields_to_check += [
+                ('l10n_sa_edi_building_number', _('Building Number for the Buyer is required on Standard Invoices')),
+                ('street2', _('Neighborhood for the Buyer is required on Standard Invoices')),
+            ]
+        return self._l10n_sa_check_partner_missing_info(invoice.commercial_partner_id, fields_to_check)
+
+    def _l10n_sa_post_zatca_edi(self, invoice):  # no batch ensure that there is only one invoice
+        """
+            Post invoice to ZATCA and return a dict of invoices and their success/attachment
+        """
+
+        # Chain integrity check: chain head must have been REALLY posted, and did not time out
+        # When a submission times out, we reset the chain index of the invoice to False, so it has to be submitted again
+        # According to ZATCA, if we end up submitting the same invoice more than once, they will directly reach out
+        # to the taxpayer for clarifications
+        chain_head = invoice.journal_id._l10n_sa_get_last_posted_invoice()
+        if chain_head and chain_head != invoice and not chain_head._l10n_sa_is_in_chain():
+            return {invoice: {
+                'error': f"ZATCA: Cannot post invoice while chain head ({chain_head.name}) has not been posted",
+                'blocking_level': 'error',
+                'response': None,
+            }}
+        xml_content = None
+        if not invoice.l10n_sa_chain_index:
+            # If the Invoice doesn't have a chain index, it means it either has not been submitted before,
+            # or it was submitted and rejected. Either way, we need to assign it a new Chain Index and regenerate
+            # the data that depends on it before submitting (UUID, XML content, signature)
+            invoice.l10n_sa_chain_index = invoice.journal_id._l10n_sa_edi_get_next_chain_index()
+            xml_content = invoice._l10n_sa_generate_unsigned_data()
+
+        # Generate Invoice name for attachment
+        attachment_name = self.env['account.edi.xml.ubl_21.zatca']._export_invoice_filename(invoice)
+
+        # Generate XML, sign it, then submit it to ZATCA
+        response_data, submitted_xml = self._l10n_sa_export_zatca_invoice(invoice, xml_content)
+
+        # Check for submission errors
+        if response_data.get('error'):
+
+            # If the request was rejected, we save the signed xml content as an attachment
+            if response_data.get('rejected'):
+                invoice._l10n_sa_log_results(submitted_xml, response_data, error=True)
+
+            # If the request returned an exception (Timeout, ValueError... etc.) it means we're not sure if the
+            # invoice was successfully cleared/reported, and thus we keep the Index Chain.
+            # Else, we recalculate the submission Index (ICV), UUID, XML content and Signature
+            if not response_data.get('excepted'):
+                invoice.l10n_sa_chain_index = False
+
+            return {
+                invoice: {
+                    **response_data,
+                    'response': submitted_xml
+                }
+            }
+
+        # Once submission is done with no errors, check submission status
+        cleared_xml = self._l10n_sa_postprocess_einvoice_submission(invoice, submitted_xml, response_data)
+
+        # Save the submitted/returned invoice XML content once the submission has been completed successfully
+        invoice._l10n_sa_log_results(cleared_xml.encode(), response_data)
+        return {
+            invoice: {
+                'success': True,
+                'response': cleared_xml,
+                'message': '',
+                'attachment': self.env['ir.attachment'].create({
+                    'name': attachment_name,
+                    'raw': cleared_xml.encode(),
+                    'res_model': 'account.move',
+                    'res_id': invoice.id,
+                    'mimetype': 'application/xml'
+                })
+            }
+        }
+
+    # ====== EDI Format Overrides =======
+
+    def _is_required_for_invoice(self, invoice):
+        """
+            Override to add ZATCA edi checks on required invoices
+        """
+        self.ensure_one()
+        if self.code != 'sa_zatca':
+            return super()._is_required_for_invoice(invoice)
+
+        return invoice.is_sale_document() and invoice.country_code == 'SA'
+
+    def _check_move_configuration(self, invoice):
+        """
+            Override to add ZATCA compliance checks on the Invoice
+        """
+
+        def _set_missing_partner_fields(missing_fields, name):
+            return _("- Please, set the following fields on the %s: %s") % (name, ', '.join(missing_fields))
+
+        journal = invoice.journal_id
+        company = invoice.company_id
+
+        errors = super()._check_move_configuration(invoice)
+        if self.code != 'sa_zatca' or company.country_id.code != 'SA':
+            return errors
+
+        if invoice.commercial_partner_id == invoice.company_id.partner_id.commercial_partner_id:
+            errors.append(_("- You cannot post invoices where the Seller is the Buyer"))
+
+        if not all(line.tax_ids for line in invoice.invoice_line_ids.filtered(lambda line: not line.display_type)):
+            errors.append(_("- Invoice lines should have at least one Tax applied."))
+
+        if not journal._l10n_sa_ready_to_submit_einvoices():
+            errors.append(
+                _("- Finish the Onboarding procees for journal %s by requesting the CSIDs and completing the checks.") % journal.name)
+
+        if not company._l10n_sa_check_organization_unit():
+            errors.append(
+                _("- The company VAT identification must contain 15 digits, with the first and last digits being '3' as per the BR-KSA-39 and BR-KSA-40 of ZATCA KSA business rule."))
+        if not company.sudo().l10n_sa_private_key:
+            errors.append(
+                _("- No Private Key was generated for company %s. A Private Key is mandatory in order to generate Certificate Signing Requests (CSR).") % company.name)
+        if not journal.l10n_sa_serial_number:
+            errors.append(
+                _("- No Serial Number was assigned for journal %s. A Serial Number is mandatory in order to generate Certificate Signing Requests (CSR).") % journal.name)
+
+        supplier_missing_info = self._l10n_sa_check_seller_missing_info(invoice)
+        customer_missing_info = self._l10n_sa_check_buyer_missing_info(invoice)
+
+        if supplier_missing_info:
+            errors.append(_set_missing_partner_fields(supplier_missing_info, _("Supplier")))
+        if customer_missing_info:
+            errors.append(_set_missing_partner_fields(customer_missing_info, _("Customer")))
+        if invoice.invoice_date > date.today():
+            errors.append(_("- Please, make sure the invoice date is set to either the same as or before Today."))
+        if invoice.move_type in ('in_refund', 'out_refund') and not invoice._l10n_sa_check_refund_reason():
+            errors.append(
+                _("- Please, make sure both the Reversed Entry and the Reversal Reason are specified when confirming a Credit/Debit note"))
+        return errors
+
+    def _needs_web_services(self):
+        """
+            Override to add a check on edi document format code
+        """
+        self.ensure_one()
+        return self.code == 'sa_zatca' or super()._needs_web_services()
+
+    def _is_compatible_with_journal(self, journal):
+        """
+            Override to add a check on journal type & country code (SA)
+        """
+        self.ensure_one()
+        if self.code != 'sa_zatca':
+            return super()._is_compatible_with_journal(journal)
+        return journal.type == 'sale' and journal.country_code == 'SA'
+
+    def _l10n_sa_get_invoice_content_edi(self, invoice):
+        """
+            Return contents of the submitted UBL file or generate it if the invoice has not been submitted yet
+        """
+        doc = invoice.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'sa_zatca' and d.state == 'sent')
+        if doc is not None and doc.attachment_id.datas:
+            return {invoice: {'xml_file': doc.attachment_id.datas.decode()}}
+        return {invoice: {'xml_file': self._l10n_sa_generate_zatca_template(invoice)}}
+
+    def _get_move_applicability(self, move):
+        # EXTENDS account_edi
+        self.ensure_one()
+        if self.code != 'sa_zatca' or move.country_code != 'SA' or move.move_type not in ('out_invoice', 'out_refund'):
+            return super()._get_move_applicability(move)
+
+        return {
+            'post': self._l10n_sa_post_zatca_edi,
+            'edi_content': self._l10n_sa_get_invoice_content_edi,
+        }
diff --git a/addons/l10n_sa_edi/models/account_edi_xml_ubl_21_zatca.py b/addons/l10n_sa_edi/models/account_edi_xml_ubl_21_zatca.py
new file mode 100644
index 000000000000..f435876d907d
--- /dev/null
+++ b/addons/l10n_sa_edi/models/account_edi_xml_ubl_21_zatca.py
@@ -0,0 +1,410 @@
+# -*- coding: utf-8 -*-
+from hashlib import sha256
+from base64 import b64encode
+from lxml import etree
+from odoo import models, fields
+from odoo.modules.module import get_module_resource
+import re
+
+TAX_EXEMPTION_CODES = ['VATEX-SA-29', 'VATEX-SA-29-7', 'VATEX-SA-30']
+TAX_ZERO_RATE_CODES = ['VATEX-SA-32', 'VATEX-SA-33', 'VATEX-SA-34-1', 'VATEX-SA-34-2', 'VATEX-SA-34-3', 'VATEX-SA-34-4',
+                       'VATEX-SA-34-5', 'VATEX-SA-35', 'VATEX-SA-36', 'VATEX-SA-EDU', 'VATEX-SA-HEA']
+
+PAYMENT_MEANS_CODE = {
+    'bank': 42,
+    'card': 48,
+    'cash': 10,
+    'transfer': 30,
+    'unknown': 1
+}
+
+
+class AccountEdiXmlUBL21Zatca(models.AbstractModel):
+    _name = "account.edi.xml.ubl_21.zatca"
+    _inherit = 'account.edi.xml.ubl_21'
+    _description = "UBL 2.1 (ZATCA)"
+
+    def _l10n_sa_get_namespaces(self):
+        """
+            Namespaces used in the final UBL declaration, required to canonalize the finalized XML document of the Invoice
+        """
+        return {
+            'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
+            'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
+            'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2',
+            'sig': 'urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2',
+            'sac': 'urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2',
+            'sbc': 'urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2',
+            'ds': 'http://www.w3.org/2000/09/xmldsig#',
+            'xades': 'http://uri.etsi.org/01903/v1.3.2#'
+        }
+
+    def _l10n_sa_generate_invoice_xml_sha(self, xml_content):
+        """
+            Transform, canonicalize then hash the invoice xml content using the SHA256 algorithm,
+            then return the hashed content
+        """
+
+        def _canonicalize_xml(content):
+            """
+                Canonicalize XML content using the c14n method. The specs mention using the c14n11 canonicalization,
+                which is simply calling etree.tostring and setting the method argument to 'c14n'. There are minor
+                differences between c14n11 and c14n canonicalization algorithms, but for the purpose of ZATCA signing,
+                c14n is enough
+            """
+            return etree.tostring(content, method="c14n", exclusive=False, with_comments=False,
+                                  inclusive_ns_prefixes=self._l10n_sa_get_namespaces())
+
+        def _transform_and_canonicalize_xml(content):
+            """ Transform XML content to remove certain elements and signatures using an XSL template """
+            invoice_xsl = etree.parse(get_module_resource('l10n_sa_edi', 'data', 'pre-hash_invoice.xsl'))
+            transform = etree.XSLT(invoice_xsl)
+            return _canonicalize_xml(transform(content))
+
+        root = etree.fromstring(xml_content)
+        # Transform & canonicalize the XML content
+        transformed_xml = _transform_and_canonicalize_xml(root)
+        # Get the SHA256 hashed value of the XML content
+        return sha256(transformed_xml)
+
+    def _l10n_sa_generate_invoice_xml_hash(self, xml_content, mode='hexdigest'):
+        """
+            Generate the b64 encoded sha256 hash of a given xml string:
+                - First: Transform the xml content using a pre-hash_invoice.xsl file
+                - Second: Canonicalize the transformed xml content using the c14n method
+                - Third: hash the canonicalized content using the sha256 algorithm then encode it into b64 format
+        """
+        xml_sha = self._l10n_sa_generate_invoice_xml_sha(xml_content)
+        if mode == 'hexdigest':
+            xml_hash = xml_sha.hexdigest().encode()
+        elif mode == 'digest':
+            xml_hash = xml_sha.digest()
+        return b64encode(xml_hash)
+
+    def _l10n_sa_get_previous_invoice_hash(self, invoice):
+        """ Function that returns the Base 64 encoded SHA256 hash of the previously submitted invoice """
+        if invoice.company_id.l10n_sa_api_mode == 'sandbox' or not invoice.journal_id.l10n_sa_latest_submission_hash:
+            # If no invoice, or if using Sandbox, return the b64 encoded SHA256 value of the '0' character
+            return "NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ=="
+        return invoice.journal_id.l10n_sa_latest_submission_hash
+
+    def _get_delivery_vals_list(self, invoice):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+        res = super()._get_delivery_vals_list(invoice)
+        if 'partner_shipping_id' in invoice._fields:
+            for vals in res:
+                vals['actual_delivery_date'] = invoice.l10n_sa_delivery_date
+        return res
+
+    def _get_partner_party_identification_vals_list(self, partner):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+        return [{
+            'id_attrs': {'schemeID': partner.l10n_sa_additional_identification_scheme},
+            'id': partner.l10n_sa_additional_identification_number if partner.l10n_sa_additional_identification_scheme != 'TIN' else partner.vat
+        }]
+
+    def _l10n_sa_get_payment_means_code(self, invoice):
+        """ Return payment means code to be used to set the value on the XML file """
+        return 'unknown'
+
+    def _get_invoice_payment_means_vals_list(self, invoice):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+        res = super()._get_invoice_payment_means_vals_list(invoice)
+        res[0]['payment_means_code'] = PAYMENT_MEANS_CODE[self._l10n_sa_get_payment_means_code(invoice)]
+        res[0]['payment_means_code_attrs'] = {'listID': 'UN/ECE 4461'}
+        res[0]['adjustment_reason'] = invoice.ref
+        return res
+
+    def _get_partner_address_vals(self, partner):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+        return {
+            **super()._get_partner_address_vals(partner),
+            'building_number': partner.l10n_sa_edi_building_number,
+            'neighborhood': partner.street2,
+            'plot_identification': partner.l10n_sa_edi_plot_identification,
+        }
+
+    def _export_invoice_filename(self, invoice):
+        """
+            Generate the name of the invoice XML file according to ZATCA business rules:
+            Seller Vat Number (BT-31), Date (BT-2), Time (KSA-25), Invoice Number (BT-1)
+        """
+        vat = invoice.company_id.partner_id.commercial_partner_id.vat
+        invoice_number = re.sub("[^a-zA-Z0-9 -]", "-", invoice.name)
+        invoice_date = fields.Datetime.context_timestamp(self.with_context(tz='Asia/Riyadh'), invoice.l10n_sa_confirmation_datetime)
+        return '%s_%s_%s.xml' % (vat, invoice_date.strftime('%Y%m%dT%H%M%S'), invoice_number)
+
+    def _l10n_sa_get_invoice_transaction_code(self, invoice):
+        """
+            Returns the transaction code string to be inserted in the UBL file follows the following format:
+                - NNPNESB, in compliance with KSA Business Rule KSA-2, where:
+                    - NN (positions 1 and 2) = invoice subtype:
+                        - 01 for tax invoice
+                        - 02 for simplified tax invoice
+                    - E (position 5) = Exports invoice transaction, 0 for false, 1 for true
+        """
+        return '0%s00%s00' % (
+            '2' if invoice._l10n_sa_is_simplified() else '1',
+            '1' if invoice.commercial_partner_id.country_id != invoice.company_id.country_id and not invoice._l10n_sa_is_simplified() else '0'
+        )
+
+    def _l10n_sa_get_invoice_type(self, invoice):
+        """
+            Returns the invoice type string to be inserted in the UBL file
+                - 383: Debit Note
+                - 381: Credit Note
+                - 388: Invoice
+        """
+        return 383 if invoice.debit_origin_id else 381 if invoice.move_type == 'out_refund' else 388
+
+    def _l10n_sa_get_billing_reference_vals(self, invoice):
+        """ Get the billing reference vals required to render the BillingReference for credit/debit notes """
+        if self._l10n_sa_get_invoice_type(invoice) != 388:
+            return {
+                'id': (invoice.reversed_entry_id.name or invoice.ref) if invoice.move_type == 'out_refund' else invoice.debit_origin_id.name,
+                'issue_date': None,
+            }
+        return {}
+
+    def _get_partner_party_tax_scheme_vals_list(self, partner, role):
+        """
+            Override to return an empty list if the partner is a customer and their country is not KSA.
+            This is according to KSA Business Rule BR-KSA-46 which states that in the case of Export Invoices,
+            the buyer VAT registration number or buyer group VAT registration number must not exist in the Invoice
+        """
+        if role != 'customer' or partner.country_id.code == 'SA':
+            return super()._get_partner_party_tax_scheme_vals_list(partner, role)
+        return []
+
+    def _apply_invoice_tax_filter(self, base_line, tax_values):
+        """ Override to filter out withholding tax """
+        tax_id = self.env['account.tax'].browse(tax_values['id'])
+        res = not tax_id.l10n_sa_is_retention
+        # If the move that is being sent is not a down payment invoice, and the sale module is installed
+        # we need to make sure the line is neither retention, nor a down payment line
+        if not base_line['record'].move_id._is_downpayment():
+            return not tax_id.l10n_sa_is_retention and not base_line['record']._get_downpayment_lines()
+        return res
+
+    def _apply_invoice_line_filter(self, invoice_line):
+        """ Override to filter out down payment lines """
+        if not invoice_line.move_id._is_downpayment():
+            return not invoice_line._get_downpayment_lines()
+        return True
+
+    def _l10n_sa_get_prepaid_amount(self, invoice, vals):
+        """ Calculate the down-payment amount according to ZATCA rules """
+        downpayment_lines = False if invoice._is_downpayment() else invoice.line_ids.filtered(lambda l: l._get_downpayment_lines())
+        if downpayment_lines:
+            tax_vals = invoice._prepare_edi_tax_details(filter_to_apply=lambda l, t: not self.env['account.tax'].browse(t['id']).l10n_sa_is_retention)
+            base_amount = abs(sum(tax_vals['tax_details_per_record'][l]['base_amount_currency'] for l in downpayment_lines))
+            tax_amount = abs(sum(tax_vals['tax_details_per_record'][l]['tax_amount_currency'] for l in downpayment_lines))
+            return {
+                'total_amount': base_amount + tax_amount,
+                'base_amount': base_amount,
+                'tax_amount': tax_amount
+            }
+
+    def _l10n_sa_get_monetary_vals(self, invoice, vals):
+        """ Calculate the invoice monteray amount values, including prepaid amounts (down payment) """
+        # We use base_amount_currency + tax_amount_currency instead of amount_total because we do not want to include
+        # withholding tax amounts in our calculations
+        total_amount = abs(vals['taxes_vals']['base_amount_currency'] + vals['taxes_vals']['tax_amount_currency'])
+
+        tax_inclusive_amount = total_amount
+        tax_exclusive_amount = abs(vals['taxes_vals']['base_amount_currency'])
+        prepaid_amount = 0
+        payable_amount = total_amount
+
+        # - When we calculate the tax values, we filter out taxes and invoice lines linked to downpayments.
+        #   As such, when we calculate the TaxInclusiveAmount, it already accounts for the tax amount of the downpayment
+        #   Same goes for the TaxExclusiveAmount, and we do not need to add the Tax amount of the downpayment
+        # - The payable amount does not account for the tax amount of the downpayment, so we add it
+        downpayment_vals = self._l10n_sa_get_prepaid_amount(invoice, vals)
+
+        if downpayment_vals:
+            # Makes no sense, but according to ZATCA, if there is a downpayment, the TotalInclusiveAmount
+            # should include the total amount of the invoice (including downpayment amount) PLUS the downpayment
+            # total amount, AGAIN.
+            prepaid_amount = tax_inclusive_amount + downpayment_vals['total_amount']
+            payable_amount = - downpayment_vals['total_amount']
+
+        return {
+            'tax_inclusive_amount': tax_inclusive_amount,
+            'tax_exclusive_amount': tax_exclusive_amount,
+            'prepaid_amount': prepaid_amount,
+            'payable_amount': payable_amount
+        }
+
+    def _get_tax_category_list(self, invoice, taxes):
+        """ Override to filter out withholding taxes """
+        non_retention_taxes = taxes.filtered(lambda t: not t.l10n_sa_is_retention)
+        return super()._get_tax_category_list(invoice, non_retention_taxes)
+
+    def _export_invoice_vals(self, invoice):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+        vals = super()._export_invoice_vals(invoice)
+
+        vals.update({
+            'main_template': 'account_edi_ubl_cii.ubl_20_Invoice',
+            'InvoiceType_template': 'l10n_sa_edi.ubl_21_InvoiceType_zatca',
+            'InvoiceLineType_template': 'l10n_sa_edi.ubl_21_InvoiceLineType_zatca',
+            'AddressType_template': 'l10n_sa_edi.ubl_21_AddressType_zatca',
+            'PartyType_template': 'l10n_sa_edi.ubl_21_PartyType_zatca',
+            'TaxTotalType_template': 'l10n_sa_edi.ubl_21_TaxTotalType_zatca',
+            'PaymentMeansType_template': 'l10n_sa_edi.ubl_21_PaymentMeansType_zatca',
+        })
+
+        vals['vals'].update({
+            'profile_id': 'reporting:1.0',
+            'invoice_type_code_attrs': {'name': self._l10n_sa_get_invoice_transaction_code(invoice)},
+            'invoice_type_code': self._l10n_sa_get_invoice_type(invoice),
+            'issue_date': fields.Datetime.context_timestamp(self.with_context(tz='Asia/Riyadh'),
+                                                            invoice.l10n_sa_confirmation_datetime),
+            'previous_invoice_hash': self._l10n_sa_get_previous_invoice_hash(invoice),
+            'billing_reference_vals': self._l10n_sa_get_billing_reference_vals(invoice),
+            'tax_total_vals': self._l10n_sa_get_additional_tax_total_vals(invoice, vals),
+            # Due date is not required for ZATCA UBL 2.1
+            'due_date': None,
+        })
+
+        vals['vals']['legal_monetary_total_vals'].update(self._l10n_sa_get_monetary_vals(invoice, vals))
+
+        return vals
+
+    def _l10n_sa_get_additional_tax_total_vals(self, invoice, vals):
+        """
+            For ZATCA, an additional TaxTotal element needs to be included in the UBL file
+            (Only for the Invoice, not the lines)
+
+            If the invoice is in a different currency from the one set on the company (SAR), then the additional
+            TaxAmount element needs to hold the tax amount converted to the company's currency.
+
+            Business Rules: BT-110 & BT-111
+        """
+        curr_amount = abs(vals['taxes_vals']['tax_amount_currency'])
+        if invoice.currency_id != invoice.company_currency_id:
+            curr_amount = abs(vals['taxes_vals']['tax_amount'])
+        return vals['vals']['tax_total_vals'] + [{
+            'currency': invoice.company_currency_id,
+            'currency_dp': invoice.company_currency_id.decimal_places,
+            'tax_amount': curr_amount,
+        }]
+
+    def _get_invoice_line_item_vals(self, line, taxes_vals):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+        vals = super()._get_invoice_line_item_vals(line, taxes_vals)
+        vals['sellers_item_identification_vals'] = {'id': line.product_id.code or line.product_id.default_code}
+        return vals
+
+    def _l10n_sa_get_line_prepayment_vals(self, line, taxes_vals):
+        """
+            If an invoice line is linked to a down payment invoice, we need to return the proper values
+            to be included in the UBL
+        """
+        if not line.move_id._is_downpayment() and line.sale_line_ids and all(sale_line.is_downpayment for sale_line in line.sale_line_ids):
+            prepayment_move_id = line.sale_line_ids.invoice_lines.move_id.filtered(lambda m: m._is_downpayment())
+            return {
+                'prepayment_id': prepayment_move_id.name,
+                'issue_date': fields.Datetime.context_timestamp(self.with_context(tz='Asia/Riyadh'),
+                                                                prepayment_move_id.l10n_sa_confirmation_datetime),
+                'document_type_code': 386
+            }
+        return {}
+
+    def _get_invoice_line_vals(self, line, taxes_vals):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+
+        def grouping_key_generator(base_line, tax_values):
+            tax = tax_values['tax_repartition_line'].tax_id
+            tax_category_vals = self._get_tax_category_list(line.move_id, tax)[0]
+            grouping_key = {
+                'tax_category_id': tax_category_vals['id'],
+                'tax_category_percent': tax_category_vals['percent'],
+                '_tax_category_vals_': tax_category_vals,
+                'tax_amount_type': tax.amount_type,
+            }
+            if tax.amount_type == 'fixed':
+                grouping_key['tax_name'] = tax.name
+            return grouping_key
+
+        if not line.move_id._is_downpayment() and line._get_downpayment_lines():
+            # When we initially calculate the taxes_vals, we filter out the down payment lines, which means we have no
+            # values to set in the TaxableAmount and TaxAmount nodes on the InvoiceLine for the down payment.
+            # This means ZATCA will return a warning message for the BR-KSA-80 rule since it cannot calculate the
+            # TaxableAmount and the TaxAmount nodes correctly. To avoid this, we re-caclculate the taxes_vals just before
+            # we set the values for the down payment line, and we do not pass any filters to the _prepare_edi_tax_details
+            # method
+            line_taxes = line.move_id._prepare_edi_tax_details(grouping_key_generator=grouping_key_generator)
+            taxes_vals = line_taxes['tax_details_per_record'][line]
+
+        line_vals = super()._get_invoice_line_vals(line, taxes_vals)
+        total_amount_sa = abs(taxes_vals['tax_amount_currency'] + taxes_vals['base_amount_currency'])
+        extension_amount = abs(line_vals['line_extension_amount'])
+        if not line.move_id._is_downpayment() and line._get_downpayment_lines():
+            total_amount_sa = extension_amount = 0
+            line_vals['price_vals']['price_amount'] = 0
+            line_vals['tax_total_vals'][0]['tax_amount'] = 0
+            line_vals['prepayment_vals'] = self._l10n_sa_get_line_prepayment_vals(line, taxes_vals)
+        line_vals['tax_total_vals'][0]['total_amount_sa'] = total_amount_sa
+        line_vals['invoiced_quantity'] = abs(line_vals['invoiced_quantity'])
+        line_vals['line_extension_amount'] = extension_amount
+
+        return line_vals
+
+    def _get_invoice_tax_totals_vals_list(self, invoice, taxes_vals):
+        """
+            Override to include/update values specific to ZATCA's UBL 2.1 specs.
+            In this case, we make sure the tax amounts are always absolute (no negative values)
+        """
+        res = [{
+            'currency': invoice.currency_id,
+            'currency_dp': invoice.currency_id.decimal_places,
+            'tax_amount': abs(taxes_vals['tax_amount_currency']),
+            'tax_subtotal_vals': [{
+                'currency': invoice.currency_id,
+                'currency_dp': invoice.currency_id.decimal_places,
+                'taxable_amount': abs(vals['base_amount_currency']),
+                'tax_amount': abs(vals['tax_amount_currency']),
+                'percent': vals['_tax_category_vals_']['percent'],
+                'tax_category_vals': vals['_tax_category_vals_'],
+            } for vals in taxes_vals['tax_details'].values()],
+        }]
+        return res
+
+    def _get_tax_unece_codes(self, invoice, tax):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+
+        def _exemption_reason(code, reason):
+            return {
+                'tax_category_code': code,
+                'tax_exemption_reason_code': reason,
+                'tax_exemption_reason': exemption_codes[reason].split(reason)[1].lstrip(),
+            }
+
+        supplier = invoice.company_id.partner_id.commercial_partner_id
+        customer = invoice.commercial_partner_id
+        if supplier.country_id == customer.country_id and supplier.country_id.code == 'SA':
+            if not tax or tax.amount == 0:
+                exemption_codes = dict(tax._fields["l10n_sa_exemption_reason_code"]._description_selection(self.env))
+                if tax.l10n_sa_exemption_reason_code in TAX_EXEMPTION_CODES:
+                    return _exemption_reason('E', tax.l10n_sa_exemption_reason_code)
+                elif tax.l10n_sa_exemption_reason_code in TAX_ZERO_RATE_CODES:
+                    return _exemption_reason('Z', tax.l10n_sa_exemption_reason_code)
+                else:
+                    return {
+                        'tax_category_code': 'O',
+                        'tax_exemption_reason_code': 'Not subject to VAT',
+                        'tax_exemption_reason': 'Not subject to VAT',
+                    }
+            else:
+                return {
+                    'tax_category_code': 'S',
+                    'tax_exemption_reason_code': None,
+                    'tax_exemption_reason': None,
+                }
+        return super()._get_tax_unece_codes(invoice, tax)
+
+    def _get_invoice_payment_terms_vals_list(self, invoice):
+        """ Override to include/update values specific to ZATCA's UBL 2.1 specs """
+        return []
diff --git a/addons/l10n_sa_edi/models/account_journal.py b/addons/l10n_sa_edi/models/account_journal.py
new file mode 100644
index 000000000000..e7ca06eb4676
--- /dev/null
+++ b/addons/l10n_sa_edi/models/account_journal.py
@@ -0,0 +1,624 @@
+import json
+import requests
+from markupsafe import Markup
+from lxml import etree
+from datetime import datetime
+from base64 import b64encode, b64decode
+from odoo import models, fields, service, _, api
+from odoo.exceptions import UserError
+from odoo.modules.module import get_module_resource
+from requests.exceptions import HTTPError, RequestException
+from cryptography import x509
+from cryptography.x509 import ObjectIdentifier, load_der_x509_certificate
+from cryptography.x509.oid import NameOID
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.serialization import Encoding, load_pem_private_key
+from urllib.parse import urljoin
+
+ZATCA_API_URLS = {
+    "sandbox": "https://gw-fatoora.zatca.gov.sa/e-invoicing/developer-portal/",
+    "preprod": "https://gw-fatoora.zatca.gov.sa/e-invoicing/simulation/",
+    "prod": "https://gw-fatoora.zatca.gov.sa/e-invoicing/core/",
+    "apis": {
+        "ccsid": "compliance",
+        "pcsid": "production/csids",
+        "compliance": "compliance/invoices",
+        "reporting": "invoices/reporting/single",
+        "clearance": "invoices/clearance/single",
+    }
+}
+
+CERT_TEMPLATE_NAME = {
+    'prod': b'\x0c\x12ZATCA-Code-Signing',
+    'sandbox': b'\x13\x15PREZATCA-Code-Signing',
+    'preprod': b'\x13\x15PREZATCA-Code-Signing',
+}
+# This SANDBOX_AUTH is only used for testing purposes, and is shared to all users of the sandbox environment
+SANDBOX_AUTH = {
+    'binarySecurityToken': "TUlJRDFEQ0NBM21nQXdJQkFnSVRid0FBZTNVQVlWVTM0SS8rNVFBQkFBQjdkVEFLQmdncWhrak9QUVFEQWpCak1SVXdFd1lLQ1pJbWlaUHlMR1FCR1JZRmJHOWpZV3d4RXpBUkJnb0praWFKay9Jc1pBRVpGZ05uYjNZeEZ6QVZCZ29Ka2lhSmsvSXNaQUVaRmdkbGVIUm5ZWHAwTVJ3d0dnWURWUVFERXhOVVUxcEZTVTVXVDBsRFJTMVRkV0pEUVMweE1CNFhEVEl5TURZeE1qRTNOREExTWxvWERUSTBNRFl4TVRFM05EQTFNbG93U1RFTE1Ba0dBMVVFQmhNQ1UwRXhEakFNQmdOVkJBb1RCV0ZuYVd4bE1SWXdGQVlEVlFRTEV3MW9ZWGxoSUhsaFoyaHRiM1Z5TVJJd0VBWURWUVFERXdreE1qY3VNQzR3TGpFd1ZqQVFCZ2NxaGtqT1BRSUJCZ1VyZ1FRQUNnTkNBQVRUQUs5bHJUVmtvOXJrcTZaWWNjOUhEUlpQNGI5UzR6QTRLbTdZWEorc25UVmhMa3pVMEhzbVNYOVVuOGpEaFJUT0hES2FmdDhDL3V1VVk5MzR2dU1ObzRJQ0p6Q0NBaU13Z1lnR0ExVWRFUVNCZ0RCK3BId3dlakViTUJrR0ExVUVCQXdTTVMxb1lYbGhmREl0TWpNMGZETXRNVEV5TVI4d0hRWUtDWkltaVpQeUxHUUJBUXdQTXpBd01EYzFOVGc0TnpBd01EQXpNUTB3Q3dZRFZRUU1EQVF4TVRBd01SRXdEd1lEVlFRYURBaGFZWFJqWVNBeE1qRVlNQllHQTFVRUR3d1BSbTl2WkNCQ2RYTnphVzVsYzNNek1CMEdBMVVkRGdRV0JCU2dtSVdENmJQZmJiS2ttVHdPSlJYdkliSDlIakFmQmdOVkhTTUVHREFXZ0JSMllJejdCcUNzWjFjMW5jK2FyS2NybVRXMUx6Qk9CZ05WSFI4RVJ6QkZNRU9nUWFBL2hqMW9kSFJ3T2k4dmRITjBZM0pzTG5waGRHTmhMbWR2ZGk1ellTOURaWEowUlc1eWIyeHNMMVJUV2tWSlRsWlBTVU5GTFZOMVlrTkJMVEV1WTNKc01JR3RCZ2dyQmdFRkJRY0JBUVNCb0RDQm5UQnVCZ2dyQmdFRkJRY3dBWVppYUhSMGNEb3ZMM1J6ZEdOeWJDNTZZWFJqWVM1bmIzWXVjMkV2UTJWeWRFVnVjbTlzYkM5VVUxcEZhVzUyYjJsalpWTkRRVEV1WlhoMFoyRjZkQzVuYjNZdWJHOWpZV3hmVkZOYVJVbE9WazlKUTBVdFUzVmlRMEV0TVNneEtTNWpjblF3S3dZSUt3WUJCUVVITUFHR0gyaDBkSEE2THk5MGMzUmpjbXd1ZW1GMFkyRXVaMjkyTG5OaEwyOWpjM0F3RGdZRFZSMFBBUUgvQkFRREFnZUFNQjBHQTFVZEpRUVdNQlFHQ0NzR0FRVUZCd01DQmdnckJnRUZCUWNEQXpBbkJna3JCZ0VFQVlJM0ZRb0VHakFZTUFvR0NDc0dBUVVGQndNQ01Bb0dDQ3NHQVFVRkJ3TURNQW9HQ0NxR1NNNDlCQU1DQTBrQU1FWUNJUUNWd0RNY3E2UE8rTWNtc0JYVXovdjFHZGhHcDdycVNhMkF4VEtTdjgzOElBSWhBT0JOREJ0OSszRFNsaWpvVmZ4enJkRGg1MjhXQzM3c21FZG9HV1ZyU3BHMQ==",
+    'secret': "Xlj15LyMCgSC66ObnEO/qVPfhSbs3kDTjWnGheYhfSs="
+}
+
+
+class AccountJournal(models.Model):
+    _inherit = 'account.journal'
+
+    """
+        In order to clear/report an invoice through the ZATCA API, we need to onboard each journal by following
+        three steps:
+
+            STEP 1:
+                Make a call to the Compliance CSID API '/compliance'.
+                This will return three things:
+                    -   X509 Compliance Cryptographic Stamp Identifier (CCSID/Certificate)
+                    -   Password (Secret)
+                    -   Compliance Request ID
+            STEP 2:
+                Make a call to the Compliance Checks API '/compliance/invoices', by passing the hashed xml content
+                of the files available in the tests/compliance folder. This will check if the provided
+                Standard/Simplified Invoices comply with UBL 2.1 standards in line with ZATCA specifications
+            STEP 3:
+                Make a call to the Production CSID API '/production/csids' including the Compliance Certificate,
+                Password and Request ID from STEP 1.
+                This will return three things:
+                    -   X509 Production Certificate
+                    -   Password (Secret)
+                    -   Production Request ID
+    """
+
+    l10n_sa_csr = fields.Binary(attachment=True, copy=False, groups="base.group_system",
+                                help="The Certificate Signing Request that is submitted to the Compliance API")
+    l10n_sa_csr_errors = fields.Html("Onboarding Errors", copy=False)
+
+    l10n_sa_compliance_csid_json = fields.Char("CCSID JSON", copy=False, groups="base.group_system",
+                                               help="Compliance CSID data received from the Compliance CSID API "
+                                                    "in dumped json format")
+    l10n_sa_production_csid_json = fields.Char("PCSID JSON", copy=False, groups="base.group_system",
+                                               help="Production CSID data received from the Production CSID API "
+                                                    "in dumped json format")
+    l10n_sa_production_csid_validity = fields.Datetime("PCSID Expiration", help="Production CSID expiration date",
+                                                       compute="_l10n_sa_compute_production_csid_validity", store=True)
+    l10n_sa_compliance_checks_passed = fields.Boolean("Compliance Checks Done", default=False, copy=False,
+                                                      help="Specifies if the Compliance Checks have been completed successfully")
+
+    l10n_sa_chain_sequence_id = fields.Many2one('ir.sequence', string='ZATCA account.move chain sequence',
+                                                readonly=True, copy=False)
+
+    l10n_sa_serial_number = fields.Char("Serial Number", copy=False,
+                                        help="The serial number of the Taxpayer solution unit. Provided by ZATCA")
+
+    l10n_sa_latest_submission_hash = fields.Char("Latest Submission Hash", copy=False,
+                                                 help="Hash of the latest submitted invoice to be used as the Previous Invoice Hash (KSA-13)")
+
+    # ====== Utility Functions =======
+
+    def _l10n_sa_ready_to_submit_einvoices(self):
+        """
+            Helper function to know if the required CSIDs have been obtained, and the compliance checks have been
+            completed
+        """
+        self.ensure_one()
+        return self.sudo().l10n_sa_production_csid_json
+
+    # ====== CSR Generation =======
+
+    def _l10n_sa_csr_required_fields(self):
+        """ Return the list of fields required to generate a valid CSR as per ZATCA requirements """
+        return ['l10n_sa_private_key', 'vat', 'name', 'city', 'country_id', 'state_id']
+
+    def _l10n_sa_get_csr_str(self):
+        """
+            Return s string representation of a ZATCA compliant CSR that will be sent to the Compliance API in order to get back
+            a signed X509 certificate
+        """
+        self.ensure_one()
+
+        def _encode(s):
+            """
+                Some of the information included in the CSR could be in arabic, and thus needs to be encoded in a
+                specific format in order to be compliant with the ZATCA CCSID/PCSID APIs
+            """
+            return s.encode().decode('CP1252')
+
+        company_id = self.company_id
+        version_info = service.common.exp_version()
+        builder = x509.CertificateSigningRequestBuilder()
+        subject_names = (
+            # Country Name
+            (NameOID.COUNTRY_NAME, company_id.country_id.code),
+            # Organization Unit Name
+            (NameOID.ORGANIZATIONAL_UNIT_NAME, (company_id.vat or '')[:10]),
+            # Organization Name
+            (NameOID.ORGANIZATION_NAME, _encode(company_id.name)),
+            # Subject Common Name
+            (NameOID.COMMON_NAME, _encode(company_id.name)),
+            # Organization Identifier
+            (ObjectIdentifier('2.5.4.97'), company_id.vat),
+            # State/Province Name
+            (NameOID.STATE_OR_PROVINCE_NAME, _encode(company_id.state_id.name)),
+            # Locality Name
+            (NameOID.LOCALITY_NAME, _encode(company_id.city)),
+        )
+        # The CertificateSigningRequestBuilder instances are immutable, which is why everytime we modify one,
+        # we have to assign it back to itself to keep track of the changes
+        builder = builder.subject_name(x509.Name([
+            x509.NameAttribute(n[0], u'%s' % n[1]) for n in subject_names
+        ]))
+
+        x509_alt_names_extension = x509.SubjectAlternativeName([
+            x509.DirectoryName(x509.Name([
+                # EGS Serial Number. Manufacturer or Solution Provider Name, Model or Version and Serial Number.
+                # To be written in the following format: "1-... |2-... |3-..."
+                x509.NameAttribute(ObjectIdentifier('2.5.4.4'), '1-Odoo|2-%s|3-%s' % (
+                    version_info['server_version_info'][0], self.l10n_sa_serial_number)),
+                # Organisation Identifier (UID)
+                x509.NameAttribute(NameOID.USER_ID, company_id.vat),
+                # Invoice Type. 4-digit numerical input using 0 & 1
+                x509.NameAttribute(NameOID.TITLE, company_id._l10n_sa_get_csr_invoice_type()),
+                # Location
+                x509.NameAttribute(ObjectIdentifier('2.5.4.26'), _encode(company_id.street)),
+                # Industry
+                x509.NameAttribute(ObjectIdentifier('2.5.4.15'),
+                                   _encode(company_id.partner_id.industry_id.name or 'Other')),
+            ]))
+        ])
+
+        x509_extensions = (
+            # Add Certificate template name extension
+            (x509.UnrecognizedExtension(ObjectIdentifier('1.3.6.1.4.1.311.20.2'),
+                                        CERT_TEMPLATE_NAME[company_id.l10n_sa_api_mode]), False),
+            # Add alternative names extension
+            (x509_alt_names_extension, False),
+        )
+
+        for ext in x509_extensions:
+            builder = builder.add_extension(ext[0], critical=ext[1])
+
+        private_key = load_pem_private_key(company_id.l10n_sa_private_key, password=None, backend=default_backend())
+        request = builder.sign(private_key, hashes.SHA256(), default_backend())
+
+        return b64encode(request.public_bytes(Encoding.PEM)).decode()
+
+    def _l10n_sa_generate_csr(self):
+        """
+            Generate a CSR for the Journal to be used for the Onboarding process and Invoice submissions
+        """
+        self.ensure_one()
+        if any(not self.company_id[f] for f in self._l10n_sa_csr_required_fields()):
+            raise UserError(_("Please, make sure all the following fields have been correctly set on the Company: \n")
+                            + "\n".join(
+                " - %s" % self.company_id._fields[f].string for f in self._l10n_sa_csr_required_fields() if
+                not self.company_id[f]))
+        self._l10n_sa_reset_certificates()
+        self.l10n_sa_csr = self._l10n_sa_get_csr_str()
+
+    # ====== Certificate Methods =======
+
+    @api.depends('l10n_sa_production_csid_json')
+    def _l10n_sa_compute_production_csid_validity(self):
+        """
+            Compute the expiration date of the Production certificate
+        """
+        for journal in self:
+            journal.l10n_sa_production_csid_validity = False
+            if journal.l10n_sa_production_csid_json:
+                journal.l10n_sa_production_csid_validity = self._l10n_sa_get_pcsid_validity(
+                    json.loads(journal.l10n_sa_production_csid_json))
+
+    def _l10n_sa_reset_certificates(self):
+        """
+            Reset all certificate values, including CSR and compliance checks
+        """
+        for journal in self.sudo():
+            journal.l10n_sa_csr = False
+            journal.l10n_sa_production_csid_json = False
+            journal.l10n_sa_compliance_csid_json = False
+            journal.l10n_sa_compliance_checks_passed = False
+
+    def _l10n_sa_api_onboard_journal(self, otp):
+        """
+            Perform the onboarding for the journal. The onboarding consists of three steps:
+                1.  Get the Compliance CSID
+                2.  Perform the Compliance Checks
+                3.  Get the Production CSID
+        """
+        self.ensure_one()
+        try:
+            # If the company does not have a private key, we generate it.
+            # The private key is used to generate the CSR but also to sign the invoices
+            if not self.company_id.l10n_sa_private_key:
+                self.company_id.l10n_sa_private_key = self.company_id._l10n_sa_generate_private_key()
+            self._l10n_sa_generate_csr()
+            # STEP 1: The first step of the process is to get the CCSID
+            self._l10n_sa_get_compliance_CSID(otp)
+            # STEP 2: Once we have the CCSID, we preform the compliance checks
+            self._l10n_sa_run_compliance_checks()
+            # STEP 3: Once the compliance checks are completed, we request the PCSID
+            self._l10n_sa_get_production_CSID()
+            # Once all three steps are completed, we set the errors field to False
+            self.l10n_sa_csr_errors = False
+        except (RequestException, HTTPError, UserError) as e:
+            # In case of an exception returned from ZATCA (not timeout), we will need to regenerate the CSR
+            # As the same CSR cannot be used twice for the same CCSID request
+            self._l10n_sa_reset_certificates()
+            self.l10n_sa_csr_errors = e.args[0] or _("Journal could not be onboarded")
+
+    def _l10n_sa_get_compliance_CSID(self, otp):
+        """
+            Request a Compliance Cryptographic Stamp Identifier (CCSID) from ZATCA
+        """
+        CCSID_data = self._l10n_sa_api_get_compliance_CSID(otp)
+        if CCSID_data.get('error'):
+            raise UserError(_("Could not obtain Compliance CSID: %s") % CCSID_data['error'])
+        self.sudo().write({
+            'l10n_sa_compliance_csid_json': json.dumps(CCSID_data),
+            'l10n_sa_production_csid_json': False,
+            'l10n_sa_compliance_checks_passed': False,
+        })
+
+    def _l10n_sa_get_production_CSID(self, OTP=None):
+        """
+            Request a Production Cryptographic Stamp Identifier (PCSID) from ZATCA
+        """
+
+        self_sudo = self.sudo()
+
+        if not self_sudo.l10n_sa_compliance_csid_json:
+            raise UserError(_("Cannot request a Production CSID before requesting a CCSID first"))
+        elif not self_sudo.l10n_sa_compliance_checks_passed:
+            raise UserError(_("Cannot request a Production CSID before completing the Compliance Checks"))
+
+        renew = False
+        zatca_format = self.env.ref('l10n_sa_edi.edi_sa_zatca')
+
+        if self_sudo.l10n_sa_production_csid_json:
+            time_now = zatca_format._l10n_sa_get_zatca_datetime(datetime.now())
+            if zatca_format._l10n_sa_get_zatca_datetime(self_sudo.l10n_sa_production_csid_validity) < time_now:
+                renew = True
+            else:
+                raise UserError(_("The Production CSID is still valid. You can only renew it once it has expired."))
+
+        CCSID_data = json.loads(self_sudo.l10n_sa_compliance_csid_json)
+        PCSID_data = self_sudo._l10n_sa_request_production_csid(CCSID_data, renew, OTP)
+        if PCSID_data.get('error'):
+            raise UserError(_("Could not obtain Production CSID: %s") % PCSID_data['error'])
+        self_sudo.l10n_sa_production_csid_json = json.dumps(PCSID_data)
+
+    # ====== Compliance Checks =======
+
+    def _l10n_sa_get_compliance_files(self):
+        """
+            Return the list of files to be used for the compliance checks.
+        """
+        file_names, compliance_files = [
+            'standard/invoice.xml', 'standard/credit.xml', 'standard/debit.xml',
+            'simplified/invoice.xml', 'simplified/credit.xml', 'simplified/debit.xml',
+        ], {}
+        for file in file_names:
+            fpath = get_module_resource('l10n_sa_edi', 'tests/compliance', file)
+            with open(fpath, 'rb') as ip:
+                compliance_files[file] = ip.read().decode()
+        return compliance_files
+
+    def _l10n_sa_run_compliance_checks(self):
+        """
+            Run Compliance Checks once the CCSID has been obtained.
+
+            The goal of the Compliance Checks is to make sure our system is able to produce, sign and send Invoices
+            correctly. For this we use dummy invoice UBL files available under the tests/compliance folder:
+
+            Standard Invoice, Standard Credit Note, Standard Debit Note, Simplified Invoice, Simplified Credit Note,
+            Simplified Debit Note.
+
+            We read each one of these files separately, sign them, then process them through the Compliance Checks API.
+        """
+
+        self.ensure_one()
+        self_sudo = self.sudo()
+        if self.country_code != 'SA':
+            raise UserError(_("Compliance checks can only be run for companies operating from KSA"))
+        if not self_sudo.l10n_sa_compliance_csid_json:
+            raise UserError(_("You need to request the CCSID first before you can proceed"))
+        CCSID_data = json.loads(self_sudo.l10n_sa_compliance_csid_json)
+        compliance_files = self._l10n_sa_get_compliance_files()
+        for fname, fval in compliance_files.items():
+            invoice_hash_hex = self.env['account.edi.xml.ubl_21.zatca']._l10n_sa_generate_invoice_xml_hash(
+                fval).decode()
+            digital_signature = self.env.ref('l10n_sa_edi.edi_sa_zatca')._l10n_sa_get_digital_signature(self.company_id, invoice_hash_hex).decode()
+            prepared_xml = self._l10n_sa_prepare_compliance_xml(fname, fval, CCSID_data['binarySecurityToken'],
+                                                                digital_signature)
+            result = self._l10n_sa_api_compliance_checks(prepared_xml.decode(), CCSID_data)
+            if result.get('error'):
+                raise UserError(Markup("<p class='mb-0'>%s <b>%s</b></p>") % (_("Could not complete Compliance Checks for the following file:"), fname))
+            if result['validationResults']['status'] == 'WARNING':
+                warnings = "".join(Markup("<li><b>%s</b>: %s </li>") % (e['code'], e['message']) for e in result['validationResults']['warningMessages'])
+                self.l10n_sa_csr_errors = Markup("<br/><br/><ul class='pl-3'><b>%s</b>%s</ul>") % (_("Warnings:"), warnings)
+            elif result['validationResults']['status'] != 'PASS':
+                errors = "".join(Markup("<li><b>%s</b>: %s </li>") % (e['code'], e['message']) for e in result['validationResults']['errorMessages'])
+                raise UserError(Markup("<p class='mb-0'>%s <b>%s</b> %s</p>")
+                                % (_("Could not complete Compliance Checks for the following file:"), fname, Markup("<br/><br/><ul class='pl-3'><b>%s</b>%s</ul>") % (_("Errors:"), errors)))
+        self.l10n_sa_compliance_checks_passed = True
+
+    def _l10n_sa_prepare_compliance_xml(self, xml_name, xml_raw, PCSID, signature):
+        """
+            Prepare XML content to be used for Compliance checks
+        """
+        xml_content = self._l10n_sa_prepare_invoice_xml(xml_raw)
+        signed_xml = self.env.ref('l10n_sa_edi.edi_sa_zatca')._l10n_sa_sign_xml(xml_content, PCSID, signature)
+        if xml_name.startswith('simplified'):
+            qr_code_str = self.env['account.move']._l10n_sa_get_qr_code(self, signed_xml, b64decode(PCSID).decode(),
+                                                                        signature, True)
+            root = etree.fromstring(signed_xml)
+            qr_node = root.xpath('//*[local-name()="ID"][text()="QR"]/following-sibling::*/*')[0]
+            qr_node.text = b64encode(qr_code_str).decode()
+            return etree.tostring(root, with_tail=False)
+        return signed_xml
+
+    def _l10n_sa_prepare_invoice_xml(self, xml_content):
+        """
+            Prepare the XML content of the test invoices before running the compliance checks
+        """
+        ubl_extensions = etree.fromstring(self.env['ir.qweb']._render('l10n_sa_edi.export_sa_zatca_ubl_extensions'))
+        root = etree.fromstring(xml_content.encode())
+        root.insert(0, ubl_extensions)
+        ns_map = self.env['account.edi.xml.ubl_21.zatca']._l10n_sa_get_namespaces()
+
+        def _get_node(xpath_str):
+            return root.xpath(xpath_str, namespaces=ns_map)[0]
+
+        # Update the Company VAT number in the test invoice
+        vat_el = _get_node('//cbc:CompanyID')
+        vat_el.text = self.company_id.vat
+
+        # Update the Company Name in the test invoice
+        name_nodes = ['cac:PartyName/cbc:Name', 'cac:PartyLegalEntity/cbc:RegistrationName', 'cac:Contact/cbc:Name']
+        for node in name_nodes:
+            comp_name_el = _get_node('//cac:AccountingSupplierParty/cac:Party/' + node)
+            comp_name_el.text = self.company_id.display_name
+
+        return etree.tostring(root)
+
+    # ====== Index Chain & Previous Invoice Calculation =======
+
+    def _l10n_sa_edi_get_next_chain_index(self):
+        self.ensure_one()
+        if not self.l10n_sa_chain_sequence_id:
+            self.l10n_sa_chain_sequence_id = self.env['ir.sequence'].create({
+                'name': f'ZATCA account move sequence for Journal {self.name} (id: {self.id})',
+                'code': f'l10n_sa_edi.account.move.{self.id}',
+                'implementation': 'no_gap',
+                'company_id': self.company_id.id,
+            })
+        return self.l10n_sa_chain_sequence_id.next_by_id()
+
+    def _l10n_sa_get_last_posted_invoice(self):
+        """
+        Returns the last invoice posted to this journal's chain.
+        That invoice may have been received by the govt or not (eg. in case of a timeout).
+        Only upon confirmed reception/refusal of that invoice can another one be posted.
+        """
+        self.ensure_one()
+        return self.env['account.move'].search(
+            [
+                ('journal_id', '=', self.id),
+                ('l10n_sa_chain_index', '!=', 0)
+            ],
+            limit=1, order='l10n_sa_chain_index desc'
+        )
+
+    # ====== API Calls to ZATCA =======
+
+    def _l10n_sa_api_get_compliance_CSID(self, otp):
+        """
+            API call to the Compliance CSID API to generate a CCSID certificate, password and compliance request_id
+            Requires a CSR token and a One Time Password (OTP)
+        """
+        self.ensure_one()
+        if not otp:
+            raise UserError(_("Please, set a valid OTP to be used for Onboarding"))
+        if not self.l10n_sa_csr:
+            raise UserError(_("Please, generate a CSR before requesting a CCSID"))
+        request_data = {
+            'body': json.dumps({'csr': self.l10n_sa_csr.decode()}),
+            'header': {'OTP': otp}
+        }
+        return self._l10n_sa_call_api(request_data, ZATCA_API_URLS['apis']['ccsid'], 'POST')
+
+    def _l10n_sa_api_get_production_CSID(self, CCSID_data):
+        """
+            API call to the Production CSID API to generate a PCSID certificate, password and production request_id
+            Requires a requestID from the Compliance CSID API
+        """
+        request_data = {
+            'body': json.dumps({'compliance_request_id': str(CCSID_data['requestID'])}),
+            'header': {'Authorization': self._l10n_sa_authorization_header(CCSID_data)}
+        }
+        return self._l10n_sa_call_api(request_data, ZATCA_API_URLS['apis']['pcsid'], 'POST')
+
+    def _l10n_sa_api_renew_production_CSID(self, PCSID_data, OTP):
+        """
+            API call to the Production CSID API to renew a PCSID certificate, password and production request_id
+            Requires an expired Production CSIDPCSID_data
+        """
+        self.ensure_one()
+        auth_data = PCSID_data
+        # For renewal, the sandbox API expects a specific Username/Password, which are set in the SANDBOX_AUTH dict
+        if self.company_id.l10n_sa_api_mode == 'sandbox':
+            auth_data = SANDBOX_AUTH
+        request_data = {
+            'body': json.dumps({'csr': self.l10n_sa_csr.decode()}),
+            'header': {
+                'OTP': OTP,
+                'Authorization': self._l10n_sa_authorization_header(auth_data)
+            }
+        }
+        return self._l10n_sa_call_api(request_data, ZATCA_API_URLS['apis']['pcsid'], 'PATCH')
+
+    def _l10n_sa_api_compliance_checks(self, xml_content, CCSID_data):
+        """
+            API call to the COMPLIANCE endpoint to generate a security token used for subsequent API calls
+            Requires a CSR token and a One Time Password (OTP)
+        """
+        invoice_tree = etree.fromstring(xml_content)
+
+        # Get the Invoice Hash from the XML document
+        invoice_hash_node = invoice_tree.xpath('//*[@Id="invoiceSignedData"]/*[local-name()="DigestValue"]')[0]
+        invoice_hash = invoice_hash_node.text
+
+        # Get the Invoice UUID from the XML document
+        invoice_uuid_node = invoice_tree.xpath('//*[local-name()="UUID"]')[0]
+        invoice_uuid = invoice_uuid_node.text
+
+        request_data = {
+            'body': json.dumps({
+                "invoiceHash": invoice_hash,
+                "uuid": invoice_uuid,
+                "invoice": b64encode(xml_content.encode()).decode()
+            }),
+            'header': {
+                'Authorization': self._l10n_sa_authorization_header(CCSID_data),
+                'Clearance-Status': '1'
+            }
+        }
+        return self._l10n_sa_call_api(request_data, ZATCA_API_URLS['apis']['compliance'], 'POST')
+
+    def _l10n_sa_get_api_clearance_url(self, invoice):
+        """
+            Return the API to be used for clearance. To be overridden to account for other cases, such as reporting.
+        """
+        return ZATCA_API_URLS['apis']['reporting' if invoice._l10n_sa_is_simplified() else 'clearance']
+
+    def _l10n_sa_api_clearance(self, invoice, xml_content, PCSID_data):
+        """
+            API call to the CLEARANCE/REPORTING endpoint to sign an invoice
+                - If SIMPLIFIED invoice: Reporting
+                - If STANDARD invoice: Clearance
+        """
+        invoice_tree = etree.fromstring(xml_content)
+        invoice_hash_node = invoice_tree.xpath('//*[@Id="invoiceSignedData"]/*[local-name()="DigestValue"]')[0]
+        invoice_hash = invoice_hash_node.text
+        request_data = {
+            'body': json.dumps({
+                "invoiceHash": invoice_hash,
+                "uuid": invoice.l10n_sa_uuid,
+                "invoice": b64encode(xml_content.encode()).decode()
+            }),
+            'header': {
+                'Authorization': self._l10n_sa_authorization_header(PCSID_data),
+                'Clearance-Status': '1'
+            }
+        }
+        url_string = self._l10n_sa_get_api_clearance_url(invoice)
+        return self._l10n_sa_call_api(request_data, url_string, 'POST')
+
+    # ====== Certificate Methods =======
+
+    def _l10n_sa_get_pcsid_validity(self, PCSID_data):
+        """
+            Return PCSID expiry date
+        """
+        b64_decoded_pcsid = b64decode(PCSID_data['binarySecurityToken'])
+        x509_certificate = load_der_x509_certificate(b64decode(b64_decoded_pcsid.decode()), default_backend())
+        return x509_certificate.not_valid_after
+
+    def _l10n_sa_request_production_csid(self, csid_data, renew=False, otp=None):
+        """
+            Generate company Production CSID data
+        """
+        self.ensure_one()
+        return (
+            self._l10n_sa_api_renew_production_CSID(csid_data, otp)
+            if renew
+            else self._l10n_sa_api_get_production_CSID(csid_data)
+        )
+
+    def _l10n_sa_api_get_pcsid(self):
+        """
+            Get CSIDs required to perform ZATCA api calls, and regenerate them if they need to be regenerated.
+        """
+        self.ensure_one()
+        if not self.l10n_sa_production_csid_json:
+            raise UserError(_("Please, make a request to obtain the Compliance CSID and Production CSID before sending "
+                            "documents to ZATCA"))
+        pcsid_validity = self.env.ref('l10n_sa_edi.edi_sa_zatca')._l10n_sa_get_zatca_datetime(self.l10n_sa_production_csid_validity)
+        time_now = self.env.ref('l10n_sa_edi.edi_sa_zatca')._l10n_sa_get_zatca_datetime(datetime.now())
+        if pcsid_validity < time_now and self.company_id.l10n_sa_api_mode != 'sandbox':
+            raise UserError(_("Production certificate has expired, please renew the PCSID before proceeding"))
+        return json.loads(self.l10n_sa_production_csid_json)
+
+    # ====== API Helper Methods =======
+
+    def _l10n_sa_call_api(self, request_data, request_url, method):
+        """
+            Helper function to make api calls to the ZATCA API Endpoint
+        """
+        api_url = ZATCA_API_URLS[self.env.company.l10n_sa_api_mode]
+        request_url = urljoin(api_url, request_url)
+        try:
+            request_response = requests.request(method, request_url, data=request_data.get('body'),
+                                                headers={
+                                                    **self._l10n_sa_api_headers(),
+                                                    **request_data.get('header')
+                                                }, timeout=(30, 30))
+            request_response.raise_for_status()
+        except (ValueError, HTTPError) as ex:
+            # In the case of an explicit error from ZATCA, i.e we got a response but the code of the response is not 2xx
+            return {
+                'error': _("Server returned an unexpected error: ") + (request_response.text or str(ex)),
+                'blocking_level': 'error'
+            }
+        except RequestException as ex:
+            # Usually only happens if a Timeout occurs. In this case we're not sure if the invoice was accepted or
+            # rejected, or if it even made it to ZATCA
+            return {'error': str(ex), 'blocking_level': 'warning', 'excepted': True}
+
+        try:
+            response_data = request_response.json()
+        except json.decoder.JSONDecodeError:
+            return {
+                'error': _("JSON response from ZATCA could not be decoded"),
+                'blocking_level': 'error'
+            }
+
+        if not request_response.ok and (response_data.get('errors') or response_data.get('warnings')):
+            if isinstance(response_data, dict) and response_data.get('errors'):
+                return {
+                    'error': _("Invoice submission to ZATCA returned errors"),
+                    'json_errors': response_data['errors'],
+                    'blocking_level': 'error',
+                }
+            return {
+                'error': request_response.reason,
+                'blocking_level': 'error'
+            }
+        return response_data
+
+    def _l10n_sa_api_headers(self):
+        """
+            Return the base headers to be included in ZATCA API calls
+        """
+        return {
+            'Content-Type': 'application/json',
+            'Accept-Language': 'en',
+            'Accept-Version': 'V2'
+        }
+
+    def _l10n_sa_authorization_header(self, CSID_data):
+        """
+            Compute the Authorization header by combining the CSID and the Secret key, then encode to Base64
+        """
+        auth_data = CSID_data
+        auth_str = "%s:%s" % (auth_data['binarySecurityToken'], auth_data['secret'])
+        return 'Basic ' + b64encode(auth_str.encode()).decode()
+
+    def _l10n_sa_load_edi_demo_data(self):
+        self.ensure_one()
+        self.company_id.l10n_sa_private_key = self.company_id._l10n_sa_generate_private_key()
+        self.write({
+            'l10n_sa_serial_number': 'SIDI3-CBMPR-L2D8X-KM0KN-X4ISJ',
+            'l10n_sa_compliance_checks_passed': True,
+            'l10n_sa_csr': b'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ2NqQ0NBaGNDQVFBd2djRXhDekFKQmdOVkJBWVRBbE5CTVJNd0VRWURWUVFMREFvek1UQXhOelV6T1RjMApNUk13RVFZRFZRUUtEQXBUUVNCRGIyMXdZVzU1TVJNd0VRWURWUVFEREFwVFFTQkRiMjF3WVc1NU1SZ3dGZ1lEClZRUmhEQTh6TVRBeE56VXpPVGMwTURBd01ETXhEekFOQmdOVkJBZ01CbEpwZVdGa2FERklNRVlHQTFVRUJ3dy8KdzVqQ3A4T1o0b0NldzVuaWdLYkRtTUt2dzVuRm9NT1o0b0NndzVqQ3FTRERtTUtudzVuaWdKN0RtZUtBcHNPWgo0b0NndzVuTGhzT1l3ckhEbU1LcE1GWXdFQVlIS29aSXpqMENBUVlGSzRFRUFBb0RRZ0FFN2ZpZWZWQ21HcTlzCmV0OVl4aWdQNzZWUmJxZlh0VWNtTk1VN3FkTlBiSm5NNGh5R1QwanpPcXUrSWNXWW5IelFJYmxJVmsydENPQnQKYjExanY4MGVwcUNCOVRDQjhnWUpLb1pJaHZjTkFRa09NWUhrTUlIaE1DUUdDU3NHQVFRQmdqY1VBZ1FYRXhWUQpVa1ZhUVZSRFFTMURiMlJsTFZOcFoyNXBibWN3Z2JnR0ExVWRFUVNCc0RDQnJhU0JxakNCcHpFME1ESUdBMVVFCkJBd3JNUzFQWkc5dmZESXRNVFY4TXkxVFNVUkpNeTFEUWsxUVVpMU1Na1E0V0MxTFRUQkxUaTFZTkVsVFNqRWYKTUIwR0NnbVNKb21UOGl4a0FRRU1Eek14TURFM05UTTVOelF3TURBd016RU5NQXNHQTFVRURBd0VNVEV3TURFdgpNQzBHQTFVRUdnd21RV3dnUVcxcGNpQk5iMmhoYlcxbFpDQkNhVzRnUVdKa2RXd2dRWHBwZWlCVGRISmxaWFF4CkRqQU1CZ05WQkE4TUJVOTBhR1Z5TUFvR0NDcUdTTTQ5QkFNQ0Ewa0FNRVlDSVFEb3VCeXhZRDRuQ2pUQ2V6TkYKczV6SmlVWW1QZVBRNnFWNDdZemRHeWRla1FJaEFPRjNVTWF4UFZuc29zOTRFMlNkT2JJcTVYYVAvKzlFYWs5TgozMUtWRUkvTQotLS0tLUVORCBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K',
+            'l10n_sa_compliance_csid_json': """{"requestID": 1234567890123, "dispositionMessage": "ISSUED", "binarySecurityToken": "TUlJQ2xUQ0NBanVnQXdJQkFnSUdBWWgydEhlOU1Bb0dDQ3FHU000OUJBTUNNQlV4RXpBUkJnTlZCQU1NQ21WSmJuWnZhV05wYm1jd0hoY05Nak13TmpBeE1URXlOVEV6V2hjTk1qZ3dOVE14TWpFd01EQXdXakNCd1RFTE1Ba0dBMVVFQmhNQ1UwRXhFekFSQmdOVkJBc01Dak14TURFM05UTTVOelF4RXpBUkJnTlZCQW9NQ2xOQklFTnZiWEJoYm5reEV6QVJCZ05WQkFNTUNsTkJJRU52YlhCaGJua3hHREFXQmdOVkJHRU1Eek14TURFM05UTTVOelF3TURBd016RVBNQTBHQTFVRUNBd0dVbWw1WVdSb01VZ3dSZ1lEVlFRSEREL0RtTUtudzVuaWdKN0RtZUtBcHNPWXdxL0RtY1dndzVuaWdLRERtTUtwSU1PWXdxZkRtZUtBbnNPWjRvQ213NW5pZ0tERG1jdUd3NWpDc2NPWXdxa3dWakFRQmdjcWhrak9QUUlCQmdVcmdRUUFDZ05DQUFUdCtKNTlVS1lhcjJ4NjMxakdLQS92cFZGdXA5ZTFSeVkweFR1cDAwOXNtY3ppSElaUFNQTTZxNzRoeFppY2ZOQWh1VWhXVGEwSTRHMXZYV08velI2bW80SE1NSUhKTUF3R0ExVWRFd0VCL3dRQ01BQXdnYmdHQTFVZEVRU0JzRENCcmFTQnFqQ0JwekUwTURJR0ExVUVCQXdyTVMxUFpHOXZmREl0TVRWOE15MVRTVVJKTXkxRFFrMVFVaTFNTWtRNFdDMUxUVEJMVGkxWU5FbFRTakVmTUIwR0NnbVNKb21UOGl4a0FRRU1Eek14TURFM05UTTVOelF3TURBd016RU5NQXNHQTFVRURBd0VNVEV3TURFdk1DMEdBMVVFR2d3bVFXd2dRVzFwY2lCTmIyaGhiVzFsWkNCQ2FXNGdRV0prZFd3Z1FYcHBlaUJUZEhKbFpYUXhEakFNQmdOVkJBOE1CVTkwYUdWeU1Bb0dDQ3FHU000OUJBTUNBMGdBTUVVQ0lRQ2FBNlNKMXBXWDQ4UUE1V1pZVEQ4VmJpODFwZExSY01iZm1NQStZMmNBWlFJZ0NqbXp6Uzh4TnNDWllvWTFoWGIrN3R2NUpKRDVWeUVMR3hER1lyRHFpa2c9", "secret": "dBwSQ1ykNStUO6XRQAQhuDAWAdg/GgNZYNmiwClAGcQ=", "errors": null}""",
+            'l10n_sa_production_csid_json': """{"requestID": 30368, "tokenType": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3", "dispositionMessage": "ISSUED", "binarySecurityToken": "TUlJRDJ6Q0NBNENnQXdJQkFnSVRid0FBZHFEbUlocXNqcG01Q3dBQkFBQjJvREFLQmdncWhrak9QUVFEQWpCak1SVXdFd1lLQ1pJbWlaUHlMR1FCR1JZRmJHOWpZV3d4RXpBUkJnb0praWFKay9Jc1pBRVpGZ05uYjNZeEZ6QVZCZ29Ka2lhSmsvSXNaQUVaRmdkbGVIUm5ZWHAwTVJ3d0dnWURWUVFERXhOVVUxcEZTVTVXVDBsRFJTMVRkV0pEUVMweE1CNFhEVEl5TURNeU9ERTFORFl6TWxvWERUSXlNRE16TURFMU5EWXpNbG93VFRFTE1Ba0dBMVVFQmhNQ1UwRXhEakFNQmdOVkJBb1RCVXBoY21seU1Sb3dHQVlEVlFRTEV4RktaV1JrWVdnZ1FuSmhibU5vTVRJek5ERVNNQkFHQTFVRUF4TUpNVEkzTGpBdU1DNHhNRll3RUFZSEtvWkl6ajBDQVFZRks0RUVBQW9EUWdBRUQvd2IybGhCdkJJQzhDbm5adm91bzZPelJ5bXltVTlOV1JoSXlhTWhHUkVCQ0VaQjRFQVZyQnVWMnhYaXhZNHFCWWY5ZGRlcnprVzlEd2RvM0lsSGdxT0NBaW93Z2dJbU1JR0xCZ05WSFJFRWdZTXdnWUNrZmpCOE1Sd3dHZ1lEVlFRRURCTXlNakl5TWpNeU5EUTBNelF6YW1abU5ETXlNUjh3SFFZS0NaSW1pWlB5TEdRQkFRd1BNekV3TVRjMU16azNOREF3TURBek1RMHdDd1lEVlFRTURBUXhNREV4TVJFd0R3WURWUVFhREFoVFlXMXdiR1VnUlRFWk1CY0dBMVVFRHd3UVUyRnRjR3hsSUVKMWMzTnBibVZ6Y3pBZEJnTlZIUTRFRmdRVWhXY3NiYkpoakQ1WldPa3dCSUxDK3dOVmZLWXdId1lEVlIwakJCZ3dGb0FVZG1DTSt3YWdyR2RYTlozUG1xeW5LNWsxdFM4d1RnWURWUjBmQkVjd1JUQkRvRUdnUDRZOWFIUjBjRG92TDNSemRHTnliQzU2WVhSallTNW5iM1l1YzJFdlEyVnlkRVZ1Y205c2JDOVVVMXBGU1U1V1QwbERSUzFUZFdKRFFTMHhMbU55YkRDQnJRWUlLd1lCQlFVSEFRRUVnYUF3Z1owd2JnWUlLd1lCQlFVSE1BR0dZbWgwZEhBNkx5OTBjM1JqY213dWVtRjBZMkV1WjI5MkxuTmhMME5sY25SRmJuSnZiR3d2VkZOYVJXbHVkbTlwWTJWVFEwRXhMbVY0ZEdkaGVuUXVaMjkyTG14dlkyRnNYMVJUV2tWSlRsWlBTVU5GTFZOMVlrTkJMVEVvTVNrdVkzSjBNQ3NHQ0NzR0FRVUZCekFCaGg5b2RIUndPaTh2ZEhOMFkzSnNMbnBoZEdOaExtZHZkaTV6WVM5dlkzTndNQTRHQTFVZER3RUIvd1FFQXdJSGdEQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBZ1lJS3dZQkJRVUhBd013SndZSkt3WUJCQUdDTnhVS0JCb3dHREFLQmdnckJnRUZCUWNEQWpBS0JnZ3JCZ0VGQlFjREF6QUtCZ2dxaGtqT1BRUURBZ05KQURCR0FpRUF5Tmh5Y1EzYk5sTEZkT1BscVlUNlJWUVRXZ25LMUdoME5IZGNTWTRQZkMwQ0lRQ1NBdGhYdnY3dGV0VUw2OVdqcDhCeG5MTE13ZXJ4WmhCbmV3by9nRjNFSkE9PQ==", "secret": "f9YRhopN/G7x0TECOY6nKSCHLNYlb5riAHSFPICo4qw="}"""
+        })
diff --git a/addons/l10n_sa_edi/models/account_move.py b/addons/l10n_sa_edi/models/account_move.py
new file mode 100644
index 000000000000..45455adf8df1
--- /dev/null
+++ b/addons/l10n_sa_edi/models/account_move.py
@@ -0,0 +1,204 @@
+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'))
diff --git a/addons/l10n_sa_edi/models/account_tax.py b/addons/l10n_sa_edi/models/account_tax.py
new file mode 100644
index 000000000000..665d54f8a8ba
--- /dev/null
+++ b/addons/l10n_sa_edi/models/account_tax.py
@@ -0,0 +1,58 @@
+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
diff --git a/addons/l10n_sa_edi/models/res_company.py b/addons/l10n_sa_edi/models/res_company.py
new file mode 100644
index 000000000000..e5db7e0a7529
--- /dev/null
+++ b/addons/l10n_sa_edi/models/res_company.py
@@ -0,0 +1,93 @@
+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))
diff --git a/addons/l10n_sa_edi/models/res_config_settings.py b/addons/l10n_sa_edi/models/res_config_settings.py
new file mode 100644
index 000000000000..64ed9e43959a
--- /dev/null
+++ b/addons/l10n_sa_edi/models/res_config_settings.py
@@ -0,0 +1,14 @@
+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)
diff --git a/addons/l10n_sa_edi/models/res_partner.py b/addons/l10n_sa_edi/models/res_partner.py
new file mode 100644
index 000000000000..c49d5bda4b1f
--- /dev/null
+++ b/addons/l10n_sa_edi/models/res_partner.py
@@ -0,0 +1,36 @@
+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']
diff --git a/addons/l10n_sa_edi/security/ir.model.access.csv b/addons/l10n_sa_edi/security/ir.model.access.csv
new file mode 100644
index 000000000000..58c6aabd2c87
--- /dev/null
+++ b/addons/l10n_sa_edi/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+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
diff --git a/addons/l10n_sa_edi/static/src/scss/form_view.scss b/addons/l10n_sa_edi/static/src/scss/form_view.scss
new file mode 100644
index 000000000000..dcaad578cb08
--- /dev/null
+++ b/addons/l10n_sa_edi/static/src/scss/form_view.scss
@@ -0,0 +1,21 @@
+.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;
+    }
+  }
+
+}
diff --git a/addons/l10n_sa_edi/tests/__init__.py b/addons/l10n_sa_edi/tests/__init__.py
new file mode 100644
index 000000000000..f64672ea7671
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import common
+from . import test_edi_zatca
diff --git a/addons/l10n_sa_edi/tests/common.py b/addons/l10n_sa_edi/tests/common.py
new file mode 100644
index 000000000000..2eb016509d43
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/common.py
@@ -0,0 +1,245 @@
+# coding: utf-8
+from datetime import datetime
+
+from odoo import Command
+from odoo.tests import tagged
+from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
+
+
+@tagged('post_install_l10n', '-at_install', 'post_install')
+class TestSaEdiCommon(AccountEdiTestCommon):
+
+    @classmethod
+    def setUpClass(cls, chart_template_ref='l10n_sa.sa_chart_template_standard', edi_format_ref='l10n_sa_edi.edi_sa_zatca'):
+        super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
+        # Setup company
+        cls.company = cls.company_data['company']
+        cls.company.name = 'SA Company Test'
+        cls.company.country_id = cls.env.ref('base.sa')
+        cls.company.email = "info@company.saexample.com"
+        cls.company.phone = '+966 51 234 5678'
+        cls.customer_invoice_journal = cls.env['account.journal'].search([('company_id', '=', cls.company.id), ('name', '=', 'Customer Invoices')])
+        cls.company.l10n_sa_edi_building_number = '1234'
+        cls.company.l10n_sa_edi_plot_identification = '1234'
+        cls.company.street2 = "Testomania"
+        cls.company.l10n_sa_additional_identification_number = '2525252525252'
+        cls.company.l10n_sa_additional_identification_scheme = 'CRN'
+        cls.company.vat = '311111111111113'
+        cls.company.l10n_sa_private_key = cls.env['res.company']._l10n_sa_generate_private_key()
+        cls.company.state_id = cls.env['res.country.state'].create({
+            'name': 'Riyadh',
+            'code': 'RYA',
+            'country_id': cls.company.country_id.id
+        })
+        cls.company.street = 'Al Amir Mohammed Bin Abdul Aziz Street'
+        cls.company.city = 'المدينة المنورة'
+        cls.company.zip = '42317'
+        cls.customer_invoice_journal.l10n_sa_serial_number = '123456789'
+        cls.partner_us = cls.env['res.partner'].create({
+            'name': 'Chichi Lboukla',
+            'ref': 'Azure Interior',
+            'street': '4557 De Silva St',
+            'l10n_sa_edi_building_number': '12300',
+            'l10n_sa_edi_plot_identification': '2323',
+            'l10n_sa_additional_identification_scheme': 'CRN',
+            'l10n_sa_additional_identification_number': '353535353535353',
+            'city': 'Fremont',
+            'zip': '94538',
+            'street2': 'Neighbor!',
+            'country_id': cls.env.ref('base.us').id,
+            'state_id': cls.env['res.country.state'].search([('name', '=', 'California')]).id,
+            'email': 'azure.Interior24@example.com',
+            'phone': '(870)-931-0505',
+            'company_type': 'company',
+            'lang': 'en_US',
+        })
+
+        cls.partner_sa = cls.env['res.partner'].create({
+            'name': 'Chichi Lboukla',
+            'ref': 'Azure Interior',
+            'street': '4557 De Silva St',
+            'l10n_sa_edi_building_number': '12300',
+            'l10n_sa_edi_plot_identification': '2323',
+            'l10n_sa_additional_identification_scheme': 'CRN',
+            'l10n_sa_additional_identification_number': '353535353535353',
+            'city': 'Fremont',
+            'zip': '94538',
+            'street2': 'Neighbor!',
+            'country_id': cls.env.ref('base.sa').id,
+            'state_id': cls.env['res.country.state'].search([('name', '=', 'California')]).id,
+            'email': 'azure.Interior24@example.com',
+            'phone': '(870)-931-0505',
+            'company_type': 'company',
+            'lang': 'en_US',
+        })
+
+        cls.partner_sa_simplified = cls.env['res.partner'].create({
+            'name': 'Mohammed Ali',
+            'ref': 'Mohammed Ali',
+            'country_id': cls.env.ref('base.sa').id,
+            'l10n_sa_additional_identification_scheme': 'MOM',
+            'l10n_sa_additional_identification_number': '3123123213131',
+            'state_id': cls.company.state_id.id,
+            'company_type': 'person',
+            'lang': 'en_US',
+        })
+
+        # 15% tax
+        cls.tax_15 = cls.env['account.tax'].search([('company_id', '=', cls.company.id), ('name', '=', 'Sales Tax 15%')])
+
+        # Large cabinet product
+        cls.product_a = cls.env['product.product'].create({
+            'name': 'Product A',
+            'uom_id': cls.env.ref('uom.product_uom_unit').id,
+            'standard_price': 320.0,
+            'default_code': 'P0001',
+        })
+        cls.product_b = cls.env['product.product'].create({
+            'name': 'Product B',
+            'uom_id': cls.env.ref('uom.product_uom_unit').id,
+            'standard_price': 15.8,
+            'default_code': 'P0002',
+        })
+
+        cls.product_burger = cls.env['product.product'].create({
+            'name': 'Burger',
+            'uom_id': cls.env.ref('uom.product_uom_unit').id,
+            'standard_price': 265.00,
+        })
+
+        cls.remove_ubl_extensions_xpath = '''<xpath expr="//*[local-name()='UBLExtensions']" position="replace"/>'''
+
+        cls.invoice_applied_xpath = '''
+            <xpath expr="(//*[local-name()='Invoice']/*[local-name()='ID'])[1]" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            <xpath expr="(//*[local-name()='Invoice']/*[local-name()='UUID'])[1]" position="replace">
+                <UUID>___ignore___</UUID>
+            </xpath>
+            <xpath expr="(//*[local-name()='Contact']/*[local-name()='ID'])[1]" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            <xpath expr="(//*[local-name()='Contact']/*[local-name()='ID'])[2]" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            <xpath expr="//*[local-name()='PaymentMeans']/*[local-name()='InstructionID']" position="replace">
+                <InstructionID>___ignore___</InstructionID>
+            </xpath>
+            <xpath expr="(//*[local-name()='PaymentMeans']/*[local-name()='PaymentID'])" position="replace">
+                <PaymentID>___ignore___</PaymentID>
+            </xpath>
+            <xpath expr="//*[local-name()='InvoiceLine']/*[local-name()='ID']" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            '''
+
+        cls.credit_note_applied_xpath = '''
+            <xpath expr="(//*[local-name()='Invoice']/*[local-name()='ID'])[1]" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            <xpath expr="(//*[local-name()='Invoice']/*[local-name()='UUID'])[1]" position="replace">
+                <UUID>___ignore___</UUID>
+            </xpath>
+            <xpath expr="(//*[local-name()='Contact']/*[local-name()='ID'])[1]" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            <xpath expr="(//*[local-name()='Contact']/*[local-name()='ID'])[2]" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            <xpath expr="(//*[local-name()='OrderReference']/*[local-name()='ID'])[1]" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            <xpath expr="(//*[local-name()='InvoiceDocumentReference']/*[local-name()='ID'])[1]" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            <xpath expr="(//*[local-name()='PaymentMeans']/*[local-name()='InstructionNote'])" position="replace">
+                <InstructionNote>___ignore___</InstructionNote>
+            </xpath>
+            <xpath expr="(//*[local-name()='PaymentMeans']/*[local-name()='PaymentID'])" position="replace">
+                <PaymentID>___ignore___</PaymentID>
+            </xpath>
+            <xpath expr="//*[local-name()='InvoiceLine']/*[local-name()='ID']" position="replace">
+                <ID>___ignore___</ID>
+            </xpath>
+            '''
+
+        cls.debit_note_applied_xpath = '''
+                <xpath expr="(//*[local-name()='Invoice']/*[local-name()='ID'])[1]" position="replace">
+                    <ID>___ignore___</ID>
+                </xpath>
+                <xpath expr="(//*[local-name()='Invoice']/*[local-name()='UUID'])[1]" position="replace">
+                    <UUID>___ignore___</UUID>
+                </xpath>
+                <xpath expr="(//*[local-name()='Contact']/*[local-name()='ID'])[1]" position="replace">
+                    <ID>___ignore___</ID>
+                </xpath>
+                <xpath expr="(//*[local-name()='Contact']/*[local-name()='ID'])[2]" position="replace">
+                    <ID>___ignore___</ID>
+                </xpath>
+                <xpath expr="(//*[local-name()='OrderReference']/*[local-name()='ID'])[1]" position="replace">
+                    <ID>___ignore___</ID>
+                </xpath>
+                <xpath expr="(//*[local-name()='InvoiceDocumentReference']/*[local-name()='ID'])[1]" position="replace">
+                    <ID>___ignore___</ID>
+                </xpath>
+                <xpath expr="//*[local-name()='InvoiceLine']/*[local-name()='ID']" position="replace">
+                    <ID>___ignore___</ID>
+                </xpath>
+                <xpath expr="//*[local-name()='PaymentMeans']/*[local-name()='InstructionID']" position="replace">
+                    <InstructionID>___ignore___</InstructionID>
+                </xpath>
+                <xpath expr="(//*[local-name()='PaymentMeans']/*[local-name()='PaymentID'])" position="replace">
+                    <PaymentID>___ignore___</PaymentID>
+                </xpath>
+                <xpath expr="(//*[local-name()='PaymentMeans']/*[local-name()='InstructionNote'])" position="replace">
+                    <InstructionNote>___ignore___</InstructionNote>
+                </xpath>
+                '''
+
+    def _create_invoice(self, **kwargs):
+        vals = {
+            'name': kwargs['name'],
+            'move_type': 'out_invoice',
+            'company_id': self.company,
+            'partner_id': kwargs['partner_id'],
+            'invoice_date': kwargs['date'],
+            'invoice_date_due': kwargs['date_due'],
+            'currency_id': self.company.currency_id,
+            'invoice_line_ids': [Command.create({
+                'product_id': kwargs['product_id'].id,
+                'price_unit': kwargs['price'],
+                'quantity': kwargs.get('quantity', 1.0),
+                'tax_ids': [Command.set(self.tax_15.ids)],
+            }),
+            ],
+        }
+        move = self.env['account.move'].create(vals)
+        move.state = 'posted'
+        move.l10n_sa_confirmation_datetime = datetime.now()
+        # move.payment_reference = move.name
+        return move
+
+    def _create_debit_note(self, **kwargs):
+        invoice = self._create_invoice(**kwargs)
+
+        debit_note_wizard = self.env['account.debit.note'].with_context(
+            {'active_ids': [invoice.id], 'active_model': 'account.move', 'default_copy_lines': True}).create({
+                'reason': 'Totes forgot'})
+        res = debit_note_wizard.create_debit()
+        debit_note = self.env['account.move'].browse(res['res_id'])
+        debit_note.l10n_sa_confirmation_datetime = datetime.now()
+        debit_note.state = 'posted'
+        return debit_note
+
+    def _create_credit_note(self, **kwargs):
+        move = self._create_invoice(**kwargs)
+        move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=move.ids).create({
+            'reason': 'no reason',
+            'refund_method': 'refund',
+            'journal_id': move.journal_id.id,
+        })
+        reversal = move_reversal.reverse_moves()
+        reverse_move = self.env['account.move'].browse(reversal['res_id'])
+        reverse_move.l10n_sa_confirmation_datetime = datetime.now()
+        reverse_move.state = 'posted'
+        return reverse_move
diff --git a/addons/l10n_sa_edi/tests/compliance/simplified/credit.xml b/addons/l10n_sa_edi/tests/compliance/simplified/credit.xml
new file mode 100644
index 000000000000..5458e9d95856
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/compliance/simplified/credit.xml
@@ -0,0 +1,225 @@
+<Invoice xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2">
+  <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
+  <cbc:ProfileID>reporting:1.0</cbc:ProfileID>
+  <cbc:ID>RINV/2023/00005</cbc:ID>
+  <cbc:UUID>790b6c13-72c1-4dac-9f23-6e9d3b43e151</cbc:UUID>
+  <cbc:IssueDate>2023-03-10</cbc:IssueDate>
+  <cbc:IssueTime>14:59:38</cbc:IssueTime>
+  <cbc:InvoiceTypeCode name="0200000">381</cbc:InvoiceTypeCode>
+  <cbc:DocumentCurrencyCode>SAR</cbc:DocumentCurrencyCode>
+  <cbc:TaxCurrencyCode>SAR</cbc:TaxCurrencyCode>
+  <cbc:BuyerReference>Mohammed Ali</cbc:BuyerReference>
+  <cac:OrderReference>
+    <cbc:ID>Test</cbc:ID>
+  </cac:OrderReference>
+  <cac:BillingReference>
+    <cac:InvoiceDocumentReference>
+      <cbc:ID>INV/2023/00034</cbc:ID>
+    </cac:InvoiceDocumentReference>
+  </cac:BillingReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>QR</cbc:ID>
+    <cac:Attachment>
+      <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">N/A</cbc:EmbeddedDocumentBinaryObject>
+    </cac:Attachment>
+  </cac:AdditionalDocumentReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>PIH</cbc:ID>
+    <cac:Attachment>
+      <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==</cbc:EmbeddedDocumentBinaryObject>
+    </cac:Attachment>
+  </cac:AdditionalDocumentReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>ICV</cbc:ID>
+    <cbc:UUID>0</cbc:UUID>
+  </cac:AdditionalDocumentReference>
+  <cac:Signature>
+    <cbc:ID>urn:oasis:names:specification:ubl:signature:Invoice</cbc:ID>
+    <cbc:SignatureMethod>urn:oasis:names:specification:ubl:dsig:enveloped:xades</cbc:SignatureMethod>
+  </cac:Signature>
+  <cac:AccountingSupplierParty>
+    <cac:Party>
+      <cac:PartyIdentification>
+        <cbc:ID schemeID="CRN">2525252525252</cbc:ID>
+      </cac:PartyIdentification>
+      <cac:PartyName>
+        <cbc:Name>SA Company Test</cbc:Name>
+      </cac:PartyName>
+      <cac:PostalAddress>
+        <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+        <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+        <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+        <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+        <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+        <cbc:PostalZone>42317</cbc:PostalZone>
+        <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+        <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+        <cac:Country>
+          <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+          <cbc:Name>Saudi Arabia</cbc:Name>
+        </cac:Country>
+      </cac:PostalAddress>
+      <cac:PartyTaxScheme>
+        <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+        <cbc:CompanyID>311111111111113</cbc:CompanyID>
+        <cac:RegistrationAddress>
+          <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+          <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+          <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+          <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+          <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+          <cbc:PostalZone>42317</cbc:PostalZone>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:PartyTaxScheme>
+      <cac:PartyLegalEntity>
+        <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+        <cbc:CompanyID>311111111111113</cbc:CompanyID>
+        <cac:RegistrationAddress>
+          <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+          <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+          <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+          <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+          <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+          <cbc:PostalZone>42317</cbc:PostalZone>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+      </cac:PartyLegalEntity>
+      <cac:Contact>
+        <cbc:ID>1</cbc:ID>
+        <cbc:Name>SA Company Test</cbc:Name>
+        <cbc:Telephone>+966 51 234 5678</cbc:Telephone>
+        <cbc:ElectronicMail>info@company.saexample.com</cbc:ElectronicMail>
+      </cac:Contact>
+    </cac:Party>
+  </cac:AccountingSupplierParty>
+  <cac:AccountingCustomerParty>
+    <cac:Party>
+      <cac:PartyIdentification>
+        <cbc:ID schemeID="MOM">3123123213131</cbc:ID>
+      </cac:PartyIdentification>
+      <cac:PartyName>
+        <cbc:Name>Mohammed Ali</cbc:Name>
+      </cac:PartyName>
+      <cac:PostalAddress>
+        <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+        <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+        <cac:Country>
+          <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+          <cbc:Name>Saudi Arabia</cbc:Name>
+        </cac:Country>
+      </cac:PostalAddress>
+      <cac:PartyTaxScheme>
+        <cbc:RegistrationName>Mohammed Ali</cbc:RegistrationName>
+        <cac:RegistrationAddress>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:PartyTaxScheme>
+      <cac:PartyLegalEntity>
+        <cbc:RegistrationName>Mohammed Ali</cbc:RegistrationName>
+        <cac:RegistrationAddress>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+      </cac:PartyLegalEntity>
+      <cac:Contact>
+        <cbc:ID>7</cbc:ID>
+        <cbc:Name>Mohammed Ali</cbc:Name>
+      </cac:Contact>
+    </cac:Party>
+  </cac:AccountingCustomerParty>
+  <cac:Delivery>
+    <cbc:ActualDeliveryDate>2023-03-10</cbc:ActualDeliveryDate>
+  </cac:Delivery>
+  <cac:PaymentMeans>
+    <cbc:PaymentMeansCode listID="UN/ECE 4461">1</cbc:PaymentMeansCode>
+    <cbc:PaymentDueDate>2023-03-10</cbc:PaymentDueDate>
+    <cbc:InstructionNote>Accounting Mistake</cbc:InstructionNote>
+    <cbc:PaymentID>RINV/2023/00005</cbc:PaymentID>
+  </cac:PaymentMeans>
+  <cac:TaxTotal>
+    <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+    <cac:TaxSubtotal>
+      <cbc:TaxableAmount currencyID="SAR">795.00</cbc:TaxableAmount>
+      <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+      <cbc:Percent>15.0</cbc:Percent>
+      <cac:TaxCategory>
+        <cbc:ID>S</cbc:ID>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:TaxCategory>
+    </cac:TaxSubtotal>
+  </cac:TaxTotal>
+  <cac:TaxTotal>
+    <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+  </cac:TaxTotal>
+  <cac:LegalMonetaryTotal>
+    <cbc:LineExtensionAmount currencyID="SAR">795.00</cbc:LineExtensionAmount>
+    <cbc:TaxExclusiveAmount currencyID="SAR">795.00</cbc:TaxExclusiveAmount>
+    <cbc:TaxInclusiveAmount currencyID="SAR">914.25</cbc:TaxInclusiveAmount>
+    <cbc:PrepaidAmount currencyID="SAR">0.00</cbc:PrepaidAmount>
+    <cbc:PayableAmount currencyID="SAR">914.25</cbc:PayableAmount>
+  </cac:LegalMonetaryTotal>
+  <cac:InvoiceLine>
+    <cbc:ID>167</cbc:ID>
+    <cbc:InvoicedQuantity unitCode="C62">3.0</cbc:InvoicedQuantity>
+    <cbc:LineExtensionAmount currencyID="SAR">795.00</cbc:LineExtensionAmount>
+    <cac:TaxTotal>
+      <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+      <cbc:RoundingAmount currencyID="SAR">914.25</cbc:RoundingAmount>
+      <cac:TaxSubtotal>
+        <cbc:TaxableAmount currencyID="SAR">795.00</cbc:TaxableAmount>
+        <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxCategory>
+          <cbc:ID>S</cbc:ID>
+          <cbc:Percent>15.0</cbc:Percent>
+          <cac:TaxScheme>
+            <cbc:ID>VAT</cbc:ID>
+          </cac:TaxScheme>
+        </cac:TaxCategory>
+      </cac:TaxSubtotal>
+    </cac:TaxTotal>
+    <cac:Item>
+      <cbc:Description>Burger</cbc:Description>
+      <cbc:Name>Burger</cbc:Name>
+      <cac:ClassifiedTaxCategory>
+        <cbc:ID>S</cbc:ID>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:ClassifiedTaxCategory>
+    </cac:Item>
+    <cac:Price>
+      <cbc:PriceAmount currencyID="SAR">265.00</cbc:PriceAmount>
+    </cac:Price>
+  </cac:InvoiceLine>
+</Invoice>
diff --git a/addons/l10n_sa_edi/tests/compliance/simplified/debit.xml b/addons/l10n_sa_edi/tests/compliance/simplified/debit.xml
new file mode 100644
index 000000000000..ae57f6ea68ab
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/compliance/simplified/debit.xml
@@ -0,0 +1,226 @@
+<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2">
+  <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
+  <cbc:ProfileID>reporting:1.0</cbc:ProfileID>
+  <cbc:ID>INV/2023/00035</cbc:ID>
+  <cbc:UUID>47fdc8c6-2346-460a-8231-c28bf3bab44c</cbc:UUID>
+  <cbc:IssueDate>2023-03-10</cbc:IssueDate>
+  <cbc:IssueTime>15:01:46</cbc:IssueTime>
+  <cbc:InvoiceTypeCode name="0200000">383</cbc:InvoiceTypeCode>
+  <cbc:DocumentCurrencyCode>SAR</cbc:DocumentCurrencyCode>
+  <cbc:TaxCurrencyCode>SAR</cbc:TaxCurrencyCode>
+  <cbc:BuyerReference>Mohammed Ali</cbc:BuyerReference>
+  <cac:OrderReference>
+    <cbc:ID>Test</cbc:ID>
+  </cac:OrderReference>
+  <cac:BillingReference>
+    <cac:InvoiceDocumentReference>
+      <cbc:ID>INV/2023/00034</cbc:ID>
+    </cac:InvoiceDocumentReference>
+  </cac:BillingReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>QR</cbc:ID>
+    <cac:Attachment>
+      <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">N/A</cbc:EmbeddedDocumentBinaryObject>
+    </cac:Attachment>
+  </cac:AdditionalDocumentReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>PIH</cbc:ID>
+    <cac:Attachment>
+      <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==</cbc:EmbeddedDocumentBinaryObject>
+    </cac:Attachment>
+  </cac:AdditionalDocumentReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>ICV</cbc:ID>
+    <cbc:UUID>0</cbc:UUID>
+  </cac:AdditionalDocumentReference>
+  <cac:Signature>
+    <cbc:ID>urn:oasis:names:specification:ubl:signature:Invoice</cbc:ID>
+    <cbc:SignatureMethod>urn:oasis:names:specification:ubl:dsig:enveloped:xades</cbc:SignatureMethod>
+  </cac:Signature>
+  <cac:AccountingSupplierParty>
+    <cac:Party>
+      <cac:PartyIdentification>
+        <cbc:ID schemeID="CRN">2525252525252</cbc:ID>
+      </cac:PartyIdentification>
+      <cac:PartyName>
+        <cbc:Name>SA Company Test</cbc:Name>
+      </cac:PartyName>
+      <cac:PostalAddress>
+        <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+        <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+        <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+        <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+        <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+        <cbc:PostalZone>42317</cbc:PostalZone>
+        <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+        <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+        <cac:Country>
+          <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+          <cbc:Name>Saudi Arabia</cbc:Name>
+        </cac:Country>
+      </cac:PostalAddress>
+      <cac:PartyTaxScheme>
+        <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+        <cbc:CompanyID>311111111111113</cbc:CompanyID>
+        <cac:RegistrationAddress>
+          <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+          <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+          <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+          <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+          <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+          <cbc:PostalZone>42317</cbc:PostalZone>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:PartyTaxScheme>
+      <cac:PartyLegalEntity>
+        <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+        <cbc:CompanyID>311111111111113</cbc:CompanyID>
+        <cac:RegistrationAddress>
+          <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+          <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+          <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+          <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+          <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+          <cbc:PostalZone>42317</cbc:PostalZone>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+      </cac:PartyLegalEntity>
+      <cac:Contact>
+        <cbc:ID>1</cbc:ID>
+        <cbc:Name>SA Company Test</cbc:Name>
+        <cbc:Telephone>+966 51 234 5678</cbc:Telephone>
+        <cbc:ElectronicMail>info@company.saexample.com</cbc:ElectronicMail>
+      </cac:Contact>
+    </cac:Party>
+  </cac:AccountingSupplierParty>
+  <cac:AccountingCustomerParty>
+    <cac:Party>
+      <cac:PartyIdentification>
+        <cbc:ID schemeID="MOM">3123123213131</cbc:ID>
+      </cac:PartyIdentification>
+      <cac:PartyName>
+        <cbc:Name>Mohammed Ali</cbc:Name>
+      </cac:PartyName>
+      <cac:PostalAddress>
+        <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+        <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+        <cac:Country>
+          <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+          <cbc:Name>Saudi Arabia</cbc:Name>
+        </cac:Country>
+      </cac:PostalAddress>
+      <cac:PartyTaxScheme>
+        <cbc:RegistrationName>Mohammed Ali</cbc:RegistrationName>
+        <cac:RegistrationAddress>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:PartyTaxScheme>
+      <cac:PartyLegalEntity>
+        <cbc:RegistrationName>Mohammed Ali</cbc:RegistrationName>
+        <cac:RegistrationAddress>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+      </cac:PartyLegalEntity>
+      <cac:Contact>
+        <cbc:ID>7</cbc:ID>
+        <cbc:Name>Mohammed Ali</cbc:Name>
+      </cac:Contact>
+    </cac:Party>
+  </cac:AccountingCustomerParty>
+  <cac:Delivery>
+    <cbc:ActualDeliveryDate>2023-03-10</cbc:ActualDeliveryDate>
+  </cac:Delivery>
+  <cac:PaymentMeans>
+    <cbc:PaymentMeansCode listID="UN/ECE 4461">1</cbc:PaymentMeansCode>
+    <cbc:PaymentDueDate>2023-03-10</cbc:PaymentDueDate>
+    <cbc:InstructionID>INV/2023/00035</cbc:InstructionID>
+    <cbc:InstructionNote>More Burgers</cbc:InstructionNote>
+    <cbc:PaymentID>INV/2023/00035</cbc:PaymentID>
+  </cac:PaymentMeans>
+  <cac:TaxTotal>
+    <cbc:TaxAmount currencyID="SAR">79.50</cbc:TaxAmount>
+    <cac:TaxSubtotal>
+      <cbc:TaxableAmount currencyID="SAR">530.00</cbc:TaxableAmount>
+      <cbc:TaxAmount currencyID="SAR">79.50</cbc:TaxAmount>
+      <cbc:Percent>15.0</cbc:Percent>
+      <cac:TaxCategory>
+        <cbc:ID>S</cbc:ID>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:TaxCategory>
+    </cac:TaxSubtotal>
+  </cac:TaxTotal>
+  <cac:TaxTotal>
+    <cbc:TaxAmount currencyID="SAR">79.50</cbc:TaxAmount>
+  </cac:TaxTotal>
+  <cac:LegalMonetaryTotal>
+    <cbc:LineExtensionAmount currencyID="SAR">530.00</cbc:LineExtensionAmount>
+    <cbc:TaxExclusiveAmount currencyID="SAR">530.00</cbc:TaxExclusiveAmount>
+    <cbc:TaxInclusiveAmount currencyID="SAR">609.50</cbc:TaxInclusiveAmount>
+    <cbc:PrepaidAmount currencyID="SAR">0.00</cbc:PrepaidAmount>
+    <cbc:PayableAmount currencyID="SAR">609.50</cbc:PayableAmount>
+  </cac:LegalMonetaryTotal>
+  <cac:InvoiceLine>
+    <cbc:ID>170</cbc:ID>
+    <cbc:InvoicedQuantity unitCode="C62">2.0</cbc:InvoicedQuantity>
+    <cbc:LineExtensionAmount currencyID="SAR">530.00</cbc:LineExtensionAmount>
+    <cac:TaxTotal>
+      <cbc:TaxAmount currencyID="SAR">79.50</cbc:TaxAmount>
+      <cbc:RoundingAmount currencyID="SAR">609.50</cbc:RoundingAmount>
+      <cac:TaxSubtotal>
+        <cbc:TaxableAmount currencyID="SAR">530.00</cbc:TaxableAmount>
+        <cbc:TaxAmount currencyID="SAR">79.50</cbc:TaxAmount>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxCategory>
+          <cbc:ID>S</cbc:ID>
+          <cbc:Percent>15.0</cbc:Percent>
+          <cac:TaxScheme>
+            <cbc:ID>VAT</cbc:ID>
+          </cac:TaxScheme>
+        </cac:TaxCategory>
+      </cac:TaxSubtotal>
+    </cac:TaxTotal>
+    <cac:Item>
+      <cbc:Description>Burger</cbc:Description>
+      <cbc:Name>Burger</cbc:Name>
+      <cac:ClassifiedTaxCategory>
+        <cbc:ID>S</cbc:ID>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:ClassifiedTaxCategory>
+    </cac:Item>
+    <cac:Price>
+      <cbc:PriceAmount currencyID="SAR">265.00</cbc:PriceAmount>
+    </cac:Price>
+  </cac:InvoiceLine>
+</Invoice>
diff --git a/addons/l10n_sa_edi/tests/compliance/simplified/invoice.xml b/addons/l10n_sa_edi/tests/compliance/simplified/invoice.xml
new file mode 100644
index 000000000000..4a0ed4d07cad
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/compliance/simplified/invoice.xml
@@ -0,0 +1,218 @@
+<Invoice xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2">
+  <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
+  <cbc:ProfileID>reporting:1.0</cbc:ProfileID>
+  <cbc:ID>INV/2023/00034</cbc:ID>
+  <cbc:UUID>22b851e5-6fd9-47af-807e-78ef1526667d</cbc:UUID>
+  <cbc:IssueDate>2023-03-10</cbc:IssueDate>
+  <cbc:IssueTime>14:56:55</cbc:IssueTime>
+  <cbc:InvoiceTypeCode name="0200000">388</cbc:InvoiceTypeCode>
+  <cbc:DocumentCurrencyCode>SAR</cbc:DocumentCurrencyCode>
+  <cbc:TaxCurrencyCode>SAR</cbc:TaxCurrencyCode>
+  <cbc:BuyerReference>Mohammed Ali</cbc:BuyerReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>QR</cbc:ID>
+    <cac:Attachment>
+      <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">N/A</cbc:EmbeddedDocumentBinaryObject>
+    </cac:Attachment>
+  </cac:AdditionalDocumentReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>PIH</cbc:ID>
+    <cac:Attachment>
+      <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==</cbc:EmbeddedDocumentBinaryObject>
+    </cac:Attachment>
+  </cac:AdditionalDocumentReference>
+  <cac:AdditionalDocumentReference>
+    <cbc:ID>ICV</cbc:ID>
+    <cbc:UUID>0</cbc:UUID>
+  </cac:AdditionalDocumentReference>
+  <cac:Signature>
+    <cbc:ID>urn:oasis:names:specification:ubl:signature:Invoice</cbc:ID>
+    <cbc:SignatureMethod>urn:oasis:names:specification:ubl:dsig:enveloped:xades</cbc:SignatureMethod>
+  </cac:Signature>
+  <cac:AccountingSupplierParty>
+    <cac:Party>
+      <cac:PartyIdentification>
+        <cbc:ID schemeID="CRN">2525252525252</cbc:ID>
+      </cac:PartyIdentification>
+      <cac:PartyName>
+        <cbc:Name>SA Company Test</cbc:Name>
+      </cac:PartyName>
+      <cac:PostalAddress>
+        <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+        <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+        <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+        <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+        <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+        <cbc:PostalZone>42317</cbc:PostalZone>
+        <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+        <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+        <cac:Country>
+          <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+          <cbc:Name>Saudi Arabia</cbc:Name>
+        </cac:Country>
+      </cac:PostalAddress>
+      <cac:PartyTaxScheme>
+        <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+        <cbc:CompanyID>311111111111113</cbc:CompanyID>
+        <cac:RegistrationAddress>
+          <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+          <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+          <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+          <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+          <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+          <cbc:PostalZone>42317</cbc:PostalZone>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:PartyTaxScheme>
+      <cac:PartyLegalEntity>
+        <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+        <cbc:CompanyID>311111111111113</cbc:CompanyID>
+        <cac:RegistrationAddress>
+          <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+          <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+          <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+          <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+          <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+          <cbc:PostalZone>42317</cbc:PostalZone>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+      </cac:PartyLegalEntity>
+      <cac:Contact>
+        <cbc:ID>1</cbc:ID>
+        <cbc:Name>SA Company Test</cbc:Name>
+        <cbc:Telephone>+966 51 234 5678</cbc:Telephone>
+        <cbc:ElectronicMail>info@company.saexample.com</cbc:ElectronicMail>
+      </cac:Contact>
+    </cac:Party>
+  </cac:AccountingSupplierParty>
+  <cac:AccountingCustomerParty>
+    <cac:Party>
+      <cac:PartyIdentification>
+        <cbc:ID schemeID="MOM">3123123213131</cbc:ID>
+      </cac:PartyIdentification>
+      <cac:PartyName>
+        <cbc:Name>Mohammed Ali</cbc:Name>
+      </cac:PartyName>
+      <cac:PostalAddress>
+        <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+        <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+        <cac:Country>
+          <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+          <cbc:Name>Saudi Arabia</cbc:Name>
+        </cac:Country>
+      </cac:PostalAddress>
+      <cac:PartyTaxScheme>
+        <cbc:RegistrationName>Mohammed Ali</cbc:RegistrationName>
+        <cac:RegistrationAddress>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:PartyTaxScheme>
+      <cac:PartyLegalEntity>
+        <cbc:RegistrationName>Mohammed Ali</cbc:RegistrationName>
+        <cac:RegistrationAddress>
+          <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+          <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+          <cac:Country>
+            <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+            <cbc:Name>Saudi Arabia</cbc:Name>
+          </cac:Country>
+        </cac:RegistrationAddress>
+      </cac:PartyLegalEntity>
+      <cac:Contact>
+        <cbc:ID>7</cbc:ID>
+        <cbc:Name>Mohammed Ali</cbc:Name>
+      </cac:Contact>
+    </cac:Party>
+  </cac:AccountingCustomerParty>
+  <cac:Delivery>
+    <cbc:ActualDeliveryDate>2023-03-10</cbc:ActualDeliveryDate>
+  </cac:Delivery>
+  <cac:PaymentMeans>
+    <cbc:PaymentMeansCode listID="UN/ECE 4461">1</cbc:PaymentMeansCode>
+    <cbc:PaymentDueDate>2023-03-10</cbc:PaymentDueDate>
+    <cbc:InstructionID>INV/2023/00034</cbc:InstructionID>
+    <cbc:PaymentID>INV/2023/00034</cbc:PaymentID>
+  </cac:PaymentMeans>
+  <cac:TaxTotal>
+    <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+    <cac:TaxSubtotal>
+      <cbc:TaxableAmount currencyID="SAR">795.00</cbc:TaxableAmount>
+      <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+      <cbc:Percent>15.0</cbc:Percent>
+      <cac:TaxCategory>
+        <cbc:ID>S</cbc:ID>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:TaxCategory>
+    </cac:TaxSubtotal>
+  </cac:TaxTotal>
+  <cac:TaxTotal>
+    <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+  </cac:TaxTotal>
+  <cac:LegalMonetaryTotal>
+    <cbc:LineExtensionAmount currencyID="SAR">795.00</cbc:LineExtensionAmount>
+    <cbc:TaxExclusiveAmount currencyID="SAR">795.00</cbc:TaxExclusiveAmount>
+    <cbc:TaxInclusiveAmount currencyID="SAR">914.25</cbc:TaxInclusiveAmount>
+    <cbc:PrepaidAmount currencyID="SAR">0.00</cbc:PrepaidAmount>
+    <cbc:PayableAmount currencyID="SAR">914.25</cbc:PayableAmount>
+  </cac:LegalMonetaryTotal>
+  <cac:InvoiceLine>
+    <cbc:ID>164</cbc:ID>
+    <cbc:InvoicedQuantity unitCode="C62">3.0</cbc:InvoicedQuantity>
+    <cbc:LineExtensionAmount currencyID="SAR">795.00</cbc:LineExtensionAmount>
+    <cac:TaxTotal>
+      <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+      <cbc:RoundingAmount currencyID="SAR">914.25</cbc:RoundingAmount>
+      <cac:TaxSubtotal>
+        <cbc:TaxableAmount currencyID="SAR">795.00</cbc:TaxableAmount>
+        <cbc:TaxAmount currencyID="SAR">119.25</cbc:TaxAmount>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxCategory>
+          <cbc:ID>S</cbc:ID>
+          <cbc:Percent>15.0</cbc:Percent>
+          <cac:TaxScheme>
+            <cbc:ID>VAT</cbc:ID>
+          </cac:TaxScheme>
+        </cac:TaxCategory>
+      </cac:TaxSubtotal>
+    </cac:TaxTotal>
+    <cac:Item>
+      <cbc:Description>Burger</cbc:Description>
+      <cbc:Name>Burger</cbc:Name>
+      <cac:ClassifiedTaxCategory>
+        <cbc:ID>S</cbc:ID>
+        <cbc:Percent>15.0</cbc:Percent>
+        <cac:TaxScheme>
+          <cbc:ID>VAT</cbc:ID>
+        </cac:TaxScheme>
+      </cac:ClassifiedTaxCategory>
+    </cac:Item>
+    <cac:Price>
+      <cbc:PriceAmount currencyID="SAR">265.00</cbc:PriceAmount>
+    </cac:Price>
+  </cac:InvoiceLine>
+
+</Invoice>
diff --git a/addons/l10n_sa_edi/tests/compliance/standard/credit.xml b/addons/l10n_sa_edi/tests/compliance/standard/credit.xml
new file mode 100644
index 000000000000..d1c023fe450c
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/compliance/standard/credit.xml
@@ -0,0 +1,222 @@
+<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
+    xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+    xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+    xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2">
+    <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
+    <cbc:ProfileID>reporting:1.0</cbc:ProfileID>
+    <cbc:ID>RINV/2022/00006</cbc:ID>
+    <cbc:UUID>6c49b8e0-2ce5-11ed-b6c7-c54ae37ec60b</cbc:UUID>
+    <cbc:IssueDate>2022-09-05</cbc:IssueDate>
+    <cbc:IssueTime>09:39:15</cbc:IssueTime>
+    <cbc:InvoiceTypeCode name="0100100">381</cbc:InvoiceTypeCode>
+    <cbc:DocumentCurrencyCode>SAR</cbc:DocumentCurrencyCode>
+    <cbc:TaxCurrencyCode>SAR</cbc:TaxCurrencyCode>
+    <cbc:BuyerReference>Azure Interior</cbc:BuyerReference>
+    <cac:OrderReference>
+        <cbc:ID>test</cbc:ID>
+    </cac:OrderReference>
+    <cac:BillingReference>
+        <cac:InvoiceDocumentReference>
+            <cbc:ID>test</cbc:ID>
+        </cac:InvoiceDocumentReference>
+    </cac:BillingReference>
+    <cac:AdditionalDocumentReference>
+        <cbc:ID>PIH</cbc:ID>
+        <cac:Attachment>
+            <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">
+                NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==</cbc:EmbeddedDocumentBinaryObject>
+        </cac:Attachment>
+    </cac:AdditionalDocumentReference>
+    <cac:AdditionalDocumentReference>
+        <cbc:ID>ICV</cbc:ID>
+        <cbc:UUID>137</cbc:UUID>
+    </cac:AdditionalDocumentReference>
+    <cac:AccountingSupplierParty>
+        <cac:Party>
+            <cac:PartyIdentification>
+                <cbc:ID schemeID="CRN">2525252525252</cbc:ID>
+            </cac:PartyIdentification>
+            <cac:PartyName>
+                <cbc:Name>SA Company Test</cbc:Name>
+            </cac:PartyName>
+            <cac:PostalAddress>
+                <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                <cbc:PostalZone>42317</cbc:PostalZone>
+                <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                <cac:Country>
+                    <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                    <cbc:Name>Saudi Arabia</cbc:Name>
+                </cac:Country>
+            </cac:PostalAddress>
+            <cac:PartyTaxScheme>
+                <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+                <cbc:CompanyID>311111111111113</cbc:CompanyID>
+                <cac:RegistrationAddress>
+                    <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                    <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                    <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                    <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                    <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                    <cbc:PostalZone>42317</cbc:PostalZone>
+                    <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                    <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                    <cac:Country>
+                        <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                        <cbc:Name>Saudi Arabia</cbc:Name>
+                    </cac:Country>
+                </cac:RegistrationAddress>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:PartyTaxScheme>
+            <cac:PartyLegalEntity>
+                <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+                <cbc:CompanyID>311111111111113</cbc:CompanyID>
+                <cac:RegistrationAddress>
+                    <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                    <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                    <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                    <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                    <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                    <cbc:PostalZone>42317</cbc:PostalZone>
+                    <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                    <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                    <cac:Country>
+                        <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                        <cbc:Name>Saudi Arabia</cbc:Name>
+                    </cac:Country>
+                </cac:RegistrationAddress>
+            </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:ID>337</cbc:ID>
+                <cbc:Name>SA Company Test</cbc:Name>
+                <cbc:Telephone>+966 51 234 5678</cbc:Telephone>
+                <cbc:ElectronicMail>info@company.saexample.com</cbc:ElectronicMail>
+            </cac:Contact>
+        </cac:Party>
+    </cac:AccountingSupplierParty>
+    <cac:AccountingCustomerParty>
+        <cac:Party>
+            <cac:PartyIdentification>
+                <cbc:ID schemeID="CRN">353535353535353</cbc:ID>
+            </cac:PartyIdentification>
+            <cac:PartyName>
+                <cbc:Name>Chichi Lboukla</cbc:Name>
+            </cac:PartyName>
+            <cac:PostalAddress>
+                <cbc:StreetName>4557 De Silva St</cbc:StreetName>
+                <cbc:BuildingNumber>12300</cbc:BuildingNumber>
+                <cbc:PlotIdentification>2323</cbc:PlotIdentification>
+                <cbc:CitySubdivisionName>Neighbor!</cbc:CitySubdivisionName>
+                <cbc:CityName>Fremont</cbc:CityName>
+                <cbc:PostalZone>94538</cbc:PostalZone>
+                <cbc:CountrySubentity>California</cbc:CountrySubentity>
+                <cbc:CountrySubentityCode>CA</cbc:CountrySubentityCode>
+                <cac:Country>
+                    <cbc:IdentificationCode>US</cbc:IdentificationCode>
+                    <cbc:Name>United States</cbc:Name>
+                </cac:Country>
+            </cac:PostalAddress>
+            <cac:PartyLegalEntity>
+                <cbc:RegistrationName>Chichi Lboukla</cbc:RegistrationName>
+                <cac:RegistrationAddress>
+                    <cbc:StreetName>4557 De Silva St</cbc:StreetName>
+                    <cbc:BuildingNumber>12300</cbc:BuildingNumber>
+                    <cbc:PlotIdentification>2323</cbc:PlotIdentification>
+                    <cbc:CitySubdivisionName>Neighbor!</cbc:CitySubdivisionName>
+                    <cbc:CityName>Fremont</cbc:CityName>
+                    <cbc:PostalZone>94538</cbc:PostalZone>
+                    <cbc:CountrySubentity>California</cbc:CountrySubentity>
+                    <cbc:CountrySubentityCode>CA</cbc:CountrySubentityCode>
+                    <cac:Country>
+                        <cbc:IdentificationCode>US</cbc:IdentificationCode>
+                        <cbc:Name>United States</cbc:Name>
+                    </cac:Country>
+                </cac:RegistrationAddress>
+            </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:ID>340</cbc:ID>
+                <cbc:Name>Chichi Lboukla</cbc:Name>
+                <cbc:Telephone>(870)-931-0505</cbc:Telephone>
+                <cbc:ElectronicMail>azure.Interior24@example.com</cbc:ElectronicMail>
+            </cac:Contact>
+        </cac:Party>
+    </cac:AccountingCustomerParty>
+    <cac:Delivery>
+      <cbc:ActualDeliveryDate>2022-09-05</cbc:ActualDeliveryDate>
+    </cac:Delivery>
+    <cac:PaymentMeans>
+        <cbc:PaymentMeansCode listID="UN/ECE 4461">1</cbc:PaymentMeansCode>
+        <cbc:PaymentDueDate>2022-09-05</cbc:PaymentDueDate>
+        <cbc:InstructionNote>Ttest</cbc:InstructionNote>
+        <cbc:PaymentID>RINV/2022/00002</cbc:PaymentID>
+    </cac:PaymentMeans>
+    <cac:TaxTotal>
+        <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+        <cac:TaxSubtotal>
+            <cbc:TaxableAmount currencyID="SAR">320.00</cbc:TaxableAmount>
+            <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+            <cbc:Percent>15.0</cbc:Percent>
+            <cac:TaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:TaxCategory>
+        </cac:TaxSubtotal>
+    </cac:TaxTotal>
+    <cac:TaxTotal>
+        <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+    </cac:TaxTotal>
+    <cac:LegalMonetaryTotal>
+        <cbc:LineExtensionAmount currencyID="SAR">320.00</cbc:LineExtensionAmount>
+        <cbc:TaxExclusiveAmount currencyID="SAR">320.00</cbc:TaxExclusiveAmount>
+        <cbc:TaxInclusiveAmount currencyID="SAR">368.00</cbc:TaxInclusiveAmount>
+        <cbc:PrepaidAmount currencyID="SAR">0.00</cbc:PrepaidAmount>
+        <cbc:PayableAmount currencyID="SAR">368.00</cbc:PayableAmount>
+    </cac:LegalMonetaryTotal>
+    <cac:InvoiceLine>
+        <cbc:ID>390</cbc:ID>
+        <cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
+        <cbc:LineExtensionAmount currencyID="SAR">320.00</cbc:LineExtensionAmount>
+        <cac:TaxTotal>
+            <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+            <cbc:RoundingAmount currencyID="SAR">368.00</cbc:RoundingAmount>
+            <cac:TaxSubtotal>
+                <cbc:TaxableAmount currencyID="SAR">320.00</cbc:TaxableAmount>
+                <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+                </cac:TaxCategory>
+            </cac:TaxSubtotal>
+        </cac:TaxTotal>
+        <cac:Item>
+            <cbc:Description>[P0001] Product A</cbc:Description>
+            <cbc:Name>Product A</cbc:Name>
+            <cac:SellersItemIdentification>
+                <cbc:ID>P0001</cbc:ID>
+            </cac:SellersItemIdentification>
+            <cac:ClassifiedTaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:ClassifiedTaxCategory>
+        </cac:Item>
+        <cac:Price>
+            <cbc:PriceAmount currencyID="SAR">320.00</cbc:PriceAmount>
+        </cac:Price>
+    </cac:InvoiceLine>
+</Invoice>
diff --git a/addons/l10n_sa_edi/tests/compliance/standard/debit.xml b/addons/l10n_sa_edi/tests/compliance/standard/debit.xml
new file mode 100644
index 000000000000..5ede0d7ac44e
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/compliance/standard/debit.xml
@@ -0,0 +1,223 @@
+<Invoice 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">
+    <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
+    <cbc:ProfileID>reporting:1.0</cbc:ProfileID>
+    <cbc:ID>INV/2022/00015</cbc:ID>
+    <cbc:UUID>4dfa4796-2ce6-11ed-b6c7-c54ae37ec60b</cbc:UUID>
+    <cbc:IssueDate>2022-09-05</cbc:IssueDate>
+    <cbc:IssueTime>09:45:27</cbc:IssueTime>
+    <cbc:InvoiceTypeCode name="0100100">383</cbc:InvoiceTypeCode>
+    <cbc:DocumentCurrencyCode>SAR</cbc:DocumentCurrencyCode>
+    <cbc:TaxCurrencyCode>SAR</cbc:TaxCurrencyCode>
+    <cbc:BuyerReference>Azure Interior</cbc:BuyerReference>
+    <cac:OrderReference>
+        <cbc:ID>INV/2022/00014, Totes forgot</cbc:ID>
+    </cac:OrderReference>
+    <cac:BillingReference>
+        <cac:InvoiceDocumentReference>
+            <cbc:ID>INV/2022/00014</cbc:ID>
+        </cac:InvoiceDocumentReference>
+    </cac:BillingReference>
+    <cac:AdditionalDocumentReference>
+        <cbc:ID>PIH</cbc:ID>
+        <cac:Attachment>
+            <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">
+                NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==</cbc:EmbeddedDocumentBinaryObject>
+        </cac:Attachment>
+    </cac:AdditionalDocumentReference>
+    <cac:AdditionalDocumentReference>
+        <cbc:ID>ICV</cbc:ID>
+        <cbc:UUID>138</cbc:UUID>
+    </cac:AdditionalDocumentReference>
+    <cac:AccountingSupplierParty>
+        <cac:Party>
+            <cac:PartyIdentification>
+                <cbc:ID schemeID="CRN">2525252525252</cbc:ID>
+            </cac:PartyIdentification>
+            <cac:PartyName>
+                <cbc:Name>SA Company Test</cbc:Name>
+            </cac:PartyName>
+            <cac:PostalAddress>
+                <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                <cbc:PostalZone>42317</cbc:PostalZone>
+                <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                <cac:Country>
+                    <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                    <cbc:Name>Saudi Arabia</cbc:Name>
+                </cac:Country>
+            </cac:PostalAddress>
+            <cac:PartyTaxScheme>
+                <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+                <cbc:CompanyID>311111111111113</cbc:CompanyID>
+                <cac:RegistrationAddress>
+                <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                <cbc:PostalZone>42317</cbc:PostalZone>
+                <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                <cac:Country>
+                    <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                    <cbc:Name>Saudi Arabia</cbc:Name>
+                </cac:Country>
+                </cac:RegistrationAddress>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:PartyTaxScheme>
+            <cac:PartyLegalEntity>
+                <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+                <cbc:CompanyID>311111111111113</cbc:CompanyID>
+                <cac:RegistrationAddress>
+                <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                <cbc:PostalZone>42317</cbc:PostalZone>
+                <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                <cac:Country>
+                    <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                    <cbc:Name>Saudi Arabia</cbc:Name>
+                </cac:Country>
+                </cac:RegistrationAddress>
+            </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:ID>547</cbc:ID>
+                <cbc:Name>SA Company Test</cbc:Name>
+                <cbc:Telephone>+966 51 234 5678</cbc:Telephone>
+                <cbc:ElectronicMail>info@company.saexample.com</cbc:ElectronicMail>
+            </cac:Contact>
+        </cac:Party>
+    </cac:AccountingSupplierParty>
+    <cac:AccountingCustomerParty>
+        <cac:Party>
+            <cac:PartyIdentification>
+                <cbc:ID schemeID="CRN">353535353535353</cbc:ID>
+            </cac:PartyIdentification>
+            <cac:PartyName>
+                <cbc:Name>Chichi Lboukla</cbc:Name>
+            </cac:PartyName>
+            <cac:PostalAddress>
+                <cbc:StreetName>4557 De Silva St</cbc:StreetName>
+                <cbc:BuildingNumber>12300</cbc:BuildingNumber>
+                <cbc:PlotIdentification>2323</cbc:PlotIdentification>
+                <cbc:CitySubdivisionName>Neighbor!</cbc:CitySubdivisionName>
+                <cbc:CityName>Fremont</cbc:CityName>
+                <cbc:PostalZone>94538</cbc:PostalZone>
+                <cbc:CountrySubentity>California</cbc:CountrySubentity>
+                <cbc:CountrySubentityCode>CA</cbc:CountrySubentityCode>
+                <cac:Country>
+                    <cbc:IdentificationCode>US</cbc:IdentificationCode>
+                    <cbc:Name>United States</cbc:Name>
+                </cac:Country>
+            </cac:PostalAddress>
+            <cac:PartyLegalEntity>
+                <cbc:RegistrationName>Chichi Lboukla</cbc:RegistrationName>
+                <cac:RegistrationAddress>
+                    <cbc:StreetName>4557 De Silva St</cbc:StreetName>
+                    <cbc:BuildingNumber>12300</cbc:BuildingNumber>
+                    <cbc:PlotIdentification>2323</cbc:PlotIdentification>
+                    <cbc:CitySubdivisionName>Neighbor!</cbc:CitySubdivisionName>
+                    <cbc:CityName>Fremont</cbc:CityName>
+                    <cbc:PostalZone>94538</cbc:PostalZone>
+                    <cbc:CountrySubentity>California</cbc:CountrySubentity>
+                    <cbc:CountrySubentityCode>CA</cbc:CountrySubentityCode>
+                    <cac:Country>
+                        <cbc:IdentificationCode>US</cbc:IdentificationCode>
+                        <cbc:Name>United States</cbc:Name>
+                    </cac:Country>
+                </cac:RegistrationAddress>
+            </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:ID>550</cbc:ID>
+                <cbc:Name>Chichi Lboukla</cbc:Name>
+                <cbc:Telephone>(870)-931-0505</cbc:Telephone>
+                <cbc:ElectronicMail>azure.Interior24@example.com</cbc:ElectronicMail>
+            </cac:Contact>
+        </cac:Party>
+    </cac:AccountingCustomerParty>
+    <cac:Delivery>
+      <cbc:ActualDeliveryDate>2022-09-05</cbc:ActualDeliveryDate>
+    </cac:Delivery>
+    <cac:PaymentMeans>
+        <cbc:PaymentMeansCode listID="UN/ECE 4461">1</cbc:PaymentMeansCode>
+        <cbc:PaymentDueDate>2022-09-05</cbc:PaymentDueDate>
+        <cbc:InstructionID>INV/2022/00015</cbc:InstructionID>
+        <cbc:InstructionNote>INV/2022/00015, Totes forgot</cbc:InstructionNote>
+        <cbc:PaymentID>INV/2022/00015</cbc:PaymentID>
+    </cac:PaymentMeans>
+    <cac:TaxTotal>
+        <cbc:TaxAmount currencyID="SAR">2.37</cbc:TaxAmount>
+        <cac:TaxSubtotal>
+            <cbc:TaxableAmount currencyID="SAR">15.80</cbc:TaxableAmount>
+            <cbc:TaxAmount currencyID="SAR">2.37</cbc:TaxAmount>
+            <cbc:Percent>15.0</cbc:Percent>
+            <cac:TaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:TaxCategory>
+        </cac:TaxSubtotal>
+    </cac:TaxTotal>
+    <cac:TaxTotal>
+        <cbc:TaxAmount currencyID="SAR">2.37</cbc:TaxAmount>
+    </cac:TaxTotal>
+    <cac:LegalMonetaryTotal>
+        <cbc:LineExtensionAmount currencyID="SAR">15.80</cbc:LineExtensionAmount>
+        <cbc:TaxExclusiveAmount currencyID="SAR">15.80</cbc:TaxExclusiveAmount>
+        <cbc:TaxInclusiveAmount currencyID="SAR">18.17</cbc:TaxInclusiveAmount>
+        <cbc:PrepaidAmount currencyID="SAR">0.00</cbc:PrepaidAmount>
+        <cbc:PayableAmount currencyID="SAR">18.17</cbc:PayableAmount>
+    </cac:LegalMonetaryTotal>
+    <cac:InvoiceLine>
+        <cbc:ID>393</cbc:ID>
+        <cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
+        <cbc:LineExtensionAmount currencyID="SAR">15.80</cbc:LineExtensionAmount>
+        <cac:TaxTotal>
+            <cbc:TaxAmount currencyID="SAR">2.37</cbc:TaxAmount>
+            <cbc:RoundingAmount currencyID="SAR">18.17</cbc:RoundingAmount>
+            <cac:TaxSubtotal>
+                <cbc:TaxableAmount currencyID="SAR">15.80</cbc:TaxableAmount>
+                <cbc:TaxAmount currencyID="SAR">2.37</cbc:TaxAmount>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+                </cac:TaxCategory>
+            </cac:TaxSubtotal>
+        </cac:TaxTotal>
+        <cac:Item>
+            <cbc:Description>[P0002] Product B</cbc:Description>
+            <cbc:Name>Product B</cbc:Name>
+            <cac:SellersItemIdentification>
+                <cbc:ID>P0002</cbc:ID>
+            </cac:SellersItemIdentification>
+            <cac:ClassifiedTaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:ClassifiedTaxCategory>
+        </cac:Item>
+        <cac:Price>
+            <cbc:PriceAmount currencyID="SAR">15.80</cbc:PriceAmount>
+        </cac:Price>
+    </cac:InvoiceLine>
+</Invoice>
diff --git a/addons/l10n_sa_edi/tests/compliance/standard/invoice.xml b/addons/l10n_sa_edi/tests/compliance/standard/invoice.xml
new file mode 100644
index 000000000000..a34768f15e78
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/compliance/standard/invoice.xml
@@ -0,0 +1,213 @@
+<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
+    xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
+    xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
+    xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2">
+    <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
+    <cbc:ProfileID>reporting:1.0</cbc:ProfileID>
+    <cbc:ID>INV/2022/00014</cbc:ID>
+    <cbc:UUID>ff608a28-096e-44a1-a896-cbb52212a8a3</cbc:UUID>
+    <cbc:IssueDate>2022-09-05</cbc:IssueDate>
+    <cbc:IssueTime>08:20:02</cbc:IssueTime>
+    <cbc:InvoiceTypeCode name="0100100">388</cbc:InvoiceTypeCode>
+    <cbc:DocumentCurrencyCode>SAR</cbc:DocumentCurrencyCode>
+    <cbc:TaxCurrencyCode>SAR</cbc:TaxCurrencyCode>
+    <cbc:BuyerReference>Azure Interior</cbc:BuyerReference>
+    <cac:AdditionalDocumentReference>
+        <cbc:ID>PIH</cbc:ID>
+        <cac:Attachment>
+            <cbc:EmbeddedDocumentBinaryObject mimeCode="text/plain">NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==</cbc:EmbeddedDocumentBinaryObject>
+        </cac:Attachment>
+    </cac:AdditionalDocumentReference>
+    <cac:AdditionalDocumentReference>
+        <cbc:ID>ICV</cbc:ID>
+        <cbc:UUID>0</cbc:UUID>
+    </cac:AdditionalDocumentReference>
+    <cac:AccountingSupplierParty>
+        <cac:Party>
+            <cac:PartyIdentification>
+                <cbc:ID schemeID="CRN">2525252525252</cbc:ID>
+            </cac:PartyIdentification>
+            <cac:PartyName>
+                <cbc:Name>SA Company Test</cbc:Name>
+            </cac:PartyName>
+            <cac:PostalAddress>
+                <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                <cbc:PostalZone>42317</cbc:PostalZone>
+                <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                <cac:Country>
+                    <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                    <cbc:Name>Saudi Arabia</cbc:Name>
+                </cac:Country>
+            </cac:PostalAddress>
+            <cac:PartyTaxScheme>
+                <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+                <cbc:CompanyID>311111111111113</cbc:CompanyID>
+                <cac:RegistrationAddress>
+                    <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                    <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                    <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                    <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                    <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                    <cbc:PostalZone>42317</cbc:PostalZone>
+                    <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                    <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                    <cac:Country>
+                        <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                        <cbc:Name>Saudi Arabia</cbc:Name>
+                    </cac:Country>
+                </cac:RegistrationAddress>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:PartyTaxScheme>
+            <cac:PartyLegalEntity>
+                <cbc:RegistrationName>SA Company Test</cbc:RegistrationName>
+                <cbc:CompanyID>311111111111113</cbc:CompanyID>
+                <cac:RegistrationAddress>
+                    <cbc:StreetName>Al Amir Mohammed Bin Abdul Aziz Street</cbc:StreetName>
+                    <cbc:BuildingNumber>1234</cbc:BuildingNumber>
+                    <cbc:PlotIdentification>1234</cbc:PlotIdentification>
+                    <cbc:CitySubdivisionName>Testomania</cbc:CitySubdivisionName>
+                    <cbc:CityName>&#1575;&#1604;&#1605;&#1583;&#1610;&#1606;&#1577; &#1575;&#1604;&#1605;&#1606;&#1608;&#1585;&#1577;</cbc:CityName>
+                    <cbc:PostalZone>42317</cbc:PostalZone>
+                    <cbc:CountrySubentity>Riyadh</cbc:CountrySubentity>
+                    <cbc:CountrySubentityCode>RYA</cbc:CountrySubentityCode>
+                    <cac:Country>
+                        <cbc:IdentificationCode>SA</cbc:IdentificationCode>
+                        <cbc:Name>Saudi Arabia</cbc:Name>
+                    </cac:Country>
+                </cac:RegistrationAddress>
+            </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:ID>33</cbc:ID>
+                <cbc:Name>SA Company Test</cbc:Name>
+                <cbc:Telephone>+966 51 234 5678</cbc:Telephone>
+                <cbc:ElectronicMail>info@company.saexample.com</cbc:ElectronicMail>
+            </cac:Contact>
+        </cac:Party>
+    </cac:AccountingSupplierParty>
+    <cac:AccountingCustomerParty>
+        <cac:Party>
+            <cac:PartyIdentification>
+                <cbc:ID schemeID="CRN">353535353535353</cbc:ID>
+            </cac:PartyIdentification>
+            <cac:PartyName>
+                <cbc:Name>Chichi Lboukla</cbc:Name>
+            </cac:PartyName>
+            <cac:PostalAddress>
+                <cbc:StreetName>4557 De Silva St</cbc:StreetName>
+                <cbc:BuildingNumber>12300</cbc:BuildingNumber>
+                <cbc:PlotIdentification>2323</cbc:PlotIdentification>
+                <cbc:CitySubdivisionName>Neighbor!</cbc:CitySubdivisionName>
+                <cbc:CityName>Fremont</cbc:CityName>
+                <cbc:PostalZone>94538</cbc:PostalZone>
+                <cbc:CountrySubentity>California</cbc:CountrySubentity>
+                <cbc:CountrySubentityCode>CA</cbc:CountrySubentityCode>
+                <cac:Country>
+                    <cbc:IdentificationCode>US</cbc:IdentificationCode>
+                    <cbc:Name>United States</cbc:Name>
+                </cac:Country>
+            </cac:PostalAddress>
+            <cac:PartyLegalEntity>
+                <cbc:RegistrationName>Chichi Lboukla</cbc:RegistrationName>
+                <cac:RegistrationAddress>
+                    <cbc:StreetName>4557 De Silva St</cbc:StreetName>
+                    <cbc:BuildingNumber>12300</cbc:BuildingNumber>
+                    <cbc:PlotIdentification>2323</cbc:PlotIdentification>
+                    <cbc:CitySubdivisionName>Neighbor!</cbc:CitySubdivisionName>
+                    <cbc:CityName>Fremont</cbc:CityName>
+                    <cbc:PostalZone>94538</cbc:PostalZone>
+                    <cbc:CountrySubentity>California</cbc:CountrySubentity>
+                    <cbc:CountrySubentityCode>CA</cbc:CountrySubentityCode>
+                    <cac:Country>
+                        <cbc:IdentificationCode>US</cbc:IdentificationCode>
+                        <cbc:Name>United States</cbc:Name>
+                    </cac:Country>
+                </cac:RegistrationAddress>
+            </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:ID>42</cbc:ID>
+                <cbc:Name>Chichi Lboukla</cbc:Name>
+                <cbc:Telephone>(870)-931-0505</cbc:Telephone>
+                <cbc:ElectronicMail>azure.Interior24@example.com</cbc:ElectronicMail>
+            </cac:Contact>
+        </cac:Party>
+    </cac:AccountingCustomerParty>
+    <cac:Delivery>
+      <cbc:ActualDeliveryDate>2022-09-05</cbc:ActualDeliveryDate>
+    </cac:Delivery>
+    <cac:PaymentMeans>
+        <cbc:PaymentMeansCode listID="UN/ECE 4461">1</cbc:PaymentMeansCode>
+        <cbc:PaymentDueDate>2022-09-22</cbc:PaymentDueDate>
+        <cbc:InstructionID>INV/2022/00014</cbc:InstructionID>
+        <cbc:PaymentID>INV/2022/00014</cbc:PaymentID>
+    </cac:PaymentMeans>
+    <cac:TaxTotal>
+        <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+        <cac:TaxSubtotal>
+            <cbc:TaxableAmount currencyID="SAR">320.00</cbc:TaxableAmount>
+            <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+            <cbc:Percent>15.0</cbc:Percent>
+            <cac:TaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:TaxCategory>
+        </cac:TaxSubtotal>
+    </cac:TaxTotal>
+    <cac:TaxTotal>
+        <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+    </cac:TaxTotal>
+    <cac:LegalMonetaryTotal>
+        <cbc:LineExtensionAmount currencyID="SAR">320.00</cbc:LineExtensionAmount>
+        <cbc:TaxExclusiveAmount currencyID="SAR">320.00</cbc:TaxExclusiveAmount>
+        <cbc:TaxInclusiveAmount currencyID="SAR">368.00</cbc:TaxInclusiveAmount>
+        <cbc:PrepaidAmount currencyID="SAR">0.00</cbc:PrepaidAmount>
+        <cbc:PayableAmount currencyID="SAR">368.00</cbc:PayableAmount>
+    </cac:LegalMonetaryTotal>
+    <cac:InvoiceLine>
+        <cbc:ID>384</cbc:ID>
+        <cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
+        <cbc:LineExtensionAmount currencyID="SAR">320.00</cbc:LineExtensionAmount>
+        <cac:TaxTotal>
+            <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+            <cbc:RoundingAmount currencyID="SAR">368.00</cbc:RoundingAmount>
+            <cac:TaxSubtotal>
+                <cbc:TaxableAmount currencyID="SAR">320.00</cbc:TaxableAmount>
+                <cbc:TaxAmount currencyID="SAR">48.00</cbc:TaxAmount>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+                </cac:TaxCategory>
+            </cac:TaxSubtotal>
+        </cac:TaxTotal>
+        <cac:Item>
+            <cbc:Description>[P0001] Product A</cbc:Description>
+            <cbc:Name>Product A</cbc:Name>
+            <cac:SellersItemIdentification>
+                <cbc:ID>P0001</cbc:ID>
+            </cac:SellersItemIdentification>
+            <cac:ClassifiedTaxCategory>
+                <cbc:ID>S</cbc:ID>
+                <cbc:Percent>15.0</cbc:Percent>
+                <cac:TaxScheme>
+                    <cbc:ID>VAT</cbc:ID>
+                </cac:TaxScheme>
+            </cac:ClassifiedTaxCategory>
+        </cac:Item>
+        <cac:Price>
+            <cbc:PriceAmount currencyID="SAR">320.00</cbc:PriceAmount>
+        </cac:Price>
+    </cac:InvoiceLine>
+</Invoice>
diff --git a/addons/l10n_sa_edi/tests/test_edi_zatca.py b/addons/l10n_sa_edi/tests/test_edi_zatca.py
new file mode 100644
index 000000000000..fc79a3c7e2de
--- /dev/null
+++ b/addons/l10n_sa_edi/tests/test_edi_zatca.py
@@ -0,0 +1,124 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from datetime import datetime
+from freezegun import freeze_time
+import logging
+from pytz import timezone
+
+from odoo.tests import tagged
+from odoo.tools import misc
+
+from .common import TestSaEdiCommon
+
+_logger = logging.getLogger(__name__)
+
+
+@tagged('post_install_l10n', '-at_install', 'post_install')
+class TestEdiZatca(TestSaEdiCommon):
+
+    def testInvoiceStandard(self):
+
+        with freeze_time(datetime(year=2022, month=9, day=5, hour=8, minute=20, second=2, tzinfo=timezone('Etc/GMT-3'))):
+            standard_invoice = misc.file_open('l10n_sa_edi/tests/compliance/standard/invoice.xml', 'rb').read()
+            expected_tree = self.get_xml_tree_from_string(standard_invoice)
+            expected_tree = self.with_applied_xpath(expected_tree, self.invoice_applied_xpath)
+
+            move = self._create_invoice(name='INV/2022/00014', date='2022-09-05', date_due='2022-09-22', partner_id=self.partner_us,
+                                        product_id=self.product_a, price=320.0)
+            move._l10n_sa_generate_unsigned_data()
+            generated_file = self.env['account.edi.format']._l10n_sa_generate_zatca_template(move)
+            current_tree = self.get_xml_tree_from_string(generated_file)
+            current_tree = self.with_applied_xpath(current_tree, self.remove_ubl_extensions_xpath)
+
+            self.assertXmlTreeEqual(current_tree, expected_tree)
+
+    def testCreditNoteStandard(self):
+
+        with freeze_time(datetime(year=2022, month=9, day=5, hour=9, minute=39, second=15, tzinfo=timezone('Etc/GMT-3'))):
+            applied_xpath = self.credit_note_applied_xpath + \
+            '''
+                <xpath expr="(//*[local-name()='AdditionalDocumentReference']/*[local-name()='UUID'])[1]" position="replace">
+                    <UUID>___ignore___</UUID>
+                </xpath>
+            '''
+
+            standard_credit_note = misc.file_open('l10n_sa_edi/tests/compliance/standard/credit.xml', 'rb').read()
+            expected_tree = self.get_xml_tree_from_string(standard_credit_note)
+            expected_tree = self.with_applied_xpath(expected_tree, applied_xpath)
+
+            credit_note = self._create_credit_note(name='INV/2022/00014', date='2022-09-05', date_due='2022-09-22',
+                                                   partner_id=self.partner_us, product_id=self.product_a, price=320.0)
+            credit_note._l10n_sa_generate_unsigned_data()
+            generated_file = self.env['account.edi.format']._l10n_sa_generate_zatca_template(credit_note)
+            current_tree = self.get_xml_tree_from_string(generated_file)
+            current_tree = self.with_applied_xpath(current_tree, self.remove_ubl_extensions_xpath)
+
+            self.assertXmlTreeEqual(current_tree, expected_tree)
+
+    def testDebitNoteStandard(self):
+        with freeze_time(datetime(year=2022, month=9, day=5, hour=9, minute=45, second=27, tzinfo=timezone('Etc/GMT-3'))):
+            applied_xpath = self.debit_note_applied_xpath + \
+            '''
+                <xpath expr="(//*[local-name()='AdditionalDocumentReference']/*[local-name()='UUID'])[1]" position="replace">
+                    <UUID>___ignore___</UUID>
+                </xpath>
+            '''
+
+            standard_debit_note = misc.file_open('l10n_sa_edi/tests/compliance/standard/debit.xml', 'rb').read()
+            expected_tree = self.get_xml_tree_from_string(standard_debit_note)
+            expected_tree = self.with_applied_xpath(expected_tree, applied_xpath)
+
+            debit_note = self._create_debit_note(name='INV/2022/00001', date='2022-09-05', date_due='2022-09-22',
+                                                 partner_id=self.partner_us, product_id=self.product_b, price=15.80)
+            debit_note._l10n_sa_generate_unsigned_data()
+            generated_file = self.env['account.edi.format']._l10n_sa_generate_zatca_template(debit_note)
+            current_tree = self.get_xml_tree_from_string(generated_file)
+            current_tree = self.with_applied_xpath(current_tree, self.remove_ubl_extensions_xpath)
+
+            self.assertXmlTreeEqual(current_tree, expected_tree)
+
+    def testInvoiceSimplified(self):
+        with freeze_time(datetime(year=2023, month=3, day=10, hour=14, minute=56, second=55, tzinfo=timezone('Etc/GMT-3'))):
+            simplified_invoice = misc.file_open('l10n_sa_edi/tests/compliance/simplified/invoice.xml', 'rb').read()
+            expected_tree = self.get_xml_tree_from_string(simplified_invoice)
+            expected_tree = self.with_applied_xpath(expected_tree, self.invoice_applied_xpath)
+
+            move = self._create_invoice(name='INV/2023/00034', date='2023-03-10', date_due='2023-03-10', partner_id=self.partner_sa_simplified,
+                                        product_id=self.product_burger, price=265.00, quantity=3.0)
+            move._l10n_sa_generate_unsigned_data()
+            generated_file = self.env['account.edi.format']._l10n_sa_generate_zatca_template(move)
+            current_tree = self.get_xml_tree_from_string(generated_file)
+            current_tree = self.with_applied_xpath(current_tree, self.remove_ubl_extensions_xpath)
+
+            self.assertXmlTreeEqual(current_tree, expected_tree)
+
+    def testCreditNoteSimplified(self):
+        with freeze_time(datetime(year=2023, month=3, day=10, hour=14, minute=59, second=38, tzinfo=timezone('Etc/GMT-3'))):
+            simplified_credit_note = misc.file_open('l10n_sa_edi/tests/compliance/simplified/credit.xml', 'rb').read()
+            expected_tree = self.get_xml_tree_from_string(simplified_credit_note)
+            expected_tree = self.with_applied_xpath(expected_tree, self.credit_note_applied_xpath)
+
+            move = self._create_credit_note(name='INV/2023/00034', date='2023-03-10', date_due='2023-03-10',
+                                            partner_id=self.partner_sa_simplified, product_id=self.product_burger,
+                                            price=265.00, quantity=3.0)
+            move._l10n_sa_generate_unsigned_data()
+            generated_file = self.env['account.edi.format']._l10n_sa_generate_zatca_template(move)
+            current_tree = self.get_xml_tree_from_string(generated_file)
+            current_tree = self.with_applied_xpath(current_tree, self.remove_ubl_extensions_xpath)
+
+            self.assertXmlTreeEqual(current_tree, expected_tree)
+
+    def testDebitNoteSimplified(self):
+        with freeze_time(datetime(year=2023, month=3, day=10, hour=15, minute=1, second=46, tzinfo=timezone('Etc/GMT-3'))):
+            simplified_credit_note = misc.file_open('l10n_sa_edi/tests/compliance/simplified/debit.xml', 'rb').read()
+            expected_tree = self.get_xml_tree_from_string(simplified_credit_note)
+            expected_tree = self.with_applied_xpath(expected_tree, self.debit_note_applied_xpath)
+
+            move = self._create_debit_note(name='INV/2023/00034', date='2023-03-10', date_due='2023-03-10',
+                                           partner_id=self.partner_sa_simplified, product_id=self.product_burger,
+                                           price=265.00, quantity=2.0)
+            move._l10n_sa_generate_unsigned_data()
+            generated_file = self.env['account.edi.format']._l10n_sa_generate_zatca_template(move)
+            current_tree = self.get_xml_tree_from_string(generated_file)
+            current_tree = self.with_applied_xpath(current_tree, self.remove_ubl_extensions_xpath)
+
+            self.assertXmlTreeEqual(current_tree, expected_tree)
diff --git a/addons/l10n_sa_edi/views/account_journal_views.xml b/addons/l10n_sa_edi/views/account_journal_views.xml
new file mode 100644
index 000000000000..6cd81353c638
--- /dev/null
+++ b/addons/l10n_sa_edi/views/account_journal_views.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+
+        <record id="view_account_journal_form" model="ir.ui.view">
+            <field name="name">account.journal.form.l10n_sa_edi</field>
+            <field name="model">account.journal</field>
+            <field name="inherit_id" ref="account.view_account_journal_form"/>
+            <field name="arch" type="xml">
+                <xpath expr="//notebook" position="inside">
+                    <field name="l10n_sa_csr" invisible="1"/>
+                    <field name="l10n_sa_compliance_csid_json" invisible="1"/>
+                    <field name="l10n_sa_production_csid_json" invisible="1"/>
+                    <field name="l10n_sa_compliance_checks_passed" invisible="1"/>
+                    <page name="zatca_einvoicing" string="ZATCA" attrs="{'invisible': [('country_code', '!=', 'SA')]}">
+                        <group>
+                            <group>
+                                <field name="l10n_sa_serial_number"/>
+                            </group>
+                        </group>
+                        <p>
+                            <b>
+                                In order to be able to submit Invoices to ZATCA, the following steps need to be completed:
+                            </b>
+                            <ol class="mt-2 mb-4">
+                                <li>
+                                    Set a Serial Number for your device
+                                    <i class="fa fa-check text-success ms-1"
+                                       attrs="{'invisible': [('l10n_sa_serial_number', '=', False)]}"/>
+                                </li>
+                                <li>
+                                    Request a Compliance Certificate (CCSID)
+                                    <i class="fa fa-check text-success ms-1"
+                                       attrs="{'invisible': [('l10n_sa_compliance_csid_json', '=', False)]}"/>
+                                </li>
+                                <li>
+                                    Complete the Compliance Checks
+                                    <i class="fa fa-check text-success ms-1"
+                                       attrs="{'invisible': [('l10n_sa_compliance_checks_passed', '=', False)]}"/>
+                                </li>
+                                <li>
+                                    Request a Production Certificate (PCSID)
+                                    <i class="fa fa-check text-success ms-1"
+                                       attrs="{'invisible': [('l10n_sa_production_csid_json', '=', False)]}"/>
+                                </li>
+                            </ol>
+                        </p>
+                        <div class="alert alert-info d-flex justify-content-between align-items-center" role="alert"
+                             attrs="{'invisible':['|', ('l10n_sa_csr_errors', '!=', False), ('l10n_sa_compliance_csid_json', '!=', False)]}">
+                            <p class="mb-0">
+                                Onboard the Journal by completing each step
+                            </p>
+                            <button name="%(l10n_sa_edi_otp_wizard_act_window)d" type="action" icon="fa-key"
+                                    class="btn-info ">
+                                Onboard Journal
+                            </button>
+                        </div>
+                        <div class="alert alert-danger d-flex flex-column align-items-end" role="alert"
+                             attrs="{'invisible':['|', '|', ('l10n_sa_csr_errors', '=', False), ('l10n_sa_compliance_csid_json', '!=', False), ('l10n_sa_production_csid_json', '!=', False)]}">
+                            <div class="w-100">
+                                <h4 role="alert" class="alert-heading">Journal could not be onboarded. Please make sure the Company VAT/Identification Number are correct.</h4>
+                                <field name="l10n_sa_csr_errors" nolabel="1" readonly="1"/>
+                                <hr/>
+                            </div>
+                            <button name="%(l10n_sa_edi_otp_wizard_act_window)d" type="action" icon="fa-key"
+                                    class="btn-danger">
+                                Onboard Journal
+                            </button>
+                        </div>
+                        <div class="alert alert-info d-flex justify-content-between align-items-center" role="alert"
+                             attrs="{'invisible':['|', ('l10n_sa_compliance_checks_passed', '=', False), ('l10n_sa_production_csid_json', '=', False)]}">
+                            <p class="mb-0">
+                                The Production certificate is valid until
+                                <field name="l10n_sa_production_csid_validity" readonly="1" nolabel="1"
+                                       class="fw-bold"/>
+                            </p>
+                            <div>
+                                <button name="%(l10n_sa_edi_otp_wizard_act_window)d" type="action" icon="fa-refresh"
+                                        class="btn-info" context="{'default_l10n_sa_renewal': True}">
+                                    Renew Production CSID
+                                </button>
+                                <button name="%(l10n_sa_edi_otp_wizard_act_window)d" type="action" icon="fa-refresh" class="btn-warning ms-2"
+                                    confirm="Are you sure you wish to re-onboard the Journal?">
+                                    Re-Onboard
+                                </button>
+                            </div>
+                        </div>
+                    </page>
+                </xpath>
+            </field>
+        </record>
+
+    </data>
+</odoo>
\ No newline at end of file
diff --git a/addons/l10n_sa_edi/views/account_tax_views.xml b/addons/l10n_sa_edi/views/account_tax_views.xml
new file mode 100644
index 000000000000..ea4a5ff5d3ca
--- /dev/null
+++ b/addons/l10n_sa_edi/views/account_tax_views.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+    <record id="view_tax_form" model="ir.ui.view">
+        <field name="name">account.tax.form.zatca</field>
+        <field name="model">account.tax</field>
+        <field name="inherit_id" ref="account.view_tax_form"/>
+        <field name="arch" type="xml">
+            <xpath expr="//field[@name='country_id']" position="after">
+                <field name="l10n_sa_is_retention" attrs="{'invisible': ['|', '|', ('type_tax_use', '!=', 'sale'), ('country_code', '!=', 'SA'), ('amount', '>=', 0)]}"/>
+                <field name="l10n_sa_exemption_reason_code" attrs="{'invisible': ['|', '|', ('type_tax_use', '!=', 'sale'), ('country_code', '!=', 'SA'), ('amount', '!=', 0)]}"/>
+            </xpath>
+        </field>
+    </record>
+
+</odoo>
diff --git a/addons/l10n_sa_edi/views/report_invoice.xml b/addons/l10n_sa_edi/views/report_invoice.xml
new file mode 100644
index 000000000000..1c803086773e
--- /dev/null
+++ b/addons/l10n_sa_edi/views/report_invoice.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+
+        <template id="arabic_english_invoice" inherit_id="l10n_sa.arabic_english_invoice">
+
+            <!--    Add Currency Exchange rate if different currency than SAR    -->
+            <xpath expr="//div[hasclass('clearfix')]" position="after">
+                <div t-if="o.company_id.country_id.code == 'SA' and o.currency_id != o.company_id.currency_id"
+                     id="sar_amounts" class="row clearfix ms-auto my-3 text-nowrap table">
+                    <t t-set="sar_rate"
+                       t-value="o.env['res.currency']._get_conversion_rate(o.currency_id, o.company_id.currency_id, o.company_id, o.l10n_sa_confirmation_datetime)"/>
+                    <div name="exchange_rate" class="col-auto">
+                        <strong>Exchange Rate</strong>
+                        <p class="m-0" t-esc="sar_rate" t-options='{"widget": "float", "precision": 5}'/>
+                    </div>
+                    <div name="sar_subtotal" class="col-auto">
+                        <strong>Subtotal (SAR)</strong>
+                        <p class="m-0" t-esc="o.amount_untaxed_signed"
+                           t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/>
+                    </div>
+                    <div name="sar_vat_amount" class="col-auto">
+                        <strong>VAT Amount (SAR)</strong>
+                        <p class="m-0"
+                           t-esc="o.currency_id._convert(o.amount_tax, o.company_id.currency_id, o.company_id, o.l10n_sa_confirmation_datetime)"
+                           t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/>
+                    </div>
+                    <div name="sar_total" class="col-auto">
+                        <strong>Total (SAR)</strong>
+                        <p class="m-0" t-esc="o.amount_total_signed"
+                           t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/>
+                    </div>
+                </div>
+            </xpath>
+
+            <xpath expr="//t[@t-set='address']" position="inside">
+                <div t-if="o.partner_id.l10n_sa_additional_identification_scheme and o.partner_id.l10n_sa_additional_identification_number" class="text-end mt0">
+                        <span t-field="o.partner_id.l10n_sa_additional_identification_scheme"/>:
+                        <span t-field="o.partner_id.l10n_sa_additional_identification_number"/>
+                </div>
+            </xpath>
+            <xpath expr="//div[hasclass('col-4')]//span[@t-if=&quot;o.move_type == &apos;out_invoice&apos; and o.state == &apos;posted&apos;&quot;]" position="replace">
+                <span t-if="o.move_type == 'out_invoice' and o.state == 'posted'">
+                    <t t-if="o._l10n_sa_is_simplified()">
+                        Simplified Tax Invoice
+                    </t>
+                    <t t-else="">
+                        Tax Invoice
+                    </t>
+                </span>
+            </xpath>
+            <xpath expr="//div[hasclass('col-4')][3]//span[@t-if=&quot;o.move_type == &apos;out_invoice&apos; and o.state == &apos;posted&apos;&quot;]" position="replace">
+                <span t-if="o.move_type == 'out_invoice' and o.state == 'posted'">
+                    <t t-if="o._l10n_sa_is_simplified()">
+                        فاتورة ضريبية مبسطة
+                    </t>
+                    <t t-else="">
+                        فاتورة ضريبية
+                    </t>
+                </span>
+            </xpath>
+
+        </template>
+
+    </data>
+</odoo>
\ No newline at end of file
diff --git a/addons/l10n_sa_edi/views/res_company_views.xml b/addons/l10n_sa_edi/views/res_company_views.xml
new file mode 100644
index 000000000000..2b9049301df0
--- /dev/null
+++ b/addons/l10n_sa_edi/views/res_company_views.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <record id="view_company_form" model="ir.ui.view">
+            <field name="name">res.company.l10n_sa_edi.form</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='country_id']" position="after">
+                    <field name="l10n_sa_edi_building_number" placeholder="Building Number"
+                           attrs="{'invisible': [('country_code', '!=', 'SA')]}"
+                           class="o_address_building_number" options='{"no_open": True, "no_create": True}'/>
+                    <field name="l10n_sa_edi_plot_identification" placeholder="Plot Identification"
+                           attrs="{'invisible': [('country_code', '!=', 'SA')]}"
+                           class="o_address_plot_identification" options='{"no_open": True, "no_create": True}'/>
+                </xpath>
+                <xpath expr="//field[@name='vat']" position="before">
+                    <field name="l10n_sa_additional_identification_scheme" attrs="{'invisible': [('country_code', '!=', 'SA')]}"/>
+                    <field name="l10n_sa_additional_identification_number" attrs="{'invisible': ['|', ('country_code', '!=', 'SA'), ('l10n_sa_additional_identification_scheme', '=', 'TIN')]}"/>
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo>
diff --git a/addons/l10n_sa_edi/views/res_config_settings_view.xml b/addons/l10n_sa_edi/views/res_config_settings_view.xml
new file mode 100644
index 000000000000..9f287ba48040
--- /dev/null
+++ b/addons/l10n_sa_edi/views/res_config_settings_view.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+    <record model="ir.ui.view" id="res_config_settings_view_form">
+        <field name="name">res.config.settings.view.form.inherit.l10n_sa_edi</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[@data-key='account']/div" position="after">
+                <field name="country_code" invisible="1"/>
+                <h2 attrs="{'invisible':[('country_code', '!=', 'SA')]}">ZATCA E-Invoicing Settings</h2>
+                <div class="row mt16 o_settings_container" name="saudi_zatca_edi" attrs="{'invisible':[('country_code', '!=', 'SA')]}">
+                    <div class="col-12 o_setting_box">
+                        <div class="o_setting_left_pane"/>
+                        <div class="o_setting_right_pane">
+                            <span class="o_form_label">ZATCA API Integration</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">
+                                You can select the API used for submissions down below. There are three modes available: Sandbox, Pre-Production and Production.
+                                Once you have selected the correct API, you can start the Onboarding process by going to the Journals and checking the options under the ZATCA tab.
+                            </div>
+                            <div class="content-group">
+                                 <div class="row mt8">
+                                    <label for="l10n_sa_api_mode" class="col-2 o_light_label" string="API Mode"/>
+                                    <field name="l10n_sa_api_mode" help="Set whether the system should use the Production API"/>
+                                </div>
+                            </div>
+                            <div class="alert alert-warning mt8" role="alert">
+                                <h4 class="alert-heading" role="alert">
+                                    <i class="fa fa-warning me-2" /> Warning
+                                </h4>
+                                Once you change the submission mode to <strong>Production</strong>, you cannot change it anymore.
+                                Be very careful, as any invoice submitted to ZATCA in Production mode will be accounted for
+                                and might lead to <strong>Fines &amp; Penalties</strong>.
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </xpath>
+        </field>
+    </record>
+
+</odoo>
diff --git a/addons/l10n_sa_edi/views/res_partner_views.xml b/addons/l10n_sa_edi/views/res_partner_views.xml
new file mode 100644
index 000000000000..56d652d43967
--- /dev/null
+++ b/addons/l10n_sa_edi/views/res_partner_views.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+
+        <record id="view_partner_form" model="ir.ui.view">
+            <field name="name">res.partner.l10n_sa_edi.form</field>
+            <field name="model">res.partner</field>
+            <field name="inherit_id" ref="base.view_partner_form"/>
+            <field name="arch" type="xml">
+                <field name="vat" position="before">
+                    <field name="l10n_sa_additional_identification_scheme" attrs="{'invisible': [('country_code', '!=', 'SA')]}"/>
+                    <field name="l10n_sa_additional_identification_number" attrs="{'invisible': ['|', ('country_code', '!=', 'SA'), ('l10n_sa_additional_identification_scheme', '=', 'TIN')]}"/>
+                </field>
+            </field>
+        </record>
+
+    </data>
+</odoo>
diff --git a/addons/l10n_sa_edi/wizard/__init__.py b/addons/l10n_sa_edi/wizard/__init__.py
new file mode 100644
index 000000000000..55a601f2cee4
--- /dev/null
+++ b/addons/l10n_sa_edi/wizard/__init__.py
@@ -0,0 +1,3 @@
+from . import account_move_reversal
+from . import account_debit_note
+from . import l10n_sa_edi_otp_wizard
diff --git a/addons/l10n_sa_edi/wizard/account_debit_note.py b/addons/l10n_sa_edi/wizard/account_debit_note.py
new file mode 100644
index 000000000000..f3f7b87750b3
--- /dev/null
+++ b/addons/l10n_sa_edi/wizard/account_debit_note.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+from odoo import models
+from odoo.tools.translate import _
+from odoo.exceptions import UserError
+
+
+class AccountDebitNote(models.TransientModel):
+    _inherit = 'account.debit.note'
+
+    def create_debit(self):
+        self.ensure_one()
+        for move in self.move_ids:
+            if move.journal_id.country_code == 'SA' and not self.reason:
+                raise UserError(_("For debit notes issued in Saudi Arabia, you need to specify a Reason"))
+        return super().create_debit()
diff --git a/addons/l10n_sa_edi/wizard/account_move_reversal.py b/addons/l10n_sa_edi/wizard/account_move_reversal.py
new file mode 100644
index 000000000000..090c6dd133c9
--- /dev/null
+++ b/addons/l10n_sa_edi/wizard/account_move_reversal.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+from odoo import models
+from odoo.tools.translate import _
+from odoo.exceptions import UserError
+
+
+class AccountMoveReversal(models.TransientModel):
+    _inherit = 'account.move.reversal'
+
+    def reverse_moves(self):
+        self.ensure_one()
+        for move in self.move_ids:
+            if move.journal_id.country_code == 'SA' and not self.reason:
+                raise UserError(_("For Credit/Debit notes issued in Saudi Arabia, you need to specify a Reason"))
+        return super().reverse_moves()
diff --git a/addons/l10n_sa_edi/wizard/l10n_sa_edi_otp_wizard.py b/addons/l10n_sa_edi/wizard/l10n_sa_edi_otp_wizard.py
new file mode 100644
index 000000000000..b4dbda3d9eed
--- /dev/null
+++ b/addons/l10n_sa_edi/wizard/l10n_sa_edi_otp_wizard.py
@@ -0,0 +1,28 @@
+from odoo import fields, models, _, api
+from odoo.exceptions import UserError
+
+
+class RequestZATCAOtp(models.TransientModel):
+    _name = 'l10n_sa_edi.otp.wizard'
+    _description = 'Request ZATCA OTP'
+
+    l10n_sa_renewal = fields.Boolean("PCSID Renewal",
+                                     help="Used to decide whether we should call the PCSID renewal API or the CCSID API",
+                                     default=False)
+    l10n_sa_otp = fields.Char("OTP", copy=False, help="OTP required to get a CCSID. Can only be acquired through "
+                                                      "the Fatoora portal.")
+    journal_id = fields.Many2one('account.journal', default=lambda self: self.env.context.get('active_id'), required=True)
+
+    @api.model
+    def default_get(self, fields):
+        res = super().default_get(fields)
+        if self.env.company.l10n_sa_api_mode == 'sandbox':
+            res['l10n_sa_otp'] = '123456' if self.l10n_sa_renewal else '123345'
+        return res
+
+    def validate(self):
+        if not self.l10n_sa_otp:
+            raise UserError(_("You need to provide an OTP to be able to request a CCSID"))
+        if self.l10n_sa_renewal:
+            return self.journal_id._l10n_sa_get_production_CSID(self.l10n_sa_otp)
+        self.journal_id._l10n_sa_api_onboard_journal(self.l10n_sa_otp)
diff --git a/addons/l10n_sa_edi/wizard/l10n_sa_edi_otp_wizard.xml b/addons/l10n_sa_edi/wizard/l10n_sa_edi_otp_wizard.xml
new file mode 100644
index 000000000000..7d7e6b88b312
--- /dev/null
+++ b/addons/l10n_sa_edi/wizard/l10n_sa_edi_otp_wizard.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+    <record id="l10n_sa_edi_otp_wizard_view_form" model="ir.ui.view">
+        <field name="name">l10n_sa_edi.otp.wizard.form</field>
+        <field name="model">l10n_sa_edi.otp.wizard</field>
+        <field name="arch" type="xml">
+            <form string="Use an OTP to request for a CSID">
+                Please, set the OTP you received from ZATCA in the input below then validate.
+                <group>
+                    <field name="journal_id" invisible="1"/>
+                    <field name="l10n_sa_renewal" invisible="1"/>
+                    <field name="l10n_sa_otp"/>
+                </group>
+                <footer>
+                    <button string="Request" type="object" name="validate" class="btn btn-primary" data-hotkey="q"/>
+                    <button string="Cancel" special="cancel" data-hotkey="z" class="btn btn-secondary"/>
+                </footer>
+            </form>
+        </field>
+    </record>
+
+    <record id="l10n_sa_edi_otp_wizard_act_window" model="ir.actions.act_window">
+        <field name="name">Request a CSID</field>
+        <field name="res_model">l10n_sa_edi.otp.wizard</field>
+        <field name="view_mode">form</field>
+        <field name="target">new</field>
+    </record>
+
+</odoo>
-- 
GitLab