diff --git a/addons/at_accounting/models/bank_rec_widget_line.py b/addons/at_accounting/models/bank_rec_widget_line.py
new file mode 100644
index 0000000..a88cdad
--- /dev/null
+++ b/addons/at_accounting/models/bank_rec_widget_line.py
@@ -0,0 +1,502 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import _, api, fields, models, Command
+from odoo.osv import expression
+from odoo.tools.misc import formatLang, frozendict
+
+import markupsafe
+import uuid
+
+
+class BankRecWidgetLine(models.Model):
+ _name = "bank.rec.widget.line"
+ _inherit = "analytic.mixin"
+ _description = "Line of the bank reconciliation widget"
+
+ # This model is never saved inside the database.
+ # _auto=False' & _table_query = "0" prevent the ORM to create the corresponding postgresql table.
+ _auto = False
+ _table_query = "0"
+
+ wizard_id = fields.Many2one(comodel_name='bank.rec.widget')
+ index = fields.Char(compute='_compute_index')
+ flag = fields.Selection(
+ selection=[
+ ('liquidity', 'liquidity'),
+ ('new_aml', 'new_aml'),
+ ('aml', 'aml'),
+ ('exchange_diff', 'exchange_diff'),
+ ('tax_line', 'tax_line'),
+ ('manual', 'manual'),
+ ('early_payment', 'early_payment'),
+ ('auto_balance', 'auto_balance'),
+ ],
+ )
+
+ journal_default_account_id = fields.Many2one(
+ related='wizard_id.st_line_id.journal_id.default_account_id',
+ depends=['wizard_id'],
+ )
+ account_id = fields.Many2one(
+ comodel_name='account.account',
+ compute='_compute_account_id',
+ store=True,
+ readonly=False,
+ check_company=True,
+ domain="""[
+ ('deprecated', '=', False),
+ ('id', '!=', journal_default_account_id),
+ ('account_type', 'not in', ('asset_cash', 'off_balance')),
+ ]""",
+ )
+ date = fields.Date(
+ compute='_compute_date',
+ store=True,
+ readonly=False,
+ )
+ name = fields.Char(
+ compute='_compute_name',
+ store=True,
+ readonly=False,
+ )
+ partner_id = fields.Many2one(
+ comodel_name='res.partner',
+ compute='_compute_partner_id',
+ store=True,
+ readonly=False,
+ )
+ currency_id = fields.Many2one(
+ comodel_name='res.currency',
+ compute='_compute_currency_id',
+ store=True,
+ readonly=False,
+ )
+ company_id = fields.Many2one(related='wizard_id.company_id')
+ country_code = fields.Char(related='company_id.country_id.code', depends=['company_id'])
+ company_currency_id = fields.Many2one(related='wizard_id.company_currency_id')
+ amount_currency = fields.Monetary(
+ currency_field='currency_id',
+ compute='_compute_amount_currency',
+ store=True,
+ readonly=False,
+ )
+ balance = fields.Monetary(
+ currency_field='company_currency_id',
+ compute='_compute_balance',
+ store=True,
+ readonly=False,
+ )
+ transaction_currency_id = fields.Many2one(
+ related='wizard_id.st_line_id.foreign_currency_id',
+ depends=['wizard_id'],
+ )
+ amount_transaction_currency = fields.Monetary(
+ currency_field='transaction_currency_id',
+ related='wizard_id.st_line_id.amount_currency',
+ depends=['wizard_id'],
+ )
+ debit = fields.Monetary(
+ currency_field='company_currency_id',
+ compute='_compute_from_balance',
+ )
+ credit = fields.Monetary(
+ currency_field='company_currency_id',
+ compute='_compute_from_balance',
+ )
+ force_price_included_taxes = fields.Boolean()
+ tax_base_amount_currency = fields.Monetary(
+ currency_field='currency_id',
+ )
+
+ source_aml_id = fields.Many2one(comodel_name='account.move.line')
+ source_aml_move_id = fields.Many2one(
+ comodel_name='account.move',
+ compute='_compute_source_aml_fields',
+ store=True,
+ readonly=False,
+ )
+ source_aml_move_name = fields.Char(
+ compute='_compute_source_aml_fields',
+ store=True,
+ readonly=False,
+ )
+ tax_repartition_line_id = fields.Many2one(
+ comodel_name='account.tax.repartition.line',
+ compute='_compute_tax_repartition_line_id',
+ store=True,
+ readonly=False,
+ )
+ tax_ids = fields.Many2many(
+ comodel_name='account.tax',
+ compute='_compute_tax_ids',
+ store=True,
+ readonly=False,
+ check_company=True,
+ )
+ tax_tag_ids = fields.Many2many(
+ comodel_name='account.account.tag',
+ compute='_compute_tax_tag_ids',
+ store=True,
+ readonly=False,
+ )
+ group_tax_id = fields.Many2one(
+ comodel_name='account.tax',
+ compute='_compute_group_tax_id',
+ store=True,
+ readonly=False,
+ )
+ reconcile_model_id = fields.Many2one(comodel_name='account.reconcile.model')
+ source_amount_currency = fields.Monetary(currency_field='currency_id')
+ source_balance = fields.Monetary(currency_field='company_currency_id')
+ source_debit = fields.Monetary(
+ currency_field='company_currency_id',
+ compute='_compute_from_source_balance',
+ )
+ source_credit = fields.Monetary(
+ currency_field='company_currency_id',
+ compute='_compute_from_source_balance',
+ )
+
+ display_stroked_amount_currency = fields.Boolean(compute='_compute_display_stroked_amount_currency')
+ display_stroked_balance = fields.Boolean(compute='_compute_display_stroked_balance')
+
+ partner_currency_id = fields.Many2one(
+ comodel_name='res.currency',
+ compute='_compute_partner_info',
+ )
+ partner_receivable_account_id = fields.Many2one(
+ comodel_name='account.account',
+ compute='_compute_partner_info',
+ )
+ partner_payable_account_id = fields.Many2one(
+ comodel_name='account.account',
+ compute='_compute_partner_info',
+ )
+ partner_receivable_amount = fields.Monetary(
+ currency_field='partner_currency_id',
+ compute='_compute_partner_info',
+ )
+ partner_payable_amount = fields.Monetary(
+ currency_field='partner_currency_id',
+ compute='_compute_partner_info',
+ )
+
+ bank_account = fields.Char(
+ compute='_compute_bank_account',
+ )
+ suggestion_html = fields.Html(
+ compute='_compute_suggestion',
+ sanitize=False,
+ )
+ suggestion_amount_currency = fields.Monetary(
+ currency_field='currency_id',
+ compute='_compute_suggestion',
+ )
+ suggestion_balance = fields.Monetary(
+ currency_field='company_currency_id',
+ compute='_compute_suggestion',
+ )
+ ref = fields.Char(
+ compute='_compute_ref_narration',
+ store=True,
+ readonly=False,
+ )
+ narration = fields.Html(
+ compute='_compute_ref_narration',
+ store=True,
+ readonly=False,
+ )
+
+ manually_modified = fields.Boolean()
+
+ def _compute_index(self):
+ for line in self:
+ line.index = uuid.uuid4()
+
+ @api.depends('source_aml_id')
+ def _compute_account_id(self):
+ for line in self:
+ if line.flag in ('aml', 'new_aml', 'liquidity', 'exchange_diff'):
+ line.account_id = line.source_aml_id.account_id
+ else:
+ line.account_id = line.account_id
+
+ @api.depends('source_aml_id')
+ def _compute_date(self):
+ for line in self:
+ if line.flag in ('aml', 'new_aml', 'exchange_diff'):
+ line.date = line.source_aml_id.date
+ elif line.flag in ('liquidity', 'auto_balance', 'manual', 'early_payment', 'tax_line'):
+ line.date = line.wizard_id.st_line_id.date
+ else:
+ line.date = line.date
+
+ @api.depends('source_aml_id')
+ def _compute_name(self):
+ for line in self:
+ if line.flag in ('aml', 'new_aml', 'liquidity'):
+ # In the case the source_aml_id is from a credit note, the aml might not have a name set
+ line.name = line.source_aml_id.name or line.source_aml_move_name
+ else:
+ line.name = line.name
+
+ @api.depends('source_aml_id')
+ def _compute_partner_id(self):
+ for line in self:
+ if line.flag in ('aml', 'new_aml'):
+ line.partner_id = line.source_aml_id.partner_id
+ elif line.flag in ('liquidity', 'auto_balance', 'manual', 'early_payment', 'tax_line'):
+ line.partner_id = line.wizard_id.partner_id
+ else:
+ line.partner_id = line.partner_id
+
+ @api.depends('source_aml_id')
+ def _compute_currency_id(self):
+ for line in self:
+ if line.flag in ('aml', 'new_aml', 'liquidity', 'exchange_diff'):
+ line.currency_id = line.source_aml_id.currency_id
+ elif line.flag in ('auto_balance', 'manual', 'early_payment'):
+ line.currency_id = line.wizard_id.transaction_currency_id
+ else:
+ line.currency_id = line.currency_id
+
+ @api.depends('source_aml_id')
+ def _compute_balance(self):
+ for line in self:
+ if line.flag in ('aml', 'liquidity'):
+ line.balance = line.source_aml_id.balance
+ else:
+ line.balance = line.balance
+
+ @api.depends('source_aml_id')
+ def _compute_amount_currency(self):
+ for line in self:
+ if line.flag in ('aml', 'liquidity'):
+ line.amount_currency = line.source_aml_id.amount_currency
+ else:
+ line.amount_currency = line.amount_currency
+
+ @api.depends('balance')
+ def _compute_from_balance(self):
+ for line in self:
+ line.debit = line.balance if line.balance > 0.0 else 0.0
+ line.credit = -line.balance if line.balance < 0.0 else 0.0
+
+ @api.depends('source_balance')
+ def _compute_from_source_balance(self):
+ for line in self:
+ line.source_debit = line.source_balance if line.source_balance > 0.0 else 0.0
+ line.source_credit = -line.source_balance if line.source_balance < 0.0 else 0.0
+
+ @api.depends('source_aml_id', 'account_id', 'partner_id')
+ def _compute_analytic_distribution(self):
+ cache = {}
+ for line in self:
+ if line.flag in ('liquidity', 'aml'):
+ line.analytic_distribution = line.source_aml_id.analytic_distribution
+ elif line.flag in ('tax_line', 'early_payment'):
+ line.analytic_distribution = line.analytic_distribution
+ else:
+ arguments = frozendict({
+ "partner_id": line.partner_id.id,
+ "partner_category_id": line.partner_id.category_id.ids,
+ "account_prefix": line.account_id.code,
+ "company_id": line.company_id.id,
+ })
+ if arguments not in cache:
+ cache[arguments] = self.env['account.analytic.distribution.model']._get_distribution(arguments)
+ line.analytic_distribution = cache[arguments] or line.analytic_distribution
+
+ @api.depends('source_aml_id')
+ def _compute_tax_repartition_line_id(self):
+ for line in self:
+ if line.flag == 'aml':
+ line.tax_repartition_line_id = line.source_aml_id.tax_repartition_line_id
+ else:
+ line.tax_repartition_line_id = line.tax_repartition_line_id
+
+ @api.depends('source_aml_id')
+ def _compute_tax_ids(self):
+ for line in self:
+ if line.flag == 'aml':
+ line.tax_ids = [Command.set(line.source_aml_id.tax_ids.ids)]
+ else:
+ line.tax_ids = line.tax_ids
+
+ @api.depends('source_aml_id')
+ def _compute_tax_tag_ids(self):
+ for line in self:
+ if line.flag == 'aml':
+ line.tax_tag_ids = [Command.set(line.source_aml_id.tax_tag_ids.ids)]
+ else:
+ line.tax_tag_ids = line.tax_tag_ids
+
+ @api.depends('source_aml_id')
+ def _compute_group_tax_id(self):
+ for line in self:
+ if line.flag == 'aml':
+ line.group_tax_id = line.source_aml_id.group_tax_id
+ else:
+ line.group_tax_id = line.group_tax_id
+
+ @api.depends('currency_id', 'amount_currency', 'source_amount_currency')
+ def _compute_display_stroked_amount_currency(self):
+ for line in self:
+ line.display_stroked_amount_currency = \
+ line.flag == 'new_aml' \
+ and line.currency_id.compare_amounts(line.amount_currency, line.source_amount_currency) != 0
+
+ @api.depends('currency_id', 'balance', 'source_balance')
+ def _compute_display_stroked_balance(self):
+ for line in self:
+ line.display_stroked_balance = \
+ line.flag in ('new_aml', 'exchange_diff') \
+ and line.currency_id.compare_amounts(line.balance, line.source_balance) != 0
+
+ @api.depends('flag')
+ def _compute_source_aml_fields(self):
+ for line in self:
+ line.source_aml_move_id = None
+ line.source_aml_move_name = None
+ if line.flag in ('new_aml', 'liquidity'):
+ line.source_aml_move_id = line.source_aml_id.move_id
+ line.source_aml_move_name = line.source_aml_id.move_id.name
+ elif line.flag == 'aml':
+ partials = line.source_aml_id.matched_debit_ids + line.source_aml_id.matched_credit_ids
+ all_counterpart_lines = partials.debit_move_id + partials.credit_move_id
+ counterpart_lines = all_counterpart_lines - line.source_aml_id - partials.exchange_move_id.line_ids
+ if len(counterpart_lines) == 1:
+ line.source_aml_move_id = counterpart_lines.move_id
+ line.source_aml_move_name = counterpart_lines.move_id.name
+
+ @api.depends('wizard_id.form_index', 'partner_id')
+ def _compute_partner_info(self):
+ for line in self:
+ line.partner_receivable_amount = 0.0
+ line.partner_payable_amount = 0.0
+ line.partner_currency_id = None
+ line.partner_receivable_account_id = None
+ line.partner_payable_account_id = None
+
+ if not line.partner_id or line.index != line.wizard_id.form_index:
+ continue
+
+ line.partner_currency_id = line.company_currency_id
+ partner = line.partner_id.with_company(line.wizard_id.company_id)
+ common_domain = [('parent_state', '=', 'posted'), ('partner_id', '=', partner.id)]
+ line.partner_receivable_account_id = partner.property_account_receivable_id
+ if line.partner_receivable_account_id:
+ results = self.env['account.move.line']._read_group(
+ domain=expression.AND([common_domain, [('account_id', '=', line.partner_receivable_account_id.id)]]),
+ aggregates=['amount_residual:sum'],
+ )
+ line.partner_receivable_amount = results[0][0]
+ line.partner_payable_account_id = partner.property_account_payable_id
+ if line.partner_payable_account_id:
+ results = self.env['account.move.line']._read_group(
+ domain=expression.AND([common_domain, [('account_id', '=', line.partner_payable_account_id.id)]]),
+ aggregates=['amount_residual:sum'],
+ )
+ line.partner_payable_amount = results[0][0]
+
+ @api.depends('flag')
+ def _compute_bank_account(self):
+ for line in self:
+ bank_account = line.wizard_id.st_line_id.partner_bank_id.display_name or line.wizard_id.st_line_id.account_number
+ if line.flag == 'liquidity' and bank_account:
+ line.bank_account = bank_account
+ else:
+ line.bank_account = None
+
+ @api.depends('wizard_id.form_index', 'amount_currency', 'balance')
+ def _compute_suggestion(self):
+ for line in self:
+ line.suggestion_html = None
+ line.suggestion_amount_currency = None
+ line.suggestion_balance = None
+
+ if line.flag != 'new_aml' or line.index != line.wizard_id.form_index:
+ continue
+
+ aml = line.source_aml_id
+ wizard = line.wizard_id
+ residual_amount_before_reco = abs(aml.amount_residual_currency)
+ residual_amount_after_reco = abs(aml.amount_residual_currency + line.amount_currency)
+ reconciled_amount = residual_amount_before_reco - residual_amount_after_reco
+ is_fully_reconciled = aml.currency_id.is_zero(residual_amount_after_reco)
+ is_invoice = aml.move_id.is_invoice(include_receipts=True)
+
+ if is_fully_reconciled:
+ lines = [
+ _("The invoice %(display_name_html)s with an open amount of %(open_amount)s will be entirely paid by the transaction.")
+ if is_invoice else
+ _("%(display_name_html)s with an open amount of %(open_amount)s will be fully reconciled by the transaction.")
+ ]
+ partial_amounts = wizard._lines_check_partial_amount(line)
+ if partial_amounts:
+ lines.append(
+ _("You might want to record a %(btn_start)spartial payment%(btn_end)s.")
+ if is_invoice else
+ _("You might want to make a %(btn_start)spartial reconciliation%(btn_end)s instead.")
+ )
+ line.suggestion_amount_currency = partial_amounts['amount_currency']
+ line.suggestion_balance = partial_amounts['balance']
+ else:
+ if is_invoice:
+ lines = [
+ _("The invoice %(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s."),
+ _("You might want to set the invoice as %(btn_start)sfully paid%(btn_end)s."),
+ ]
+ else:
+ lines = [
+ _("%(display_name_html)s with an open amount of %(open_amount)s will be reduced by %(amount)s."),
+ _("You might want to %(btn_start)sfully reconcile%(btn_end)s the document."),
+ ]
+ line.suggestion_amount_currency = line.source_amount_currency
+ line.suggestion_balance = line.source_balance
+
+ display_name_html = markupsafe.Markup("""
+
+ """) % {
+ 'display_name': aml.move_id.display_name,
+ }
+
+ extra_text = markupsafe.Markup('
').join(lines) % {
+ 'amount': formatLang(self.env, reconciled_amount, currency_obj=aml.currency_id),
+ 'open_amount': formatLang(self.env, residual_amount_before_reco, currency_obj=aml.currency_id),
+ 'display_name_html': display_name_html,
+ 'btn_start': markupsafe.Markup(
+ ''),
+ }
+ line.suggestion_html = markupsafe.Markup("""