oca-ocb-l10n_me-africa/odoo-bringout-oca-ocb-l10n_sa/l10n_sa/models/account_move.py
2025-08-29 15:20:53 +02:00

86 lines
4.8 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import float_repr, format_datetime
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_sa_delivery_date = fields.Date(string='Supply Date',
default=fields.Date.context_today,
copy=False,
readonly=True,
states={'draft': [('readonly', False)]},
help="""Date when the supply of the goods and services is performed.
You can set this date to when you want ZATCA to recognize the VAT Due Liability as ZATCA will consider the earlier of this date and the Issue Date.
In case of multiple deliveries, you should take the date of the latest one.""")
l10n_sa_show_delivery_date = fields.Boolean(compute='_compute_show_delivery_date')
l10n_sa_qr_code_str = fields.Char(string='Zatka QR Code', compute='_compute_qr_code_str')
l10n_sa_confirmation_datetime = fields.Datetime(string='Confirmation Date',
readonly=True,
copy=False,
help="""Date when the invoice is confirmed and posted.
In other words, it is the date on which the invoice is generated as final document (after securing all internal approvals).""")
@api.depends('country_code', 'move_type')
def _compute_show_delivery_date(self):
for move in self:
move.l10n_sa_show_delivery_date = move.country_code == 'SA' and move.move_type in ('out_invoice', 'out_refund')
@api.depends('amount_total_signed', 'amount_tax_signed', 'l10n_sa_confirmation_datetime', 'company_id', 'company_id.vat')
def _compute_qr_code_str(self):
""" 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
"""
def get_qr_encoding(tag, field):
company_name_byte_array = field.encode()
company_name_tag_encoding = tag.to_bytes(length=1, byteorder='big')
company_name_length_encoding = len(company_name_byte_array).to_bytes(length=1, byteorder='big')
return company_name_tag_encoding + company_name_length_encoding + company_name_byte_array
for record in self:
qr_code_str = ''
if record.l10n_sa_confirmation_datetime and record.company_id.vat:
seller_name_enc = get_qr_encoding(1, record.company_id.display_name)
company_vat_enc = get_qr_encoding(2, record.company_id.vat)
time_sa = fields.Datetime.context_timestamp(self.with_context(tz='Asia/Riyadh'), record.l10n_sa_confirmation_datetime)
timestamp_enc = get_qr_encoding(3, time_sa.isoformat())
totals = record._get_l10n_sa_totals()
invoice_total_enc = get_qr_encoding(4, float_repr(abs(totals['total_amount']), 2))
total_vat_enc = get_qr_encoding(5, float_repr(abs(totals['total_tax']), 2))
str_to_encode = seller_name_enc + company_vat_enc + timestamp_enc + invoice_total_enc + total_vat_enc
qr_code_str = base64.b64encode(str_to_encode).decode()
record.l10n_sa_qr_code_str = qr_code_str
def _post(self, soft=True):
res = super()._post(soft)
for record in self:
if record.country_code == 'SA' and record.move_type in ('out_invoice', 'out_refund'):
if not record.l10n_sa_show_delivery_date:
raise UserError(_('Delivery Date cannot be empty'))
if not record.l10n_sa_confirmation_datetime:
record.l10n_sa_confirmation_datetime = fields.Datetime.now()
return res
def get_l10n_sa_confirmation_datetime_sa_tz(self):
self.ensure_one()
return format_datetime(self.env, self.l10n_sa_confirmation_datetime, tz='Asia/Riyadh', dt_format='Y-MM-dd\nHH:mm:ss')
def _l10n_sa_reset_confirmation_datetime(self):
self.filtered(lambda m: m.country_code == 'SA').l10n_sa_confirmation_datetime = False
def button_draft(self):
self._l10n_sa_reset_confirmation_datetime()
super().button_draft()
def _get_l10n_sa_totals(self):
self.ensure_one()
return {
'total_amount': self.amount_total_signed,
'total_tax': self.amount_tax_signed,
}