mirror of
https://github.com/bringout/oca-ocb-l10n_me-africa.git
synced 2026-04-27 23:22:03 +02:00
19.0 vanilla
This commit is contained in:
parent
c5006a6999
commit
80293571e7
420 changed files with 21812 additions and 44297 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
|
||||
Saudi Arabia POS Localization
|
||||
===========================================================
|
||||
|
||||
|
||||
## Installation
|
||||
|
|
@ -12,38 +13,15 @@ pip install odoo-bringout-oca-ocb-l10n_sa_pos
|
|||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- l10n_gcc_pos
|
||||
- l10n_sa
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Saudi Arabia - Point of Sale
|
||||
- **Version**: N/A
|
||||
- **Category**: Accounting/Localizations/Point of Sale
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `l10n_sa_pos`.
|
||||
- Repository: https://github.com/OCA/OCB
|
||||
- Branch: 19.0
|
||||
- Path: addons/l10n_sa_pos
|
||||
|
||||
## 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
|
||||
This package preserves the original LGPL-3 license.
|
||||
|
|
|
|||
|
|
@ -2,23 +2,28 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Saudi Arabia - Point of Sale',
|
||||
'author': 'Odoo S.A.',
|
||||
'category': 'Accounting/Localizations/Point of Sale',
|
||||
'icon': '/l10n_sa/static/description/icon.png',
|
||||
'description': """
|
||||
Saudi Arabia POS Localization
|
||||
===========================================================
|
||||
""",
|
||||
'author': 'Odoo S.A.',
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'l10n_gcc_pos',
|
||||
'l10n_sa',
|
||||
],
|
||||
'assets': {
|
||||
'point_of_sale.assets': [
|
||||
'point_of_sale._assets_pos': [
|
||||
'web/static/lib/zxing-library/zxing-library.js',
|
||||
'l10n_sa_pos/static/src/js/models.js',
|
||||
'l10n_sa_pos/static/src/xml/OrderReceipt.xml',
|
||||
'l10n_sa_pos/static/src/css/pos_receipt.css',
|
||||
'l10n_sa_pos/static/src/**/*',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'l10n_sa_pos/static/tests/tours/**/*',
|
||||
],
|
||||
'web.assets_unit_tests': [
|
||||
'l10n_sa_pos/static/src/app/utils/qr.js',
|
||||
'l10n_sa_pos/static/tests/unit/**/*',
|
||||
]
|
||||
},
|
||||
'auto_install': True,
|
||||
|
|
|
|||
130
odoo-bringout-oca-ocb-l10n_sa_pos/l10n_sa_pos/i18n/ar.po
Normal file
130
odoo-bringout-oca-ocb-l10n_sa_pos/l10n_sa_pos/i18n/ar.po
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_sa_pos
|
||||
#
|
||||
# Weblate <noreply-mt-weblate@weblate.org>, 2025.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 19.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-30 19:09+0000\n"
|
||||
"PO-Revision-Date: 2025-11-17 03:15+0000\n"
|
||||
"Last-Translator: Weblate <noreply-mt-weblate@weblate.org>\n"
|
||||
"Language-Team: Arabic <https://translate.odoo.com/projects/odoo-19-l10n/"
|
||||
"l10n_sa_pos/ar/>\n"
|
||||
"Language: ar\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
|
||||
"X-Generator: Weblate 5.12.2\n"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/app/add_zatca_refund_reason_popup/add_zatca_refund_reason_popup.xml:0
|
||||
msgid "Additional Refund Information"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-3
|
||||
msgid ""
|
||||
"Amendment of the supply value which is pre-agreed upon between the supplier "
|
||||
"and consumer"
|
||||
msgstr "تعديل قيمة التوريد المتفق عليها مسبقًا بين المورد والمستهلك"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-1
|
||||
msgid ""
|
||||
"Cancellation or suspension of the supplies after its occurrence either "
|
||||
"wholly or partially"
|
||||
msgstr "إلغاء أو تعليق التوريدات بعد حدوثها سواء كليًا أو جزئيًا"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_config__display_name
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_order__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "اسم العرض"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_config__id
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_order__id
|
||||
msgid "ID"
|
||||
msgstr "المُعرف"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-5
|
||||
msgid "In case of change in Seller's or Buyer's information"
|
||||
msgstr "في حالة تغيير بيانات البائع أو المشتري"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-2
|
||||
msgid ""
|
||||
"In case of essential change or amendment in the supply, which leads to the "
|
||||
"change of the VAT due"
|
||||
msgstr ""
|
||||
"في حالة حدوث تغيير أو تعديل جوهري في التوريد، مما يؤدي إلى تغيير ضريبة "
|
||||
"القيمة المضافة المستحقة"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-4
|
||||
msgid "In case of goods or services refund"
|
||||
msgstr "في حالة رد سلع أو خدمات"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_order__l10n_sa_reason_value
|
||||
msgid "L10N Sa Reason Value"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/app/add_zatca_refund_reason_popup/add_zatca_refund_reason_popup.xml:0
|
||||
msgid "Ok"
|
||||
msgstr "موافق"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model,name:l10n_sa_pos.model_pos_config
|
||||
msgid "Point of Sale Configuration"
|
||||
msgstr "تهيئة نقطة البيع"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model,name:l10n_sa_pos.model_pos_order
|
||||
msgid "Point of Sale Orders"
|
||||
msgstr "طلبات نقطة البيع"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/overrides/components/order_receipt/order_receipt.xml:0
|
||||
msgid "Reason:"
|
||||
msgstr "السبب:"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/overrides/components/order_receipt/order_receipt.xml:0
|
||||
msgid "Reference:"
|
||||
msgstr "المرجع:"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-python
|
||||
#: code:addons/l10n_sa_pos/models/pos_order.py:0
|
||||
msgid ""
|
||||
"You cannot create a consolidated invoice for POS orders with different ZATCA "
|
||||
"refund reasons."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-python
|
||||
#: code:addons/l10n_sa_pos/models/pos_config.py:0
|
||||
msgid "You have to set a country in your company setting."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_order__l10n_sa_reason
|
||||
msgid "ZATCA Reason"
|
||||
msgstr "سبب هيئة الزكاة والضريبة والجمارك"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/app/add_zatca_refund_reason_popup/add_zatca_refund_reason_popup.xml:0
|
||||
msgid "ZATCA Refund Reason:"
|
||||
msgstr ""
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_sa_pos
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 19.0+e\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-30 19:09+0000\n"
|
||||
"PO-Revision-Date: 2025-12-30 19:09+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/app/add_zatca_refund_reason_popup/add_zatca_refund_reason_popup.xml:0
|
||||
msgid "Additional Refund Information"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-3
|
||||
msgid ""
|
||||
"Amendment of the supply value which is pre-agreed upon between the supplier "
|
||||
"and consumer"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-1
|
||||
msgid ""
|
||||
"Cancellation or suspension of the supplies after its occurrence either "
|
||||
"wholly or partially"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_config__display_name
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_order__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_config__id
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_order__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-5
|
||||
msgid "In case of change in Seller's or Buyer's information"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-2
|
||||
msgid ""
|
||||
"In case of essential change or amendment in the supply, which leads to the "
|
||||
"change of the VAT due"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields.selection,name:l10n_sa_pos.selection__pos_order__l10n_sa_reason__br-ksa-17-reason-4
|
||||
msgid "In case of goods or services refund"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_order__l10n_sa_reason_value
|
||||
msgid "L10N Sa Reason Value"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/app/add_zatca_refund_reason_popup/add_zatca_refund_reason_popup.xml:0
|
||||
msgid "Ok"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model,name:l10n_sa_pos.model_pos_config
|
||||
msgid "Point of Sale Configuration"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model,name:l10n_sa_pos.model_pos_order
|
||||
msgid "Point of Sale Orders"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/overrides/components/order_receipt/order_receipt.xml:0
|
||||
msgid "Reason:"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/overrides/components/order_receipt/order_receipt.xml:0
|
||||
msgid "Reference:"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-python
|
||||
#: code:addons/l10n_sa_pos/models/pos_order.py:0
|
||||
msgid ""
|
||||
"You cannot create a consolidated invoice for POS orders with different ZATCA"
|
||||
" refund reasons."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-python
|
||||
#: code:addons/l10n_sa_pos/models/pos_config.py:0
|
||||
msgid "You have to set a country in your company setting."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#: model:ir.model.fields,field_description:l10n_sa_pos.field_pos_order__l10n_sa_reason
|
||||
msgid "ZATCA Reason"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_sa_pos
|
||||
#. odoo-javascript
|
||||
#: code:addons/l10n_sa_pos/static/src/app/add_zatca_refund_reason_popup/add_zatca_refund_reason_popup.xml:0
|
||||
msgid "ZATCA Refund Reason:"
|
||||
msgstr ""
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import pos_order
|
||||
|
|
|
|||
|
|
@ -1,15 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class pos_config(models.Model):
|
||||
class PosConfig(models.Model):
|
||||
_inherit = 'pos.config'
|
||||
|
||||
def open_ui(self):
|
||||
for config in self:
|
||||
if not config.company_id.country_id:
|
||||
raise UserError(_("You have to set a country in your company setting."))
|
||||
return super(pos_config, self).open_ui()
|
||||
return super().open_ui()
|
||||
|
||||
@api.model
|
||||
def _load_pos_data_read(self, records, config):
|
||||
data = super()._load_pos_data_read(records, config)
|
||||
|
||||
if data and self.env.company.country_id.code == 'SA':
|
||||
l10n_sa_reason_field = self.env['ir.model.fields']._get('account.move', 'l10n_sa_reason')
|
||||
data[0]['_zatca_refund_reasons'] = [
|
||||
{'value': refund_reason.value, 'name': refund_reason.name}
|
||||
for refund_reason in l10n_sa_reason_field.selection_ids
|
||||
]
|
||||
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -1,14 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.l10n_sa.models.account_move import ADJUSTMENT_REASONS
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class POSOrder(models.Model):
|
||||
class PosOrder(models.Model):
|
||||
_inherit = 'pos.order'
|
||||
|
||||
l10n_sa_reason = fields.Selection(string="ZATCA Reason", selection=ADJUSTMENT_REASONS)
|
||||
l10n_sa_reason_value = fields.Char(compute='_compute_l10n_sa_reason_value')
|
||||
|
||||
def _prepare_invoice_vals(self):
|
||||
vals = super()._prepare_invoice_vals()
|
||||
if self.company_id.country_id.code == 'SA':
|
||||
vals.update({'l10n_sa_confirmation_datetime': self.date_order})
|
||||
mapped_reasons = self.mapped('l10n_sa_reason')
|
||||
if len(set(mapped_reasons)) > 1:
|
||||
raise UserError(_(
|
||||
"You cannot create a consolidated invoice for POS orders with different"
|
||||
" ZATCA refund reasons."
|
||||
))
|
||||
confirmation_datetime = self.date_order if len(self) == 1 else fields.Datetime.now()
|
||||
vals.update({
|
||||
'l10n_sa_confirmation_datetime': confirmation_datetime,
|
||||
'l10n_sa_reason': mapped_reasons[0] if mapped_reasons else False,
|
||||
})
|
||||
return vals
|
||||
|
||||
@api.depends("l10n_sa_reason")
|
||||
def _compute_l10n_sa_reason_value(self):
|
||||
for record in self:
|
||||
record.l10n_sa_reason_value = dict(self._fields['l10n_sa_reason']._description_selection(self.env)).get(record.l10n_sa_reason)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
import { Component, useState } from "@odoo/owl";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { usePos } from "@point_of_sale/app/hooks/pos_hook";
|
||||
|
||||
export class AddZatcaRefundReasonPopup extends Component {
|
||||
static template = "l10n_sa_pos.AddZatcaRefundReasonPopup";
|
||||
static components = { Dialog };
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
this.state = useState({
|
||||
l10n_sa_reason: this.props.order.l10n_sa_reason || "BR-KSA-17-reason-4",
|
||||
});
|
||||
}
|
||||
confirm() {
|
||||
this.props.getPayload(this.state);
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="l10n_sa_pos.AddZatcaRefundReasonPopup">
|
||||
<Dialog title.translate="Additional Refund Information">
|
||||
<div class="mb-3">
|
||||
<label for="zatca_refund_reason" class="form-label">ZATCA Refund Reason: </label>
|
||||
<select class="detail form-select" id="zatca_refund_reason" name="l10n_sa_reason" t-model="state.l10n_sa_reason">
|
||||
<t t-foreach="pos.config._zatca_refund_reasons" t-as="l10n_sa_reason" t-key="l10n_sa_reason.value">
|
||||
<option t-att-value="l10n_sa_reason.value"
|
||||
t-att-selected="l10n_sa_reason.value === state.l10n_sa_reason ? 'selected' : undefined">
|
||||
<t t-out="l10n_sa_reason.name"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-primary o-default-button" t-on-click="confirm">Ok</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { formatDateTime } from "@web/core/l10n/dates";
|
||||
|
||||
export function computeSAQRCode(name, vat, date_isostring, amount_total, amount_tax) {
|
||||
/* Generate the qr code for Saudi e-invoicing. Specs are available at the following link at page 23
|
||||
https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20210528_ZATCA_Electronic_Invoice_Security_Features_Implementation_Standards_vShared.pdf
|
||||
*/
|
||||
const ksa_timestamp = formatDateTime(date_isostring, {
|
||||
tz: "Asia/Riyadh",
|
||||
format: "MM/dd/yyyy, HH:mm:ss",
|
||||
});
|
||||
|
||||
const seller_name_enc = _compute_qr_code_field(1, name);
|
||||
const company_vat_enc = _compute_qr_code_field(2, vat);
|
||||
const timestamp_enc = _compute_qr_code_field(3, ksa_timestamp);
|
||||
const invoice_total_enc = _compute_qr_code_field(4, amount_total.toString());
|
||||
const total_vat_enc = _compute_qr_code_field(5, amount_tax.toString());
|
||||
|
||||
const str_to_encode = seller_name_enc.concat(
|
||||
company_vat_enc,
|
||||
timestamp_enc,
|
||||
invoice_total_enc,
|
||||
total_vat_enc
|
||||
);
|
||||
|
||||
let binary = "";
|
||||
for (let i = 0; i < str_to_encode.length; i++) {
|
||||
binary += String.fromCharCode(str_to_encode[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
function _compute_qr_code_field(tag, field) {
|
||||
const textEncoder = new TextEncoder();
|
||||
const name_byte_array = Array.from(textEncoder.encode(field));
|
||||
const name_tag_encoding = [tag];
|
||||
const name_length_encoding = [name_byte_array.length];
|
||||
return name_tag_encoding.concat(name_length_encoding, name_byte_array);
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
odoo.define('l10n_sa_pos.pos', function (require) {
|
||||
"use strict";
|
||||
|
||||
var { Order } = require('point_of_sale.models');
|
||||
var Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosL10nSAOrder = (Order) => class PosL10nSAOrder extends Order {
|
||||
export_for_printing() {
|
||||
var result = super.export_for_printing(...arguments);
|
||||
if (this.pos.company.country && this.pos.company.country.code === 'SA') {
|
||||
result.is_settlement = this.is_settlement();
|
||||
if (!result.is_settlement) {
|
||||
const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter()
|
||||
let qr_values = this.compute_sa_qr_code(result.company.name, result.company.vat, result.date.isostring, result.total_with_tax, result.total_tax);
|
||||
let qr_code_svg = new XMLSerializer().serializeToString(codeWriter.write(qr_values, 150, 150));
|
||||
result.qr_code = "data:image/svg+xml;base64," + window.btoa(qr_code_svg);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* If the order is empty (there are no products)
|
||||
* and all "pay_later" payments are negative,
|
||||
* we are settling a customer's account.
|
||||
* If the module pos_settle_due is not installed,
|
||||
* the function always returns false (since "pay_later" doesn't exist)
|
||||
* @returns {boolean} true if the current order is a settlement, else false
|
||||
*/
|
||||
is_settlement() {
|
||||
return this.is_empty() &&
|
||||
!!this.paymentlines.filter(paymentline => paymentline.payment_method.type === "pay_later" && paymentline.amount < 0).length;
|
||||
}
|
||||
|
||||
compute_sa_qr_code(name, vat, date_isostring, amount_total, amount_tax) {
|
||||
/* Generate the qr code for Saudi e-invoicing. Specs are available at the following link at page 23
|
||||
https://zatca.gov.sa/ar/E-Invoicing/SystemsDevelopers/Documents/20210528_ZATCA_Electronic_Invoice_Security_Features_Implementation_Standards_vShared.pdf
|
||||
*/
|
||||
const seller_name_enc = this._compute_qr_code_field(1, name);
|
||||
const company_vat_enc = this._compute_qr_code_field(2, vat);
|
||||
const timestamp_enc = this._compute_qr_code_field(3, date_isostring);
|
||||
const invoice_total_enc = this._compute_qr_code_field(4, amount_total.toString());
|
||||
const total_vat_enc = this._compute_qr_code_field(5, amount_tax.toString());
|
||||
|
||||
const str_to_encode = seller_name_enc.concat(company_vat_enc, timestamp_enc, invoice_total_enc, total_vat_enc);
|
||||
|
||||
let binary = '';
|
||||
for (let i = 0; i < str_to_encode.length; i++) {
|
||||
binary += String.fromCharCode(str_to_encode[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
_compute_qr_code_field(tag, field) {
|
||||
const textEncoder = new TextEncoder();
|
||||
const name_byte_array = Array.from(textEncoder.encode(field));
|
||||
const name_tag_encoding = [tag];
|
||||
const name_length_encoding = [name_byte_array.length];
|
||||
return name_tag_encoding.concat(name_length_encoding, name_byte_array);
|
||||
}
|
||||
|
||||
}
|
||||
Registries.Model.extend(Order, PosL10nSAOrder);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-inherit="point_of_sale.ReceiptHeader" t-inherit-mode="extension">
|
||||
<xpath expr="//img[hasclass('pos-receipt-logo')]" position="after">
|
||||
<t t-if="order.isSACompany() and !order.isSettlement()">
|
||||
<t t-set="asQrCode" t-value="order.generateQrcode()"/>
|
||||
<img t-if="asQrCode" id="qrcode" t-att-src="asQrCode" class="pos-receipt-qrcode"/>
|
||||
<br/>
|
||||
</t>
|
||||
</xpath>
|
||||
<t t-set="show_title" position="after">
|
||||
<t t-set="show_title" t-if="order.isSACompany()" t-value="order.state !== 'draft'"/>
|
||||
</t>
|
||||
<t t-set="show_simplified_title" position="after">
|
||||
<t t-set="show_simplified_title" t-if="order.isSACompany()" t-value="!order.isSettlement() and order.isSimplified"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension">
|
||||
<span name="subtotal_span_dual" position="before">
|
||||
<span name="subtotal_span_dual_sa" t-elif="order.isSACompany() and order.config_id.l10n_gcc_dual_language_receipt" class="text-nowrap mw-100" t-translation="off">
|
||||
Subtotal / المبلغ الخاضع للضريبة
|
||||
<br/>
|
||||
(غير شامل ضريبة القيمة المضافة)
|
||||
</span>
|
||||
</span>
|
||||
<span name="total_span_dual" position="before">
|
||||
<span name="total_span_dual_sa" t-elif="order.isSACompany() and order.config_id.l10n_gcc_dual_language_receipt" class="label-total" t-translation="off">
|
||||
Total / إجمالي قيمة الفاتورة
|
||||
<br/>
|
||||
(شامل ضريبة القيمة المضافة)
|
||||
</span>
|
||||
</span>
|
||||
<xpath expr="//div[@t-if='order.company.point_of_sale_use_ticket_qr_code and order.finalized']//div[hasclass('w-100')]" position="inside">
|
||||
<div name="order_reference" t-if="order.isSACompany()">
|
||||
<t t-if="!order.config_id.l10n_gcc_dual_language_receipt">
|
||||
<span>Reference:</span><t t-out="order.pos_reference"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-out="order.pos_reference"/><span t-translation="off"> :المرجع</span><br/>
|
||||
<span t-translation="off">Reference: </span><t t-out="order.pos_reference"/>
|
||||
</t>
|
||||
</div>
|
||||
<div name="order_reason" t-if="order.isSACompany() and order.isRefund and order.l10n_sa_reason_value">
|
||||
<t t-if="!order.config_id.l10n_gcc_dual_language_receipt">
|
||||
<span>Reason:</span><br/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-translation="off"> :سبب إصدار الإشعار من الأسباب التالية</span><br/>
|
||||
<span t-translation="off">Reason: </span><br/>
|
||||
</t>
|
||||
<span t-out="order.l10n_sa_reason_value"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-if='company.street']/.." position="attributes">
|
||||
<attribute name="t-if">!order.isSACompany()</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-if='company.street']/.." position="after">
|
||||
<div name="custom_address_block" t-else="">
|
||||
<t t-if="company.street" t-out="company.street"/>
|
||||
<t t-if="company.street2" t-out="', ' + company.street2"/>
|
||||
<t t-if="company.street or company.street2"><br/></t>
|
||||
|
||||
<t t-if="company.city" t-out="company.city"/>
|
||||
<t t-if="company.state_id?.code" t-out="', ' + company.state_id.code"/>
|
||||
<t t-if="company.zip" t-out="', ' + company.zip"/>
|
||||
<t t-if="company.city or company.state_id?.code or company.zip"><br/></t>
|
||||
|
||||
<t t-if="company.country_id" t-out="company.country_id?.name"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { AddZatcaRefundReasonPopup } from "@l10n_sa_pos/app/add_zatca_refund_reason_popup/add_zatca_refund_reason_popup";
|
||||
import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen";
|
||||
import { makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(TicketScreen.prototype, {
|
||||
async addAdditionalRefundInfo(order, destinationOrder) {
|
||||
if (this.pos.company.country_id.code === "SA") {
|
||||
const payload = await makeAwaitable(this.dialog, AddZatcaRefundReasonPopup, {
|
||||
order: destinationOrder,
|
||||
});
|
||||
if (payload) {
|
||||
destinationOrder.l10n_sa_reason = payload.l10n_sa_reason;
|
||||
destinationOrder.to_invoice = true;
|
||||
}
|
||||
}
|
||||
return super.addAdditionalRefundInfo(...arguments);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { computeSAQRCode } from "@l10n_sa_pos/app/utils/qr";
|
||||
|
||||
patch(PosOrder.prototype, {
|
||||
isSACompany() {
|
||||
return this.company.country_id?.code === "SA";
|
||||
},
|
||||
|
||||
generateQrcode() {
|
||||
if (this.isSACompany()) {
|
||||
if (!this.isSettlement()) {
|
||||
const company = this.company;
|
||||
const codeWriter = new window.ZXing.BrowserQRCodeSvgWriter();
|
||||
const qr_values = this.compute_sa_qr_code(
|
||||
company.name,
|
||||
company.vat,
|
||||
this.date_order,
|
||||
this.priceIncl,
|
||||
this.amountTaxes
|
||||
);
|
||||
const qr_code_svg = new XMLSerializer().serializeToString(
|
||||
codeWriter.write(qr_values, 200, 200)
|
||||
);
|
||||
return "data:image/svg+xml;base64," + window.btoa(qr_code_svg);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
compute_sa_qr_code(name, vat, date_isostring, amount_total, amount_tax) {
|
||||
return computeSAQRCode(name, vat, date_isostring, amount_total, amount_tax);
|
||||
},
|
||||
get isSimplified() {
|
||||
return !this?.partner_id?.is_company && this.company_id.country_id?.code === "SA";
|
||||
},
|
||||
});
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//img[hasclass('pos-receipt-logo')]" position="after">
|
||||
<t t-if="receipt.is_gcc_country and !receipt.is_settlement">
|
||||
<img t-if="receipt.qr_code" id="qrcode" t-att-src="receipt.qr_code" class="pos-receipt-qrcode"/>
|
||||
<br/>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//span[@id='title_english']" position="replace">
|
||||
<t t-if="!receipt.is_settlement">
|
||||
<span id="title_english">Simplified Tax Invoice</span>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//span[@id='title_arabic']" position="replace">
|
||||
<t t-if="!receipt.is_settlement">
|
||||
<span id="title_arabic">فاتورة ضريبية مبسطة</span>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
|
||||
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
|
||||
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
|
||||
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry.category("web_tour.tours").add("test_sa_qr_is_shown", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
ProductScreen.addOrderline("Small Shelf", "1"),
|
||||
ProductScreen.clickPayButton(),
|
||||
PaymentScreen.clickPaymentMethod("Cash"),
|
||||
PaymentScreen.clickValidate(),
|
||||
{
|
||||
trigger: "#qrcode.pos-receipt-qrcode",
|
||||
content: "QR code should be visible on the receipt.",
|
||||
},
|
||||
].flat(),
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { computeSAQRCode } from "@l10n_sa_pos/app/utils/qr";
|
||||
const { DateTime } = luxon;
|
||||
|
||||
describe("SA QR Code", () => {
|
||||
test("check QR format", () => {
|
||||
const date = DateTime.fromISO("2025-03-07T10:15:17");
|
||||
const qrEncoded = computeSAQRCode("SA Company", "123456789012345", date, 100.0, 0);
|
||||
const expected =
|
||||
"AQpTQSBDb21wYW55Ag8xMjM0NTY3ODkwMTIzNDUDFDAzLzA3LzIwMjUsIDEyOjE1OjE3BAMxMDAFATA=";
|
||||
|
||||
expect(qrEncoded).toBe(expected, {
|
||||
message: `QR code mismatch: expected "${expected}", got "${qrEncoded}", make sure the timezone is respected`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_sa_pos
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.point_of_sale.tests.common import TestPoSCommon
|
||||
from odoo.addons.point_of_sale.tests.test_generic_localization import TestGenericLocalization
|
||||
from odoo.tests import tagged
|
||||
from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon
|
||||
from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'post_install_l10n')
|
||||
class TestGenericSA(TestGenericLocalization):
|
||||
@classmethod
|
||||
@AccountTestInvoicingCommon.setup_country('sa')
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
if cls.env['ir.module.module']._get('l10n_sa_edi').state == 'installed':
|
||||
cls.skipTest(cls, "l10n_sa_edi should not be installed")
|
||||
cls.main_pos_config.company_id.name = 'Generic SA'
|
||||
cls.company.write({
|
||||
'email': 'info@company.saexample.com',
|
||||
'phone': '+966 51 234 5678',
|
||||
'street2': 'Testomania',
|
||||
'vat': '311111111111113',
|
||||
'state_id': cls.env['res.country.state'].create({
|
||||
'name': 'Riyadh',
|
||||
'code': 'RYA',
|
||||
'country_id': cls.company.country_id.id
|
||||
}),
|
||||
'street': 'Al Amir Mohammed Bin Abdul Aziz Street',
|
||||
'city': 'المدينة المنورة',
|
||||
'zip': '42317',
|
||||
})
|
||||
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestUi(TestPointOfSaleHttpCommon):
|
||||
|
||||
@classmethod
|
||||
@AccountEdiTestCommon.setup_edi_format('l10n_sa_edi.edi_sa_zatca')
|
||||
@AccountEdiTestCommon.setup_country('sa')
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Setup company
|
||||
cls.company.write({
|
||||
'name': 'SA Company Test',
|
||||
'email': 'info@company.saexample.com',
|
||||
'phone': '+966 51 234 5678',
|
||||
'street2': 'Testomania',
|
||||
'vat': '311111111111113',
|
||||
'state_id': cls.env['res.country.state'].create({
|
||||
'name': 'Riyadh',
|
||||
'code': 'RYA',
|
||||
'country_id': cls.company.country_id.id
|
||||
}),
|
||||
'street': 'Al Amir Mohammed Bin Abdul Aziz Street',
|
||||
'city': 'المدينة المنورة',
|
||||
'zip': '42317',
|
||||
})
|
||||
|
||||
def test_sa_qr_is_shown(self):
|
||||
"""
|
||||
Tests that the Saudi Arabia's timezone is applied on the QR code generated at the
|
||||
end of an order.
|
||||
"""
|
||||
if self.env['ir.module.module']._get('l10n_sa_edi').state == 'installed':
|
||||
self.skipTest("The needed configuration for e-invoices is not available")
|
||||
self.main_pos_config.with_user(self.pos_admin).open_ui()
|
||||
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'test_sa_qr_is_shown', login="pos_admin")
|
||||
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestSaPosInvoice(TestPoSCommon, TestPointOfSaleHttpCommon):
|
||||
|
||||
def test_consolidate_invoices_for_same_customer(self):
|
||||
"""
|
||||
Test that consolidated invoicing for multiple POS orders of the same customer
|
||||
succeeds and sets the confirmation datetime for a Saudi Arabia company.
|
||||
"""
|
||||
self.config = self.basic_config
|
||||
self.env.company.country_id = self.env.ref('base.sa')
|
||||
|
||||
self.open_new_session()
|
||||
pos_orders = sum(self._create_orders([
|
||||
{
|
||||
'pos_order_lines_ui_args': [(self.product_a, 1)],
|
||||
'customer': self.customer,
|
||||
'is_invoiced': False,
|
||||
}
|
||||
for _ in range(2)
|
||||
]).values(), self.env['pos.order'])
|
||||
|
||||
self.env['pos.make.invoice'].create({"consolidated_billing": True}).with_context({
|
||||
"active_ids": pos_orders.ids
|
||||
}).action_create_invoices()
|
||||
invoice = pos_orders.account_move
|
||||
|
||||
self.assertTrue(invoice, "A consolidated invoice should have been created")
|
||||
self.assertTrue(
|
||||
invoice.l10n_sa_confirmation_datetime,
|
||||
"The consolidated invoice should have l10n_sa_confirmation_datetime set"
|
||||
)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-ocb-l10n_sa_pos"
|
||||
version = "16.0.0"
|
||||
description = "Saudi Arabia - Point of Sale - Odoo addon"
|
||||
description = "Saudi Arabia - Point of Sale -
|
||||
Odoo addon
|
||||
"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-l10n_gcc_pos>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-l10n_sa>=16.0.0",
|
||||
"odoo-bringout-oca-ocb-l10n_gcc_pos>=19.0.0",
|
||||
"odoo-bringout-oca-ocb-l10n_sa>=19.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
|
|
@ -17,7 +19,7 @@ classifiers = [
|
|||
"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.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue