mirror of
https://github.com/bringout/oca-ocb-l10n_me-africa.git
synced 2026-04-27 07:42:02 +02:00
Initial commit: L10N_Me Africa packages
This commit is contained in:
commit
c265268138
611 changed files with 75334 additions and 0 deletions
51
odoo-bringout-oca-ocb-l10n_sa_edi/README.md
Normal file
51
odoo-bringout-oca-ocb-l10n_sa_edi/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Saudi Arabia - E-invoicing
|
||||
|
||||
|
||||
E-invoice implementation for Saudi Arabia; Integration with ZATCA
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-l10n_sa_edi
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- account_edi_ubl_cii
|
||||
- account_debit_note
|
||||
- l10n_sa
|
||||
- base_vat
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Saudi Arabia - E-invoicing
|
||||
- **Version**: 0.2
|
||||
- **Category**: Accounting/Localizations/EDI
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `l10n_sa_edi`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
32
odoo-bringout-oca-ocb-l10n_sa_edi/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-ocb-l10n_sa_edi/doc/ARCHITECTURE.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph L10n_sa_edi Module - l10n_sa_edi
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
3
odoo-bringout-oca-ocb-l10n_sa_edi/doc/CONFIGURATION.md
Normal file
3
odoo-bringout-oca-ocb-l10n_sa_edi/doc/CONFIGURATION.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for l10n_sa_edi. Configure related models, access rights, and options as needed.
|
||||
3
odoo-bringout-oca-ocb-l10n_sa_edi/doc/CONTROLLERS.md
Normal file
3
odoo-bringout-oca-ocb-l10n_sa_edi/doc/CONTROLLERS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
8
odoo-bringout-oca-ocb-l10n_sa_edi/doc/DEPENDENCIES.md
Normal file
8
odoo-bringout-oca-ocb-l10n_sa_edi/doc/DEPENDENCIES.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [account_edi_ubl_cii](../../odoo-bringout-oca-ocb-account_edi_ubl_cii)
|
||||
- [account_debit_note](../../odoo-bringout-oca-ocb-account_debit_note)
|
||||
- [l10n_sa](../../odoo-bringout-oca-ocb-l10n_sa)
|
||||
- [base_vat](../../odoo-bringout-oca-ocb-base_vat)
|
||||
4
odoo-bringout-oca-ocb-l10n_sa_edi/doc/FAQ.md
Normal file
4
odoo-bringout-oca-ocb-l10n_sa_edi/doc/FAQ.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon l10n_sa_edi or install in UI.
|
||||
7
odoo-bringout-oca-ocb-l10n_sa_edi/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-ocb-l10n_sa_edi/doc/INSTALL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-ocb-l10n_sa_edi"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-ocb-l10n_sa_edi"
|
||||
```
|
||||
24
odoo-bringout-oca-ocb-l10n_sa_edi/doc/MODELS.md
Normal file
24
odoo-bringout-oca-ocb-l10n_sa_edi/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in l10n_sa_edi.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class account_edi_xml_ubl_21_zatca
|
||||
class account_edi_document
|
||||
class account_edi_format
|
||||
class account_edi_xml_ubl_21
|
||||
class account_journal
|
||||
class account_move
|
||||
class account_move_line
|
||||
class account_tax
|
||||
class account_tax_template
|
||||
class ir_attachment
|
||||
class res_company
|
||||
class res_config_settings
|
||||
class res_partner
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
6
odoo-bringout-oca-ocb-l10n_sa_edi/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-ocb-l10n_sa_edi/doc/OVERVIEW.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: l10n_sa_edi. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon l10n_sa_edi
|
||||
- License: LGPL-3
|
||||
3
odoo-bringout-oca-ocb-l10n_sa_edi/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-ocb-l10n_sa_edi/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
34
odoo-bringout-oca-ocb-l10n_sa_edi/doc/SECURITY.md
Normal file
34
odoo-bringout-oca-ocb-l10n_sa_edi/doc/SECURITY.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in l10n_sa_edi.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../l10n_sa_edi/security/ir.model.access.csv)**
|
||||
- 1 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../l10n_sa_edi/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
5
odoo-bringout-oca-ocb-l10n_sa_edi/doc/TROUBLESHOOTING.md
Normal file
5
odoo-bringout-oca-ocb-l10n_sa_edi/doc/TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
7
odoo-bringout-oca-ocb-l10n_sa_edi/doc/USAGE.md
Normal file
7
odoo-bringout-oca-ocb-l10n_sa_edi/doc/USAGE.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon l10n_sa_edi
|
||||
```
|
||||
10
odoo-bringout-oca-ocb-l10n_sa_edi/doc/WIZARDS.md
Normal file
10
odoo-bringout-oca-ocb-l10n_sa_edi/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Wizards
|
||||
|
||||
Transient models exposed as UI wizards in l10n_sa_edi.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class AccountDebitNote
|
||||
class AccountMoveReversal
|
||||
class RequestZATCAOtp
|
||||
```
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models, wizard
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# -*- 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.2',
|
||||
'depends': [
|
||||
'account_edi_ubl_cii',
|
||||
'account_debit_note',
|
||||
'l10n_sa',
|
||||
'base_vat'
|
||||
],
|
||||
'author': 'Odoo S.A.',
|
||||
'summary': """
|
||||
E-Invoicing, Universal Business Language
|
||||
""",
|
||||
'description': """
|
||||
E-invoice implementation for Saudi Arabia; Integration with ZATCA
|
||||
""",
|
||||
'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',
|
||||
'wizard/account_move_reversal_views.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',
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- disable l10n_sa_edi
|
||||
UPDATE res_company
|
||||
SET l10n_sa_api_mode = 'sandbox';
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">399999999900003</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">3999999999</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>
|
||||
1281
odoo-bringout-oca-ocb-l10n_sa_edi/l10n_sa_edi/i18n/ar.po
Normal file
1281
odoo-bringout-oca-ocb-l10n_sa_edi/l10n_sa_edi/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load diff
1330
odoo-bringout-oca-ocb-l10n_sa_edi/l10n_sa_edi/i18n/l10n_sa_edi.pot
Normal file
1330
odoo-bringout-oca-ocb-l10n_sa_edi/l10n_sa_edi/i18n/l10n_sa_edi.pot
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,14 @@
|
|||
from odoo import _, api, SUPERUSER_ID
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
zatca_format = env.ref('l10n_sa_edi.edi_sa_zatca')
|
||||
journals = env["account.journal"].search([
|
||||
("edi_format_ids", "in", zatca_format.id),
|
||||
("l10n_sa_compliance_checks_passed", "=", True),
|
||||
("l10n_sa_production_csid_json", "!=", False)])
|
||||
journals.activity_schedule(
|
||||
act_type_xmlid='mail.mail_activity_data_warning',
|
||||
user_id=env.ref("base.user_admin").id,
|
||||
note=_('Please Re-Onboard the Journal for a new serial number'))
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# -*- 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
|
||||
from . import ir_attachment
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,519 @@
|
|||
import logging
|
||||
|
||||
from markupsafe import Markup
|
||||
from hashlib import sha256
|
||||
from base64 import b64decode, b64encode
|
||||
from lxml import etree
|
||||
from datetime import 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
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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'):
|
||||
error = clearance_data['json_errors']
|
||||
error_msg = ''
|
||||
status_code = error.get('status_code')
|
||||
if status_code:
|
||||
error_msg = Markup("<b>[%s] </b>") % status_code
|
||||
|
||||
is_warning = True
|
||||
validation_results = error.get('validationResults', {})
|
||||
for err in validation_results.get('warningMessages', []):
|
||||
error_msg += Markup('<b>%s</b> : %s <br/>') % (err['code'], err['message'])
|
||||
for err in validation_results.get('errorMessages', []):
|
||||
is_warning = False
|
||||
error_msg += Markup('<b>%s</b> : %s <br/>') % (err['code'], err['message'])
|
||||
return {
|
||||
'error': error_msg,
|
||||
'rejected': not is_warning,
|
||||
'response': signed_xml.decode(),
|
||||
'blocking_level': 'warning' if is_warning else 'error',
|
||||
'status_code': status_code,
|
||||
}
|
||||
if not clearance_data.get('error') and clearance_data.get("status_code") != 409:
|
||||
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():
|
||||
# Applying with_prefetch() to set the _prefetch_ids = _ids,
|
||||
# preventing premature QR code computation for other invoices.
|
||||
invoice = invoice.with_prefetch()
|
||||
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',
|
||||
'response': unsigned_xml
|
||||
}, unsigned_xml)
|
||||
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
|
||||
}, 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: line.display_type == 'product').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 request timedout, just log note a warning message
|
||||
invoice._l10n_sa_log_results(submitted_xml, response_data, error=response_data.get('rejected'))
|
||||
|
||||
# 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: line.display_type == 'product')):
|
||||
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 > fields.Date.context_today(self.with_context(tz='Asia/Riyadh')):
|
||||
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 either the Reversed Entry or 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')
|
||||
return doc.attachment_id.raw or self._l10n_sa_generate_zatca_template(invoice).encode()
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
def _prepare_invoice_report(self, pdf_writer, edi_document):
|
||||
"""
|
||||
Prepare invoice report to be printed.
|
||||
:param pdf_writer: The pdf writer with the invoice pdf content loaded.
|
||||
:param edi_document: The edi document to be added to the pdf file.
|
||||
"""
|
||||
self.ensure_one()
|
||||
super()._prepare_invoice_report(pdf_writer, edi_document)
|
||||
if self.code != 'sa_zatca' or edi_document.move_id.country_code != 'SA':
|
||||
return
|
||||
|
||||
attachment = edi_document.sudo().attachment_id
|
||||
if not attachment or not attachment.datas:
|
||||
_logger.warning(f"No attachment found for invoice {edi_document.move_id.name}")
|
||||
return
|
||||
|
||||
xml_content = attachment.raw
|
||||
file_name = attachment.name
|
||||
|
||||
pdf_writer.addAttachment(file_name, xml_content, subtype='text/xml')
|
||||
if not pdf_writer.is_pdfa:
|
||||
try:
|
||||
pdf_writer.convert_to_pdfa()
|
||||
except Exception as e:
|
||||
_logger.exception("Error while converting to PDF/A: %s", e)
|
||||
content = self.env['ir.qweb']._render(
|
||||
'account_edi_ubl_cii.account_invoice_pdfa_3_facturx_metadata',
|
||||
{
|
||||
'title': edi_document.move_id.name,
|
||||
'date': fields.Date.context_today(self),
|
||||
},
|
||||
)
|
||||
pdf_writer.add_file_metadata(content.encode())
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
# -*- 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 """
|
||||
shipping_address = invoice.partner_shipping_id
|
||||
return [{'actual_delivery_date': invoice.l10n_sa_delivery_date,
|
||||
'delivery_address_vals': self._get_partner_address_vals(shipping_address) if shipping_address else {},}]
|
||||
|
||||
def _get_partner_contact_vals(self, partner):
|
||||
res = super()._get_partner_contact_vals(partner)
|
||||
if res.get('telephone'):
|
||||
res['telephone'] = re.sub(r"[^+\d]", '', res['telephone'])
|
||||
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' and partner.country_code == 'SA'
|
||||
else partner.vat
|
||||
),
|
||||
}]
|
||||
|
||||
def _get_partner_party_legal_entity_vals_list(self, partner):
|
||||
# EXTEND 'account.edi.xml.ubl_20'
|
||||
partners_party_legal = super()._get_partner_party_legal_entity_vals_list(partner)
|
||||
for partner_party_legal in partners_party_legal:
|
||||
if partner_party_legal['commercial_partner'].country_code != 'SA':
|
||||
partner_party_legal['company_id'] = False
|
||||
|
||||
return partners_party_legal
|
||||
|
||||
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.get(self._l10n_sa_get_payment_means_code(invoice), PAYMENT_MEANS_CODE['unknown'])
|
||||
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(r'[^a-zA-Z0-9 -]+', '-', invoice.name)
|
||||
invoice_date = fields.Datetime.context_timestamp(self.with_context(tz='Asia/Riyadh'), invoice.l10n_sa_confirmation_datetime)
|
||||
file_name = f"{vat}_{invoice_date.strftime('%Y%m%dT%H%M%S')}_{invoice_number}"
|
||||
file_format = self.env.context.get('l10n_sa_file_format', 'xml')
|
||||
if file_format:
|
||||
file_name = f'{file_name}.{file_format}'
|
||||
return file_name
|
||||
|
||||
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
|
||||
386 if invoice._is_downpayment() 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'])
|
||||
line_extension_amount = vals['vals']['legal_monetary_total_vals']['line_extension_amount']
|
||||
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)
|
||||
allowance_charge_vals = vals['vals']['allowance_charge_vals']
|
||||
allowance_total_amount = sum(a['amount'] for a in allowance_charge_vals if a['charge_indicator'] == 'false')
|
||||
if downpayment_vals:
|
||||
# - BR-KSA-80: To calculate payable amount, we deduct prepaid amount from total tax inclusive amount
|
||||
prepaid_amount = downpayment_vals['total_amount']
|
||||
payable_amount = tax_inclusive_amount - prepaid_amount
|
||||
return {
|
||||
'line_extension_amount': line_extension_amount - allowance_total_amount,
|
||||
'tax_inclusive_amount': tax_inclusive_amount,
|
||||
'tax_exclusive_amount': tax_exclusive_amount,
|
||||
'prepaid_amount': prepaid_amount,
|
||||
'payable_amount': payable_amount,
|
||||
'allowance_total_amount': allowance_total_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 _get_document_allowance_charge_vals_list(self, invoice):
|
||||
"""
|
||||
Charge Reasons & Codes (As per ZATCA):
|
||||
https://unece.org/fileadmin/DAM/trade/untdid/d16b/tred/tred5189.htm
|
||||
As far as ZATCA is concerned, we calculate Allowance/Charge vals for global discounts as
|
||||
a document level allowance, and we do not include any other charges or allowances
|
||||
"""
|
||||
res = super()._get_document_allowance_charge_vals_list(invoice)
|
||||
for line in invoice.invoice_line_ids.filtered(lambda l: l._is_global_discount_line()):
|
||||
taxes = line.tax_ids.flatten_taxes_hierarchy().filtered(lambda t: t.amount_type != 'fixed')
|
||||
res.append({
|
||||
'charge_indicator': 'false',
|
||||
'allowance_charge_reason_code': "95",
|
||||
'allowance_charge_reason': "Discount",
|
||||
'amount': abs(line.price_subtotal),
|
||||
'currency_dp': 2,
|
||||
'currency_name': invoice.currency_id.name,
|
||||
'tax_category_vals': [{
|
||||
'id': tax['id'],
|
||||
'percent': tax['percent'],
|
||||
'tax_scheme_id': 'VAT',
|
||||
} for tax in self._get_tax_category_list(line.move_id, taxes)],
|
||||
})
|
||||
return res
|
||||
|
||||
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))
|
||||
self._l10n_sa_postprocess_line_vals(vals)
|
||||
|
||||
return vals
|
||||
|
||||
def _l10n_sa_postprocess_line_vals(self, vals):
|
||||
"""
|
||||
Postprocess vals to remove negative line amounts, as those will be used to compute
|
||||
document level allowances (global discounts)
|
||||
"""
|
||||
final_line_vals = []
|
||||
for line_vals in vals['vals']['invoice_line_vals']:
|
||||
if line_vals['price_vals']['price_amount'] >= 0:
|
||||
final_line_vals.append(line_vals)
|
||||
vals['vals']['invoice_line_vals'] = final_line_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.move_type == 'out_invoice' and 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(filter_to_apply=lambda l, t: not self.env['account.tax'].browse(t['id']).l10n_sa_is_retention, 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)
|
||||
else:
|
||||
# - BR-KSA-80: only down-payment lines should have a tax subtotal breakdown, as that is
|
||||
# used during computation of prepaid amount as ZATCA sums up tax amount/taxable amount of all lines
|
||||
# irrespective of whether they are down-payment lines.
|
||||
line_vals['tax_total_vals'][0].pop('tax_subtotal_vals', None)
|
||||
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 or "VATEX-SA-OOS",
|
||||
'tax_exemption_reason': (
|
||||
exemption_codes[reason].split(reason)[1].lstrip()
|
||||
if reason else "Not subject to VAT"
|
||||
)
|
||||
}
|
||||
|
||||
supplier = invoice.company_id.partner_id.commercial_partner_id
|
||||
if 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 _exemption_reason('O', tax.l10n_sa_exemption_reason_code)
|
||||
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 []
|
||||
|
|
@ -0,0 +1,687 @@
|
|||
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="Unique Serial Number automatically filled when the journal is onboarded")
|
||||
|
||||
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
|
||||
|
||||
def _l10n_sa_api_onboard_sanity_checks(self):
|
||||
"""
|
||||
Perform a sanity check to validate that the journal is ready to be onboarded
|
||||
"""
|
||||
|
||||
# If the invoice wasn't sent to ZATCA because of a timeout, it will retain its existing chain index
|
||||
# Make sure there are no opened invoices with the journal's existing sequence
|
||||
move_ids = self.env['account.move'].search(
|
||||
[
|
||||
('journal_id', '=', self.id),
|
||||
('l10n_sa_chain_index', '!=', 0)
|
||||
]
|
||||
)
|
||||
stuck_moves = [move for move in move_ids if not move._l10n_sa_is_in_chain()]
|
||||
if stuck_moves:
|
||||
raise UserError(_("Oops! The journal is stuck. Please submit the pending invoices to ZATCA and try again."))
|
||||
|
||||
def _l10n_sa_edi_set_csr_fields(self):
|
||||
'''
|
||||
Sets default values for CSR generation fields in Odoo, if their values do not exist
|
||||
'''
|
||||
self.ensure_one()
|
||||
# Avoid unnecessary write calls
|
||||
if self.l10n_sa_serial_number != str(self.id):
|
||||
self.l10n_sa_serial_number = self.id
|
||||
|
||||
# ====== 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()
|
||||
|
||||
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, company_id.name),
|
||||
# Subject Common Name (Short Code - Journal Name - Company Name)
|
||||
(NameOID.COMMON_NAME, "%s-%s-%s" % (self.code, self.name, company_id.name)),
|
||||
# Organization Identifier
|
||||
(ObjectIdentifier('2.5.4.97'), company_id.vat),
|
||||
# State/Province Name
|
||||
(NameOID.STATE_OR_PROVINCE_NAME, company_id.state_id.name),
|
||||
# Locality Name
|
||||
(NameOID.LOCALITY_NAME, 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'), company_id.street),
|
||||
# Industry
|
||||
x509.NameAttribute(ObjectIdentifier('2.5.4.15'), 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.sudo().l10n_sa_production_csid_json:
|
||||
journal.l10n_sa_production_csid_validity = self._l10n_sa_get_pcsid_validity(
|
||||
json.loads(journal.sudo().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()
|
||||
# we want to perform sanity checks to ensure that the journal is ready to be onboarded
|
||||
# If the check fails, we do not want to revoke the existing PCSID because the user might still need it to post hanging invoices
|
||||
self._l10n_sa_api_onboard_sanity_checks()
|
||||
self._l10n_sa_edi_set_csr_fields()
|
||||
|
||||
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
|
||||
# Regenerate a new chain sequence
|
||||
self._l10n_sa_edi_icv_onboarding()
|
||||
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('errors') or CCSID_data.get('error'):
|
||||
raise UserError(_("Could not obtain Compliance CSID: %s",
|
||||
CCSID_data['errors'][0]['message'] if CCSID_data.get('errors') else 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_icv_onboarding(self):
|
||||
"""
|
||||
Onboarding method to create or reset ICV sequence for the journal
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.l10n_sa_chain_sequence_id:
|
||||
self.l10n_sa_chain_sequence_id.number_next = 1
|
||||
message = _("Journal re-onboarded with ZATCA successfully")
|
||||
else:
|
||||
self.l10n_sa_chain_sequence_id = self._l10n_sa_edi_create_new_chain()
|
||||
message = _("Journal onboarded with ZATCA successfully")
|
||||
self.message_post(body=message)
|
||||
|
||||
def _l10n_sa_edi_create_new_chain(self):
|
||||
self.ensure_one()
|
||||
return 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,
|
||||
})
|
||||
|
||||
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._l10n_sa_edi_create_new_chain()
|
||||
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.sudo().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.sudo().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.company_id.l10n_sa_api_mode]
|
||||
request_url = urljoin(api_url, request_url)
|
||||
status_code = False
|
||||
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:
|
||||
# The 400 case means that it is rejected by ZATCA, but we need to update the hash as done for accepted.
|
||||
# In the 401+ cases, it is like the server is overloaded e.g. and we still need to resend later. We do not
|
||||
# erase the index chain (excepted) because for ZATCA, one ICV (index chain) needs to correspond to one invoice.
|
||||
status_code = ex.response.status_code
|
||||
if status_code not in {400, 409}:
|
||||
return {
|
||||
'error': (Markup("<b>[%s]</b>") % status_code) + _("Server returned an unexpected error: %(error)s",
|
||||
error=(request_response.text or str(ex))),
|
||||
'blocking_level': 'warning',
|
||||
'status_code': status_code,
|
||||
'excepted': True,
|
||||
}
|
||||
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}
|
||||
|
||||
if request_response.status_code == '303':
|
||||
return {'error': _('Clearance and reporting seem to have been mixed up. '),
|
||||
'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'
|
||||
}
|
||||
response_data['status_code'] = request_response.status_code
|
||||
|
||||
if status_code == 409:
|
||||
return response_data
|
||||
|
||||
val_res = response_data.get('validationResults', {})
|
||||
if not request_response.ok and (val_res.get('errorMessages') or val_res.get('warningMessages')):
|
||||
error = "" if not status_code else Markup("<b>[%s]</b>") % (status_code)
|
||||
if isinstance(response_data, dict) and val_res.get('errorMessages'):
|
||||
error += _("Invoice submission to ZATCA returned errors")
|
||||
return {
|
||||
'error': error,
|
||||
'json_errors': response_data,
|
||||
'blocking_level': 'error',
|
||||
}
|
||||
error += request_response.reason
|
||||
return {
|
||||
'error': error,
|
||||
'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": "TUlJQ2N6Q0NBaG1nQXdJQkFnSUdBWStWTmxza01Bb0dDQ3FHU000OUJBTUNNQlV4RXpBUkJnTlZCQU1NQ21WSmJuWnZhV05wYm1jd0hoY05NalF3TlRJd01EZzFOVEV6V2hjTk1qa3dOVEU1TWpFd01EQXdXakNCbnpFTE1Ba0dBMVVFQmhNQ1UwRXhFekFSQmdOVkJBc01Dak01T1RrNU9UazVPVGt4RXpBUkJnTlZCQW9NQ2xOQklFTnZiWEJoYm5reEV6QVJCZ05WQkFNTUNsTkJJRU52YlhCaGJua3hHREFXQmdOVkJHRU1Eek01T1RrNU9UazVPVGt3TURBd016RVBNQTBHQTFVRUNBd0dVbWw1WVdSb01TWXdKQVlEVlFRSERCM1lwOW1FMllYWXI5bUsyWWJZcVNEWXA5bUUyWVhaaHRtSTJMSFlxVEJXTUJBR0J5cUdTTTQ5QWdFR0JTdUJCQUFLQTBJQUJOVlB3N0hGNjhUVWtQTkJQb29uT0Y2NnRPMm5IcmxUNlRMcmk3MEpLY1MvYmVMWitoRVE0MmdXdUtYckp5RmxnWm9kUVJzTFQyMEtQZnE0Q3N2YlFJMmpnY3d3Z2Nrd0RBWURWUjBUQVFIL0JBSXdBRENCdUFZRFZSMFJCSUd3TUlHdHBJR3FNSUduTVRRd01nWURWUVFFRENzeExVOWtiMjk4TWkweE5Yd3pMVk5KUkVrekxVTkNUVkJTTFV3eVJEaFlMVXROTUV0T0xWZzBTVk5LTVI4d0hRWUtDWkltaVpQeUxHUUJBUXdQTXprNU9UazVPVGs1T1RBd01EQXpNUTB3Q3dZRFZRUU1EQVF4TVRBd01TOHdMUVlEVlFRYURDWkJiQ0JCYldseUlFMXZhR0Z0YldWa0lFSnBiaUJCWW1SMWJDQkJlbWw2SUZOMGNtVmxkREVPTUF3R0ExVUVEd3dGVDNSb1pYSXdDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWdTeVhlZExqOUtMVTRUMWFBbVQvL09GZDBGWWxLQnIraFFIeGNDM0c2ajc4Q0lRRGdlNjNsQkVqTU1ETktqTm1pTklaQlBWSnlHRzl5bVJaSHdvUzV5TEQyZXc9PQ==", "secret": "uMpSz85cV0h/e/uqpJ+FaZkdYZ76uoaRYOevGufcup0=", "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": "TUlJRDNqQ0NBNFNnQXdJQkFnSVRFUUFBT0FQRjkwQWpzL3hjWHdBQkFBQTRBekFLQmdncWhrak9QUVFEQWpCaU1SVXdFd1lLQ1pJbWlaUHlMR1FCR1JZRmJHOWpZV3d4RXpBUkJnb0praWFKay9Jc1pBRVpGZ05uYjNZeEZ6QVZCZ29Ka2lhSmsvSXNaQUVaRmdkbGVIUm5ZWHAwTVJzd0dRWURWUVFERXhKUVVscEZTVTVXVDBsRFJWTkRRVFF0UTBFd0hoY05NalF3TVRFeE1Ea3hPVE13V2hjTk1qa3dNVEE1TURreE9UTXdXakIxTVFzd0NRWURWUVFHRXdKVFFURW1NQ1FHQTFVRUNoTWRUV0Y0YVcxMWJTQlRjR1ZsWkNCVVpXTm9JRk4xY0hCc2VTQk1WRVF4RmpBVUJnTlZCQXNURFZKcGVXRmthQ0JDY21GdVkyZ3hKakFrQmdOVkJBTVRIVlJUVkMwNE9EWTBNekV4TkRVdE16azVPVGs1T1RrNU9UQXdNREF6TUZZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUFvRFFnQUVvV0NLYTBTYTlGSUVyVE92MHVBa0MxVklLWHhVOW5QcHgydmxmNHloTWVqeThjMDJYSmJsRHE3dFB5ZG84bXEwYWhPTW1Obzhnd25pN1h0MUtUOVVlS09DQWdjd2dnSURNSUd0QmdOVkhSRUVnYVV3Z2FLa2daOHdnWnd4T3pBNUJnTlZCQVFNTWpFdFZGTlVmREl0VkZOVWZETXRaV1F5TW1ZeFpEZ3RaVFpoTWkweE1URTRMVGxpTlRndFpEbGhPR1l4TVdVME5EVm1NUjh3SFFZS0NaSW1pWlB5TEdRQkFRd1BNems1T1RrNU9UazVPVEF3TURBek1RMHdDd1lEVlFRTURBUXhNVEF3TVJFd0R3WURWUVFhREFoU1VsSkVNamt5T1RFYU1CZ0dBMVVFRHd3UlUzVndjR3g1SUdGamRHbDJhWFJwWlhNd0hRWURWUjBPQkJZRUZFWCtZdm1tdG5Zb0RmOUJHYktvN29jVEtZSzFNQjhHQTFVZEl3UVlNQmFBRkp2S3FxTHRtcXdza0lGelZ2cFAyUHhUKzlObk1Ic0dDQ3NHQVFVRkJ3RUJCRzh3YlRCckJnZ3JCZ0VGQlFjd0FvWmZhSFIwY0RvdkwyRnBZVFF1ZW1GMFkyRXVaMjkyTG5OaEwwTmxjblJGYm5KdmJHd3ZVRkphUlVsdWRtOXBZMlZUUTBFMExtVjRkR2RoZW5RdVoyOTJMbXh2WTJGc1gxQlNXa1ZKVGxaUFNVTkZVME5CTkMxRFFTZ3hLUzVqY25Rd0RnWURWUjBQQVFIL0JBUURBZ2VBTUR3R0NTc0dBUVFCZ2pjVkJ3UXZNQzBHSlNzR0FRUUJnamNWQ0lHR3FCMkUwUHNTaHUyZEpJZk8reG5Ud0ZWbWgvcWxaWVhaaEQ0Q0FXUUNBUkl3SFFZRFZSMGxCQll3RkFZSUt3WUJCUVVIQXdNR0NDc0dBUVVGQndNQ01DY0dDU3NHQVFRQmdqY1ZDZ1FhTUJnd0NnWUlLd1lCQlFVSEF3TXdDZ1lJS3dZQkJRVUhBd0l3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUloQUxFL2ljaG1uV1hDVUtVYmNhM3ljaThvcXdhTHZGZEhWalFydmVJOXVxQWJBaUE5aEM0TThqZ01CQURQU3ptZDJ1aVBKQTZnS1IzTEUwM1U3NWVxYkMvclhBPT0=", "secret": "CkYsEXfV8c1gFHAtFWoZv73pGMvh/Qyo4LzKM2h/8Hg="}"""
|
||||
})
|
||||
|
|
@ -0,0 +1,319 @@
|
|||
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', 'edi_document_ids',
|
||||
'l10n_sa_invoice_signature', 'l10n_sa_chain_index', 'state')
|
||||
def _compute_qr_code_str(self):
|
||||
""" Override to update QR code generation in accordance with ZATCA Phase 2"""
|
||||
phase_one_moves = self.env['account.move']
|
||||
for move in self:
|
||||
zatca_document = move.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'sa_zatca')
|
||||
if move.country_code == 'SA' and move.move_type in ('out_invoice', 'out_refund') and zatca_document and move.state != 'draft':
|
||||
qr_code_str = ''
|
||||
if move._l10n_sa_is_simplified():
|
||||
x509_cert = json.loads(move.journal_id.sudo().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, True)
|
||||
qr_code_str = b64encode(qr_code_str).decode()
|
||||
elif zatca_document.state == 'sent' and zatca_document.sudo().attachment_id.datas:
|
||||
document_xml = zatca_document.attachment_id.with_context(bin_size=False).datas.decode()
|
||||
root = etree.fromstring(b64decode(document_xml))
|
||||
qr_node = root.xpath('//*[local-name()="ID"][text()="QR"]/following-sibling::*/*')[0]
|
||||
qr_code_str = qr_node.text
|
||||
move.l10n_sa_qr_code_str = qr_code_str
|
||||
else:
|
||||
# In the case where the Invoice is not a ZATCA invoice, or is Phase 1, or is not confirmed,
|
||||
# we call super to trigger the initial QR code generation for Phase 1
|
||||
phase_one_moves |= move
|
||||
super(AccountMove, phase_one_moves)._compute_qr_code_str()
|
||||
|
||||
|
||||
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 or 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 = b''
|
||||
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 and signature:
|
||||
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:%S").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_reset_confirmation_datetime(self):
|
||||
""" OVERRIDE: we want rejected phase 2 invoices to keep the original confirmation datetime"""
|
||||
for move in self.filtered(lambda m: m.country_code == 'SA'):
|
||||
zatca_doc = move.edi_document_ids.filtered(lambda d: d.edi_format_id.code == 'sa_zatca')
|
||||
if not zatca_doc or zatca_doc[0].blocking_level != 'error': # Error is the rejection case
|
||||
move.l10n_sa_confirmation_datetime = 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()
|
||||
if not response_data.get("excepted"):
|
||||
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, subtitle, content = ("success", _("Invoice Successfully Submitted to ZATCA"), "", "" if (not error or not response_data) else response_data)
|
||||
status_code = response_data.get('status_code')
|
||||
attachment = False
|
||||
if error:
|
||||
xml_filename = self.env['account.edi.xml.ubl_21.zatca']._export_invoice_filename(self)
|
||||
xml_filename = xml_filename[:-4] + '-rejected.xml'
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'raw': xml_content,
|
||||
'name': xml_filename,
|
||||
'description': 'Rejected ZATCA Document not to be deleted - ثيقة ZATCA المرفوضة لا يجوز حذفها',
|
||||
'res_id': self.id,
|
||||
'res_model': self._name,
|
||||
'type': 'binary',
|
||||
'mimetype': 'application/xml',
|
||||
})
|
||||
bootstrap_cls, title = ("danger", _("Invoice was rejected by ZATCA"))
|
||||
subtitle = _('The invoice was rejected by ZATCA. Please, check the response below:')
|
||||
content = response_data['error']
|
||||
if response_data and response_data.get('validationResults', {}).get('warningMessages'):
|
||||
bootstrap_cls, title = ("warning", _("Invoice was Accepted by ZATCA (with Warnings)"))
|
||||
subtitle = _('The invoice was accepted by ZATCA, but returned warnings. Please, check the response below:')
|
||||
content = Markup("""<b>%(status_code)s</b>%(errors)s""") % {
|
||||
"status_code": f"[{status_code}] " if status_code else "",
|
||||
"errors": Markup("<br/>").join([
|
||||
Markup("<b>%(code)s</b> : %(message)s") % {
|
||||
"code": m['code'],
|
||||
"message": m['message'],
|
||||
} for m in response_data['validationResults']['warningMessages']
|
||||
])
|
||||
}
|
||||
if response_data.get("error") and response_data.get("excepted"):
|
||||
bootstrap_cls, title = ("warning", _("Warning: Unable to Retrieve a Response from ZATCA"))
|
||||
subtitle = _('Unable to retrieve response from ZATCA. Please, check the response below:')
|
||||
content = response_data['error']
|
||||
if status_code == 409:
|
||||
bootstrap_cls, title = ("warning", _("Warning: Invoice was already successfully reported to ZATCA"))
|
||||
subtitle = _("This invoice was already successfully reported to ZATCA. Please, check the response below:")
|
||||
content = Markup("""<b>%(status_code)s</b>%(errors)s""") % {
|
||||
"status_code": f"[{status_code}] " if status_code else "",
|
||||
"errors": Markup("<br/>").join([
|
||||
Markup("<b>%(code)s</b> : %(message)s") % {
|
||||
"code": m['code'],
|
||||
"message": m['message'],
|
||||
} for m in response_data['validationResults']['errorMessages']
|
||||
])
|
||||
}
|
||||
self.with_context(no_new_invoice=True).message_post(body=Markup("""
|
||||
<div role='alert' class='alert alert-%s'>
|
||||
<h4 class='alert-heading'>%s</h4>
|
||||
<p class='mb-0'>
|
||||
%s
|
||||
</p>
|
||||
%s
|
||||
<p class='mb-0'>
|
||||
%s
|
||||
</p>
|
||||
</div>
|
||||
""") % (bootstrap_cls, title, subtitle, Markup("<hr>") if content else "", content),
|
||||
attachment_ids=attachment and [attachment.id] or []
|
||||
)
|
||||
|
||||
def _is_l10n_sa_eligibile_invoice(self):
|
||||
self.ensure_one()
|
||||
return self.is_invoice() and self.l10n_sa_confirmation_datetime and self.country_code == 'SA'
|
||||
|
||||
def _get_report_base_filename(self):
|
||||
"""
|
||||
Generate the name of the invoice PDF file according to ZATCA business rules:
|
||||
Seller Vat Number (BT-31), Date (BT-2), Time (KSA-25), Invoice Number (BT-1)
|
||||
"""
|
||||
if self._is_l10n_sa_eligibile_invoice():
|
||||
return self.with_context(l10n_sa_file_format=False).env['account.edi.xml.ubl_21.zatca']._export_invoice_filename(self)
|
||||
return super()._get_report_base_filename()
|
||||
|
||||
def _get_report_attachment_filename(self):
|
||||
if self._is_l10n_sa_eligibile_invoice():
|
||||
return self.with_context(l10n_sa_file_format='pdf').env['account.edi.xml.ubl_21.zatca']._export_invoice_filename(self)
|
||||
return super()._get_report_attachment_filename()
|
||||
|
||||
def _get_report_mail_attachment_filename(self):
|
||||
if self._is_l10n_sa_eligibile_invoice():
|
||||
return self.with_context(l10n_sa_file_format=False).env['account.edi.xml.ubl_21.zatca']._export_invoice_filename(self)
|
||||
return super()._get_report_mail_attachment_filename()
|
||||
|
||||
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'))
|
||||
|
||||
def _get_tax_lines_to_aggregate(self):
|
||||
"""
|
||||
If the final invoice has downpayment lines, we skip the tax correction, as we need to recalculate tax amounts
|
||||
without taking into account those lines
|
||||
"""
|
||||
if self.country_code == 'SA' and not self._is_downpayment() and self.line_ids._get_downpayment_lines():
|
||||
return self.env['account.move.line']
|
||||
return super()._get_tax_lines_to_aggregate()
|
||||
|
||||
def _get_l10n_sa_totals(self):
|
||||
self.ensure_one()
|
||||
invoice_vals = self.env['account.edi.xml.ubl_21.zatca']._export_invoice_vals(self)
|
||||
return {
|
||||
'total_amount': invoice_vals['vals']['legal_monetary_total_vals']['tax_inclusive_amount'],
|
||||
'total_tax': invoice_vals['vals']['tax_total_vals'][-1]['tax_amount'],
|
||||
}
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
def _apply_retention_tax_filter(self, tax_values):
|
||||
return not tax_values['tax_id'].l10n_sa_is_retention
|
||||
|
||||
def _is_global_discount_line(self):
|
||||
"""
|
||||
Any line that has a negative amount and is not linked to a down-payment is considered as a
|
||||
global discount line. These can be created either manually, or through a promotions program.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return not self._get_downpayment_lines() and self.price_subtotal < 0
|
||||
|
||||
@api.depends('price_subtotal', 'price_total')
|
||||
def _compute_tax_amount(self):
|
||||
super()._compute_tax_amount()
|
||||
taxes_vals_by_move = {}
|
||||
for record in self:
|
||||
move = record.move_id
|
||||
if move.country_code == 'SA':
|
||||
taxes_vals = taxes_vals_by_move.get(move.id)
|
||||
if not taxes_vals:
|
||||
taxes_vals = move._prepare_invoice_aggregated_taxes(
|
||||
filter_tax_values_to_apply=lambda l, t: not self.env['account.tax'].browse(t['id']).l10n_sa_is_retention
|
||||
)
|
||||
taxes_vals_by_move[move.id] = taxes_vals
|
||||
record.l10n_gcc_invoice_tax_amount = abs(taxes_vals.get('tax_details_per_record', {}).get(record, {}).get('tax_amount_currency', 0))
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
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.'),
|
||||
('VATEX-SA-OOS', 'VATEX-SA-OOS Not subject to VAT.')
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from odoo import api, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_rejected_zatca_document(self):
|
||||
'''
|
||||
Prevents unlinking of rejected XML documents
|
||||
'''
|
||||
descr = 'Rejected ZATCA Document not to be deleted - ثيقة ZATCA المرفوضة لا يجوز حذفها'
|
||||
for attach in self.filtered(lambda a: a.description == descr and a.res_model == 'account.move'):
|
||||
move = self.env['account.move'].browse(attach.res_id)
|
||||
if move.country_code == "SA":
|
||||
raise UserError(_("You can't unlink an attachment being an EDI document refused by the government."))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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)
|
||||
|
|
@ -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']
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
248
odoo-bringout-oca-ocb-l10n_sa_edi/l10n_sa_edi/tests/common.py
Normal file
248
odoo-bringout-oca-ocb-l10n_sa_edi/l10n_sa_edi/tests/common.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
# coding: utf-8
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import new_test_user
|
||||
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': '+1 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>
|
||||
'''
|
||||
cls.user_saudi = new_test_user(cls.env, 'xav', email='em@il.com', notification_type='inbox', groups='account.group_account_invoice', tz='Asia/Riyadh')
|
||||
|
||||
def _create_invoice(self, **kwargs):
|
||||
vals = {
|
||||
'name': kwargs['name'],
|
||||
'move_type': 'out_invoice',
|
||||
'company_id': self.company.id,
|
||||
'partner_id': kwargs['partner_id'].id,
|
||||
'invoice_date': kwargs['date'],
|
||||
'invoice_date_due': kwargs['date_due'],
|
||||
'currency_id': self.company.currency_id.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)],
|
||||
}),
|
||||
],
|
||||
}
|
||||
user = kwargs.get('user') or self.env.user
|
||||
move = self.env['account.move'].with_user(user.id).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
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
<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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>+966512345678</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:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<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: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.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
<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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>+966512345678</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:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<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: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.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
<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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>+966512345678</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:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<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: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.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
<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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>+966512345678</cbc:Telephone>
|
||||
<cbc:ElectronicMail>info@company.saexample.com</cbc:ElectronicMail>
|
||||
</cac:Contact>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<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>+18709310505</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:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<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: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.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
<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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>+966512345678</cbc:Telephone>
|
||||
<cbc:ElectronicMail>info@company.saexample.com</cbc:ElectronicMail>
|
||||
</cac:Contact>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<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>+18709310505</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:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<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: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.8</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
<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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>+966512345678</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">US12345677</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>+18709310505</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:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<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: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.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
# 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 import Command
|
||||
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)
|
||||
|
||||
self.partner_us.vat = 'US12345677'
|
||||
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 testInvoiceWithDownpayment(self):
|
||||
|
||||
if 'sale' not in self.env["ir.module.module"]._installed():
|
||||
self.skipTest("Sale module is not installed")
|
||||
|
||||
def test_generated_file(move, test_file, xpath_to_apply):
|
||||
move.write({
|
||||
'invoice_date': '2022-09-05',
|
||||
'invoice_date_due': '2022-09-22',
|
||||
'state': 'posted',
|
||||
'l10n_sa_confirmation_datetime': datetime.now(),
|
||||
})
|
||||
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)
|
||||
|
||||
expected_file = misc.file_open(f'l10n_sa_edi/tests/test_files/{test_file}.xml', 'rb').read()
|
||||
expected_tree = self.get_xml_tree_from_string(expected_file)
|
||||
expected_tree = self.with_applied_xpath(expected_tree, xpath_to_apply)
|
||||
|
||||
self.assertXmlTreeEqual(current_tree, expected_tree)
|
||||
|
||||
retention_tax = self.env['account.tax'].create({
|
||||
'l10n_sa_is_retention': True,
|
||||
'name': 'Retention Tax',
|
||||
'amount_type': 'percent',
|
||||
'amount': -5.0,
|
||||
})
|
||||
|
||||
with freeze_time(datetime(year=2022, month=9, day=5, hour=8, minute=20, second=2, tzinfo=timezone('Etc/GMT-3'))):
|
||||
self.partner_us.vat = 'US12345677'
|
||||
|
||||
pricelist = self.env['product.pricelist'].create({'name': 'SAR', 'currency_id': self.env.ref('base.SAR').id})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_us.id,
|
||||
'pricelist_id': pricelist.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 1000,
|
||||
'product_uom_qty': 1,
|
||||
'tax_id': [Command.set((self.tax_15 + retention_tax).ids)],
|
||||
})
|
||||
]
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
|
||||
context = {
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': [sale_order.id],
|
||||
'active_id': sale_order.id,
|
||||
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||||
}
|
||||
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'fixed',
|
||||
'fixed_amount': 100,
|
||||
'deposit_taxes_id': [Command.set(self.tax_15.ids)],
|
||||
})._create_invoices(sale_order)
|
||||
|
||||
final = self.env['sale.advance.payment.inv'].with_context(context).create({})._create_invoices(sale_order)
|
||||
|
||||
for move, test_file in (
|
||||
(downpayment, "downpayment_invoice"),
|
||||
(final, "final_invoice")
|
||||
):
|
||||
with self.subTest(move=move, test_file=test_file):
|
||||
test_generated_file(move, test_file, self.invoice_applied_xpath)
|
||||
|
||||
for move, test_file in (
|
||||
(downpayment, "downpayment_credit_note"),
|
||||
(final, "final_credit_note")
|
||||
):
|
||||
with self.subTest(move=move, test_file=test_file):
|
||||
wiz_context = {
|
||||
'active_model': 'account.move',
|
||||
'active_ids': [move.id],
|
||||
'default_journal_id': move.journal_id.id,
|
||||
}
|
||||
refund_invoice_wiz = self.env['account.move.reversal'].with_context(wiz_context).create({
|
||||
'reason': 'please reverse :c',
|
||||
'refund_method': 'refund',
|
||||
'date': '2022-09-05',
|
||||
})
|
||||
refund_invoice = self.env['account.move'].browse(refund_invoice_wiz.reverse_moves()['res_id'])
|
||||
test_generated_file(refund_invoice, test_file, self.credit_note_applied_xpath)
|
||||
|
||||
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)
|
||||
|
||||
@freeze_time("2024-02-14 21:30:00", tz_offset=0)
|
||||
def test_invoice_standard_with_accepted_time(self):
|
||||
|
||||
move = self._create_invoice(
|
||||
name='INV/2024/00014',
|
||||
date='2024-02-15',
|
||||
date_due='2024-02-15',
|
||||
partner_id=self.partner_us,
|
||||
product_id=self.product_a,
|
||||
price=320.0,
|
||||
user=self.user_saudi,
|
||||
)
|
||||
errors = self.edi_format.with_user(self.user_saudi.id)._check_move_configuration(move)
|
||||
msg = '- Please, make sure the invoice date is set to either the same as or before Today.'
|
||||
self.assertFalse(msg in errors)
|
||||
|
||||
@freeze_time("2022-09-21 15:30:00", tz_offset=0)
|
||||
def test_invoice_standard_with_future_time(self):
|
||||
|
||||
move = self._create_invoice(
|
||||
name='INV/2024/00014',
|
||||
date='2024-02-20',
|
||||
date_due='2024-02-28',
|
||||
partner_id=self.partner_us,
|
||||
product_id=self.product_a,
|
||||
price=320.0,
|
||||
user=self.user_saudi,
|
||||
)
|
||||
errors = self.edi_format.with_user(self.user_saudi.id)._check_move_configuration(move)
|
||||
msg = '- Please, make sure the invoice date is set to either the same as or before Today.'
|
||||
self.assertTrue(msg in errors)
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-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/00001</cbc:ID>
|
||||
<cbc:UUID>ea8e1ab4-6b4e-4cb2-8efc-f8e229622774</cbc:UUID>
|
||||
<cbc:IssueDate>2022-09-05</cbc:IssueDate>
|
||||
<cbc:IssueTime>08:20:02</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>Reversal of: INV/2022/00001, please reverse :c</cbc:ID>
|
||||
</cac:OrderReference>
|
||||
<cac:BillingReference>
|
||||
<cac:InvoiceDocumentReference>
|
||||
<cbc:ID>INV/2022/00001</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>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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>346</cbc:ID>
|
||||
<cbc:Name>SA Company Test</cbc:Name>
|
||||
<cbc:Telephone>+966512345678</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">US12345677</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>350</cbc:ID>
|
||||
<cbc:Name>Chichi Lboukla</cbc:Name>
|
||||
<cbc:Telephone>+18709310505</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:InstructionNote>Reversal of: INV/2022/00001, please reverse :c</cbc:InstructionNote>
|
||||
<cbc:PaymentID>RINV/2022/00001</cbc:PaymentID>
|
||||
</cac:PaymentMeans>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">15.00</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="SAR">100.00</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="SAR">15.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">15.00</cbc:TaxAmount>
|
||||
</cac:TaxTotal>
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">100.00</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="SAR">100.00</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="SAR">115.00</cbc:TaxInclusiveAmount>
|
||||
<cbc:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<cbc:PrepaidAmount currencyID="SAR">0.00</cbc:PrepaidAmount>
|
||||
<cbc:PayableAmount currencyID="SAR">115.00</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">15.00</cbc:TaxAmount>
|
||||
<cbc:RoundingAmount currencyID="SAR">115.00</cbc:RoundingAmount>
|
||||
</cac:TaxTotal>
|
||||
<cac:Item>
|
||||
<cbc:Description>Down Payment</cbc:Description>
|
||||
<cbc:Name>Down payment</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">100.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
<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/00001</cbc:ID>
|
||||
<cbc:UUID>7a06f916-5f83-4519-9355-89d778d246bd</cbc:UUID>
|
||||
<cbc:IssueDate>2022-09-05</cbc:IssueDate>
|
||||
<cbc:IssueTime>08:20:02</cbc:IssueTime>
|
||||
<cbc:InvoiceTypeCode name="0100100">386</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>SAR</cbc:DocumentCurrencyCode>
|
||||
<cbc:TaxCurrencyCode>SAR</cbc:TaxCurrencyCode>
|
||||
<cbc:BuyerReference>Azure Interior</cbc:BuyerReference>
|
||||
<cac:OrderReference>
|
||||
<cbc:ID>INV/2022/00001</cbc:ID>
|
||||
</cac:OrderReference>
|
||||
<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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>407</cbc:ID>
|
||||
<cbc:Name>SA Company Test</cbc:Name>
|
||||
<cbc:Telephone>+966512345678</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">US12345677</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>411</cbc:ID>
|
||||
<cbc:Name>Chichi Lboukla</cbc:Name>
|
||||
<cbc:Telephone>+18709310505</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/00001</cbc:InstructionID>
|
||||
<cbc:PaymentID>INV/2022/00001</cbc:PaymentID>
|
||||
</cac:PaymentMeans>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">15.00</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="SAR">100.00</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="SAR">15.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">15.00</cbc:TaxAmount>
|
||||
</cac:TaxTotal>
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">100.00</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="SAR">100.00</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="SAR">115.00</cbc:TaxInclusiveAmount>
|
||||
<cbc:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<cbc:PrepaidAmount currencyID="SAR">0.00</cbc:PrepaidAmount>
|
||||
<cbc:PayableAmount currencyID="SAR">115.00</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">15.00</cbc:TaxAmount>
|
||||
<cbc:RoundingAmount currencyID="SAR">115.00</cbc:RoundingAmount>
|
||||
</cac:TaxTotal>
|
||||
<cac:Item>
|
||||
<cbc:Description>Down Payment</cbc:Description>
|
||||
<cbc:Name>Down payment</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">100.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
<Invoice xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-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/00002</cbc:ID>
|
||||
<cbc:UUID>e2ab7427-4f07-4f3b-b874-9ca03da4880a</cbc:UUID>
|
||||
<cbc:IssueDate>2022-09-05</cbc:IssueDate>
|
||||
<cbc:IssueTime>08:20:02</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>Reversal of: INV/2022/00002, please reverse :c</cbc:ID>
|
||||
</cac:OrderReference>
|
||||
<cac:BillingReference>
|
||||
<cac:InvoiceDocumentReference>
|
||||
<cbc:ID>RINV/2022/00002</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>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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>366</cbc:ID>
|
||||
<cbc:Name>SA Company Test</cbc:Name>
|
||||
<cbc:Telephone>+966512345678</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">US12345677</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>370</cbc:ID>
|
||||
<cbc:Name>Chichi Lboukla</cbc:Name>
|
||||
<cbc:Telephone>+18709310505</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:InstructionNote>Reversal of: INV/2022/00002, please reverse :c</cbc:InstructionNote>
|
||||
<cbc:PaymentID>RINV/2022/00002</cbc:PaymentID>
|
||||
</cac:PaymentMeans>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">150.00</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="SAR">1000.00</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="SAR">150.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">150.00</cbc:TaxAmount>
|
||||
</cac:TaxTotal>
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">1000.00</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="SAR">1000.00</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="SAR">1150.00</cbc:TaxInclusiveAmount>
|
||||
<cbc:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<cbc:PrepaidAmount currencyID="SAR">115.00</cbc:PrepaidAmount>
|
||||
<cbc:PayableAmount currencyID="SAR">1035.00</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">1000.00</cbc:LineExtensionAmount>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">150.00</cbc:TaxAmount>
|
||||
<cbc:RoundingAmount currencyID="SAR">1150.00</cbc:RoundingAmount>
|
||||
</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">1000.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>2</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">0.00</cbc:LineExtensionAmount>
|
||||
<cac:DocumentReference>
|
||||
<cbc:ID>INV/2022/00001</cbc:ID>
|
||||
<cbc:IssueDate>2022-09-05</cbc:IssueDate>
|
||||
<cbc:IssueTime>08:20:02</cbc:IssueTime>
|
||||
<cbc:DocumentTypeCode>386</cbc:DocumentTypeCode>
|
||||
</cac:DocumentReference>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">0.00</cbc:TaxAmount>
|
||||
<cbc:RoundingAmount currencyID="SAR">0.00</cbc:RoundingAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="SAR">100.00</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="SAR">15.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>Down Payment: 09 2022 (Draft)</cbc:Description>
|
||||
<cbc:Name>Down payment</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">0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-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/00002</cbc:ID>
|
||||
<cbc:UUID>f60b0627-777e-4374-b8a3-ea071d9220cc</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:OrderReference>
|
||||
<cbc:ID>INV/2022/00002</cbc:ID>
|
||||
</cac:OrderReference>
|
||||
<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>المدينة المنورة</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>المدينة المنورة</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>المدينة المنورة</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>316</cbc:ID>
|
||||
<cbc:Name>SA Company Test</cbc:Name>
|
||||
<cbc:Telephone>+966512345678</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">US12345677</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>320</cbc:ID>
|
||||
<cbc:Name>Chichi Lboukla</cbc:Name>
|
||||
<cbc:Telephone>+18709310505</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/00001</cbc:InstructionID>
|
||||
<cbc:PaymentID>INV/2022/00001</cbc:PaymentID>
|
||||
</cac:PaymentMeans>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">150.00</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="SAR">1000.00</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="SAR">150.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">150.00</cbc:TaxAmount>
|
||||
</cac:TaxTotal>
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">1000.00</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="SAR">1000.00</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="SAR">1150.00</cbc:TaxInclusiveAmount>
|
||||
<cbc:AllowanceTotalAmount currencyID="SAR">0.00</cbc:AllowanceTotalAmount>
|
||||
<cbc:PrepaidAmount currencyID="SAR">115.00</cbc:PrepaidAmount>
|
||||
<cbc:PayableAmount currencyID="SAR">1035.00</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">1000.00</cbc:LineExtensionAmount>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">150.00</cbc:TaxAmount>
|
||||
<cbc:RoundingAmount currencyID="SAR">1150.00</cbc:RoundingAmount>
|
||||
</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">1000.0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>2</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="SAR">0.00</cbc:LineExtensionAmount>
|
||||
<cac:DocumentReference>
|
||||
<cbc:ID>INV/2022/00001</cbc:ID>
|
||||
<cbc:IssueDate>2022-09-05</cbc:IssueDate>
|
||||
<cbc:IssueTime>08:20:02</cbc:IssueTime>
|
||||
<cbc:DocumentTypeCode>386</cbc:DocumentTypeCode>
|
||||
</cac:DocumentReference>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="SAR">0.00</cbc:TaxAmount>
|
||||
<cbc:RoundingAmount currencyID="SAR">0.00</cbc:RoundingAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="SAR">100.00</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="SAR">15.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>Down Payment: 09 2022 (Draft)</cbc:Description>
|
||||
<cbc:Name>Down payment</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">0</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>
|
||||
|
|
@ -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'), ('type', '!=', 'sale')]}">
|
||||
<group>
|
||||
<group>
|
||||
<field name="l10n_sa_serial_number" readonly="1" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</group>
|
||||
<p groups="base.group_system">
|
||||
<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" groups="base.group_system"
|
||||
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" groups="base.group_system"
|
||||
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" groups="base.group_system"
|
||||
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" groups="base.group_system"
|
||||
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" groups="base.group_system"
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<?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">
|
||||
<table t-if="o.company_id.country_id.code == 'SA' and o.currency_id != o.company_id.currency_id"
|
||||
id="sar_amounts" style="page-break-inside: avoid;" class="clearfix mx-auto mt-3 table-sm table-borderless">
|
||||
<t t-set="curr_date" t-value="o.invoice_date or datetime.datetime.today()"></t>
|
||||
<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, curr_date)"/>
|
||||
<tr>
|
||||
<td class="w-25 text-start" dir="rtl"><strong>سعر الصرف</strong></td>
|
||||
<td class="w-25 text-start" dir="rtl"><strong>الإجمالي الفرعي بالريال السعودي</strong></td>
|
||||
<td class="w-25 text-start" dir="rtl"><strong>مبلغ ضريبة القيمة المضافة بالريال السعودي</strong></td>
|
||||
<td class="w-25 text-start" dir="rtl"><strong>الإجمالي بالريال السعودي</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="w-25"><span class="fw-bold">Exchange Rate</span></td>
|
||||
<td class="w-25"><span class="fw-bold">Subtotal</span></td>
|
||||
<td class="w-25"><span class="fw-bold">VAT Amount</span></td>
|
||||
<td class="w-25"><span class="fw-bold">Total</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><p class="m-0" t-esc="sar_rate" t-options='{"widget": "float", "precision": 5}'/></td>
|
||||
<td><p class="m-0" t-esc="o.amount_untaxed_signed"
|
||||
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/></td>
|
||||
<td><p class="m-0"
|
||||
t-esc="o.currency_id._convert(o.amount_tax, o.company_id.currency_id, o.company_id, curr_date)"
|
||||
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/></td>
|
||||
<td><p class="m-0" t-esc="o.amount_total_signed"
|
||||
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</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="o.move_type == 'out_invoice' and o.state == 'posted'"]" 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="o.move_type == 'out_invoice' and o.state == 'posted'"]" 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 & Penalties</strong>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import account_move_reversal
|
||||
from . import account_debit_note
|
||||
from . import l10n_sa_edi_otp_wizard
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_account_move_reversal_inherit_l10n_sa_edi" model="ir.ui.view">
|
||||
<field name="name">account.move.reversal.form.inherit.l10n_sa_edi</field>
|
||||
<field name="inherit_id" ref="account.view_account_move_reversal"/>
|
||||
<field name="model">account.move.reversal</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="reason" position="replace">
|
||||
<field name="country_code" invisible="1"/>
|
||||
<field name="reason" string="Reason" attrs="{'invisible': [('move_type', '=', 'entry'), ('country_code', '!=', 'SA')], 'required': [('country_code', '=', 'SA')]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
47
odoo-bringout-oca-ocb-l10n_sa_edi/pyproject.toml
Normal file
47
odoo-bringout-oca-ocb-l10n_sa_edi/pyproject.toml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-ocb-l10n_sa_edi"
|
||||
version = "16.0.0"
|
||||
description = "Saudi Arabia - E-invoicing -
|
||||
E-Invoicing, Universal Business Language
|
||||
"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-account_edi_ubl_cii>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-account_debit_note>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-l10n_sa>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-base_vat>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["l10n_sa_edi"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue