mirror of
https://github.com/bringout/oca-ocb-l10n_me-africa.git
synced 2026-04-28 01:02:01 +02:00
19.0 vanilla
This commit is contained in:
parent
c5006a6999
commit
80293571e7
420 changed files with 21812 additions and 44297 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import account_chart_template
|
||||
from . import account_move
|
||||
from . import ir_attachment
|
||||
from . import template_sa
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountChartTemplate(models.Model):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
def _prepare_all_journals(self, acc_template_ref, company, journals_dict=None):
|
||||
""" If Saudi Arabia chart, we add 3 new journals Tax Adjustments, IFRS 16 and Zakat"""
|
||||
if self == self.env.ref('l10n_sa.sa_chart_template_standard'):
|
||||
if not journals_dict:
|
||||
journals_dict = []
|
||||
journals_dict.extend(
|
||||
[{'name': 'Tax Adjustments', 'company_id': company.id, 'code': 'TA', 'type': 'general',
|
||||
'favorite': True, 'sequence': 1},
|
||||
{'name': 'IFRS 16 Right of Use Asset', 'company_id': company.id, 'code': 'IFRS', 'type': 'general',
|
||||
'favorite': True, 'sequence': 10},
|
||||
{'name': 'Zakat', 'company_id': company.id, 'code': 'ZAKAT', 'type': 'general', 'favorite': True,
|
||||
'sequence': 10}])
|
||||
return super()._prepare_all_journals(acc_template_ref, company, journals_dict=journals_dict)
|
||||
|
||||
def _load_template(self, company, code_digits=None, account_ref=None, taxes_ref=None):
|
||||
account_ref, taxes_ref = super(AccountChartTemplate, self)._load_template(company=company,
|
||||
code_digits=code_digits,
|
||||
account_ref=account_ref,
|
||||
taxes_ref=taxes_ref)
|
||||
if self == self.env.ref('l10n_sa.sa_chart_template_standard'):
|
||||
ifrs_journal_id = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'IFRS')], limit=1)
|
||||
if ifrs_journal_id:
|
||||
ifrs_account_ids = [self.env.ref('l10n_sa.sa_account_100101').id,
|
||||
self.env.ref('l10n_sa.sa_account_100102').id,
|
||||
self.env.ref('l10n_sa.sa_account_400070').id]
|
||||
ifrs_accounts = self.env['account.account'].browse([account_ref.get(id) for id in ifrs_account_ids])
|
||||
for account in ifrs_accounts:
|
||||
account.allowed_journal_ids = [(4, ifrs_journal_id.id, 0)]
|
||||
zakat_journal_id = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'ZAKAT')], limit=1)
|
||||
if zakat_journal_id:
|
||||
zakat_account_ids = [self.env.ref('l10n_sa.sa_account_201019').id,
|
||||
self.env.ref('l10n_sa.sa_account_400072').id]
|
||||
zakat_accounts = self.env['account.account'].browse([account_ref.get(id) for id in zakat_account_ids])
|
||||
for account in zakat_accounts:
|
||||
account.allowed_journal_ids = [(4, zakat_journal_id.id, 0)]
|
||||
return account_ref, taxes_ref
|
||||
|
|
@ -1,35 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from pytz import timezone, utc
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_repr, format_datetime
|
||||
|
||||
ADJUSTMENT_REASONS = [
|
||||
("BR-KSA-17-reason-1", "Cancellation or suspension of the supplies after its occurrence either wholly or partially"),
|
||||
("BR-KSA-17-reason-2", "In case of essential change or amendment in the supply, which leads to the change of the VAT due"),
|
||||
("BR-KSA-17-reason-3", "Amendment of the supply value which is pre-agreed upon between the supplier and consumer"),
|
||||
("BR-KSA-17-reason-4", "In case of goods or services refund"),
|
||||
("BR-KSA-17-reason-5", "In case of change in Seller's or Buyer's information"),
|
||||
]
|
||||
|
||||
|
||||
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',
|
||||
l10n_sa_qr_code_str = fields.Char(string='Zatka QR Code', compute='_compute_qr_code_str', compute_sudo=True)
|
||||
l10n_sa_show_reason = fields.Boolean(compute="_compute_show_l10n_sa_reason")
|
||||
l10n_sa_reason = fields.Selection(string="ZATCA Reason", selection=ADJUSTMENT_REASONS, copy=False)
|
||||
l10n_sa_confirmation_datetime = fields.Datetime(string='ZATCA Issue 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).""")
|
||||
help="""Date on which the invoice is generated as final document (after securing all internal approvals).""")
|
||||
|
||||
def _get_name_invoice_report(self):
|
||||
# EXTENDS account
|
||||
self.ensure_one()
|
||||
if self.company_id.country_code == 'SA':
|
||||
return 'l10n_sa.l10n_sa_report_invoice_document'
|
||||
return super()._get_name_invoice_report()
|
||||
|
||||
def _l10n_gcc_get_invoice_title(self):
|
||||
# EXTENDS l10n_gcc_invoice
|
||||
self.ensure_one()
|
||||
if self.company_id.country_code != "SA":
|
||||
return super()._l10n_gcc_get_invoice_title()
|
||||
|
||||
if self._l10n_sa_is_simplified():
|
||||
return self.env._("Simplified Tax Invoice")
|
||||
|
||||
return self.env._("Tax Invoice")
|
||||
|
||||
@api.depends('country_code', 'move_type')
|
||||
def _compute_show_delivery_date(self):
|
||||
# EXTENDS 'account'
|
||||
super()._compute_show_delivery_date()
|
||||
for move in self:
|
||||
move.l10n_sa_show_delivery_date = move.country_code == 'SA' and move.move_type in ('out_invoice', 'out_refund')
|
||||
if move.country_code == 'SA':
|
||||
move.show_delivery_date = move.is_sale_document()
|
||||
|
||||
@api.depends('amount_total_signed', 'amount_tax_signed', 'l10n_sa_confirmation_datetime', 'company_id', 'company_id.vat')
|
||||
def _compute_qr_code_str(self):
|
||||
|
|
@ -48,7 +71,7 @@ class AccountMove(models.Model):
|
|||
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())
|
||||
timestamp_enc = get_qr_encoding(3, time_sa.strftime(self._get_iso_format_asia_riyadh_date('T')))
|
||||
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))
|
||||
|
|
@ -59,12 +82,14 @@ class AccountMove(models.Model):
|
|||
|
||||
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()
|
||||
for move in self:
|
||||
if move.country_code == 'SA' and move.is_sale_document():
|
||||
vals = {}
|
||||
if not move.l10n_sa_confirmation_datetime:
|
||||
vals['l10n_sa_confirmation_datetime'] = self._get_normalized_l10n_sa_confirmation_datetime(move.invoice_date)
|
||||
if not move.delivery_date:
|
||||
vals['delivery_date'] = move.invoice_date
|
||||
move.write(vals)
|
||||
return res
|
||||
|
||||
def get_l10n_sa_confirmation_datetime_sa_tz(self):
|
||||
|
|
@ -72,11 +97,20 @@ class AccountMove(models.Model):
|
|||
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
|
||||
for move in self.filtered(lambda m: m.country_code == 'SA'):
|
||||
move.l10n_sa_confirmation_datetime = False
|
||||
|
||||
def button_draft(self):
|
||||
self._l10n_sa_reset_confirmation_datetime()
|
||||
super().button_draft()
|
||||
def _l10n_sa_get_adjustment_reason(self):
|
||||
self.ensure_one()
|
||||
readable_zatca_reason = dict(self._fields['l10n_sa_reason'].selection).get(self.l10n_sa_reason)
|
||||
return readable_zatca_reason if self.l10n_sa_show_reason else self.ref
|
||||
|
||||
def _compute_show_l10n_sa_reason(self):
|
||||
for record in self:
|
||||
record.l10n_sa_show_reason = record.country_code == 'SA' and (record.move_type == 'out_refund' or (record.move_type == 'out_invoice' and record.debit_origin_id))
|
||||
|
||||
def _get_iso_format_asia_riyadh_date(self, separator=' '):
|
||||
return f'%Y-%m-%d{separator}%H:%M:%S'
|
||||
|
||||
def _get_l10n_sa_totals(self):
|
||||
self.ensure_one()
|
||||
|
|
@ -84,3 +118,42 @@ class AccountMove(models.Model):
|
|||
'total_amount': self.amount_total_signed,
|
||||
'total_tax': self.amount_tax_signed,
|
||||
}
|
||||
|
||||
def _l10n_sa_is_legal(self):
|
||||
# Check if the document is legal in Saudi
|
||||
self.ensure_one()
|
||||
return self.company_id.country_id.code == 'SA' and self.state == 'posted' and self.l10n_sa_qr_code_str
|
||||
|
||||
def _get_normalized_l10n_sa_confirmation_datetime(self, invoice_date, invoice_time=None):
|
||||
"""
|
||||
Ensures the confirmation datetime does not exceed the current time in Asia/Riyadh to prevent ZATCA rejections.
|
||||
"""
|
||||
sa_tz = timezone('Asia/Riyadh')
|
||||
now_sa = datetime.now(sa_tz)
|
||||
selected_date = fields.Date.from_string(invoice_date) if isinstance(invoice_date, str) else invoice_date
|
||||
if selected_date > now_sa.date():
|
||||
raise UserError(_("Please set the Invoice Date to be either less than or equal to today as per the Asia/Riyadh time zone, since ZATCA does not allow future-dated invoicing."))
|
||||
return min(now_sa, sa_tz.localize(datetime.combine(selected_date, invoice_time or now_sa.time()))).astimezone(utc).replace(tzinfo=None)
|
||||
|
||||
def write(self, vals):
|
||||
result = super().write(vals)
|
||||
invoice_date = vals.get('invoice_date')
|
||||
if not invoice_date:
|
||||
return result
|
||||
for move in self.filtered('l10n_sa_confirmation_datetime'):
|
||||
sa_time = move.l10n_sa_confirmation_datetime.replace(tzinfo=utc).astimezone(timezone('Asia/Riyadh')).time()
|
||||
move.l10n_sa_confirmation_datetime = self._get_normalized_l10n_sa_confirmation_datetime(invoice_date, sa_time)
|
||||
return result
|
||||
|
||||
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.commercial_partner_id.company_type == "person"
|
||||
if self.partner_id.commercial_partner_id
|
||||
else self.partner_id.company_type == "person"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
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_posted_pdf_invoices(self):
|
||||
'''
|
||||
Prevents unlinking of invoice pdfs linked to an invoice that is posted.
|
||||
'''
|
||||
restricted_moves = self._get_posted_pdf_moves_to_check().filtered(lambda move: move.country_code == 'SA' and move.state == 'posted')
|
||||
if restricted_moves:
|
||||
raise UserError(_("The Invoice PDF(s) cannot be deleted according to ZATCA rules: %s", ', '.join(restricted_moves.mapped('invoice_pdf_report_id.name'))))
|
||||
|
||||
def _get_posted_pdf_moves_to_check(self):
|
||||
'''
|
||||
Returns the moves to check whether they can be unlinked.
|
||||
'''
|
||||
return self.env['account.move'].browse(self.filtered(lambda rec: rec.res_model == 'account.move' and rec.res_field == 'invoice_pdf_report_file').mapped('res_id'))
|
||||
73
odoo-bringout-oca-ocb-l10n_sa/l10n_sa/models/template_sa.py
Normal file
73
odoo-bringout-oca-ocb-l10n_sa/l10n_sa/models/template_sa.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models
|
||||
from odoo.addons.account.models.chart_template import template
|
||||
|
||||
|
||||
class AccountChartTemplate(models.AbstractModel):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
@template('sa')
|
||||
def _get_sa_template_data(self):
|
||||
return {
|
||||
'property_account_receivable_id': 'sa_account_102011',
|
||||
'property_account_payable_id': 'sa_account_201002',
|
||||
'code_digits': '6',
|
||||
}
|
||||
|
||||
@template('sa', 'res.company')
|
||||
def _get_sa_res_company(self):
|
||||
return {
|
||||
self.env.company.id: {
|
||||
'account_fiscal_country_id': 'base.sa',
|
||||
'bank_account_code_prefix': '101',
|
||||
'cash_account_code_prefix': '105',
|
||||
'transfer_account_code_prefix': '100',
|
||||
'account_default_pos_receivable_account_id': 'sa_account_102012',
|
||||
'income_currency_exchange_account_id': 'sa_account_500011',
|
||||
'expense_currency_exchange_account_id': 'sa_account_400053',
|
||||
'account_sale_tax_id': 'sa_sales_tax_15',
|
||||
'account_purchase_tax_id': 'sa_purchase_tax_15',
|
||||
'expense_account_id': 'sa_account_400001',
|
||||
'income_account_id': 'sa_account_500001',
|
||||
'deferred_expense_account_id': 'sa_account_104020',
|
||||
'deferred_revenue_account_id': 'sa_account_201018',
|
||||
'account_cash_basis_base_account_id': 'sa_account_201030',
|
||||
'account_stock_journal_id': 'inventory_valuation',
|
||||
'account_stock_valuation_id': 'sa_account_131100',
|
||||
},
|
||||
}
|
||||
|
||||
@template('sa', 'account.journal')
|
||||
def _get_sa_account_journal(self):
|
||||
""" If Saudi Arabia chart, we add 3 new journals Tax Adjustments, IFRS 16 and Zakat"""
|
||||
return {
|
||||
"tax_adjustment": {
|
||||
'name': 'Tax Adjustments',
|
||||
'code': 'TA',
|
||||
'type': 'general',
|
||||
'show_on_dashboard': True,
|
||||
'sequence': 1,
|
||||
},
|
||||
"ifrs16": {
|
||||
'name': 'IFRS 16 Right of Use Asset',
|
||||
'code': 'IFRS',
|
||||
'type': 'general',
|
||||
'show_on_dashboard': True,
|
||||
'sequence': 10,
|
||||
},
|
||||
"zakat": {
|
||||
'name': 'Zakat',
|
||||
'code': 'ZAKAT',
|
||||
'type': 'general',
|
||||
'show_on_dashboard': True,
|
||||
'sequence': 10,
|
||||
}
|
||||
}
|
||||
|
||||
@template('sa', 'account.account')
|
||||
def _get_sa_account_account(self):
|
||||
return {
|
||||
'sa_account_131100': {
|
||||
'account_stock_variation_id': 'sa_account_400001',
|
||||
},
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue