from collections import defaultdict
from datetime import date

import markupsafe

from odoo import Command, models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools import frozendict, OrderedSet
from odoo.tools.misc import clean_context


class AccountPaymentRegister(models.TransientModel):
    _name = 'account.payment.register'
    _description = 'Pay'
    _check_company_auto = True

    # == Business fields ==
    payment_date = fields.Date(string="Payment Date", required=True,
        default=fields.Date.context_today)
    amount = fields.Monetary(currency_field='currency_id', store=True, readonly=False,
        compute='_compute_amount')
    hide_writeoff_section = fields.Boolean(compute="_compute_hide_writeoff_section")
    communication = fields.Char(string="Memo", store=True, readonly=False,
        compute='_compute_communication')
    group_payment = fields.Boolean(string="Group Payments", store=True, readonly=False,
        compute='_compute_group_payment',
        help="Only one payment will be created by partner (bank), instead of one per bill.")
    early_payment_discount_mode = fields.Boolean(compute='_compute_early_payment_discount_mode')
    currency_id = fields.Many2one(
        comodel_name='res.currency',
        string='Currency',
        compute='_compute_currency_id', store=True, readonly=False, precompute=True,
        help="The payment's currency.")
    journal_id = fields.Many2one(
        comodel_name='account.journal',
        compute='_compute_journal_id', store=True, readonly=False, precompute=True,
        check_company=True,
        domain="[('id', 'in', available_journal_ids)]")
    available_journal_ids = fields.Many2many(
        comodel_name='account.journal',
        compute='_compute_available_journal_ids'
    )
    available_partner_bank_ids = fields.Many2many(
        comodel_name='res.partner.bank',
        compute='_compute_available_partner_bank_ids',
    )
    partner_bank_id = fields.Many2one(
        comodel_name='res.partner.bank',
        string="Recipient Bank Account",
        readonly=False,
        store=True,
        compute='_compute_partner_bank_id',
        domain="[('id', 'in', available_partner_bank_ids)]",
    )
    company_currency_id = fields.Many2one('res.currency', string="Company Currency",
        related='company_id.currency_id')
    qr_code = fields.Html(
        string="QR Code URL",
        compute="_compute_qr_code",
    )

    batches = fields.Binary(compute='_compute_batches', export_string_translation=False)
    installments_mode = fields.Selection(
        selection=[
            ('next', "Next Installment"),
            ('overdue', "Overdue Amount"),
            ('before_date', "Before Next Payment Date"),
            ('full', "Full Amount"),
        ],
        compute='_compute_installments_mode',
        readonly=False,
        store=True,
        export_string_translation=False
    )
    installments_switch_html = fields.Html(
        compute='_compute_installments_switch_values',
    )
    installments_switch_amount = fields.Monetary(
        compute='_compute_installments_switch_values',
        currency_field='currency_id',
    )
    custom_user_amount = fields.Monetary(currency_field='currency_id')
    custom_user_currency_id = fields.Many2one(comodel_name='res.currency')

    # == Fields given through the context ==
    line_ids = fields.Many2many('account.move.line', 'account_payment_register_move_line_rel', 'wizard_id', 'line_id',
        string="Journal items", readonly=True, copy=False,)
    payment_type = fields.Selection([
        ('outbound', 'Send Money'),
        ('inbound', 'Receive Money'),
    ], string='Payment Type', store=True, copy=False,
        compute='_compute_from_lines')
    partner_type = fields.Selection([
        ('customer', 'Customer'),
        ('supplier', 'Vendor'),
    ], store=True, copy=False,
        compute='_compute_from_lines')
    source_amount = fields.Monetary(
        string="Amount to Pay (company currency)", store=True, copy=False,
        currency_field='company_currency_id',
        compute='_compute_from_lines')
    source_amount_currency = fields.Monetary(
        string="Amount to Pay (foreign currency)", store=True, copy=False,
        currency_field='source_currency_id',
        compute='_compute_from_lines')
    source_currency_id = fields.Many2one('res.currency',
        string='Source Currency', store=True, copy=False,
        compute='_compute_from_lines')
    can_edit_wizard = fields.Boolean(store=True, copy=False,
        compute='_compute_from_lines')  # used to check if user can edit info such as the amount
    can_group_payments = fields.Boolean(store=True, copy=False,
        compute='_compute_can_group_payments')  # can the user see the 'group_payments' box
    company_id = fields.Many2one('res.company', store=True, copy=False,
        compute='_compute_from_lines')
    partner_id = fields.Many2one('res.partner',
        string="Customer/Vendor", store=True, copy=False, ondelete='restrict',
        compute='_compute_from_lines')

    # == Payment methods fields ==
    payment_method_line_id = fields.Many2one('account.payment.method.line', string='Payment Method',
        readonly=False, store=True,
        compute='_compute_payment_method_line_id',
        domain="[('id', 'in', available_payment_method_line_ids)]",
        help="Manual: Pay or Get paid by any method outside of Odoo.\n"
        "Payment Providers: Each payment provider has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n"
        "Check: Pay bills by check and print it from Odoo.\n"
        "Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n"
        "SEPA Credit Transfer: Pay in the SEPA zone by submitting a SEPA Credit Transfer file to your bank. Module account_sepa is necessary.\n"
        "SEPA Direct Debit: Get paid in the SEPA zone thanks to a mandate your partner will have granted to you. Module account_sepa is necessary.\n")
    available_payment_method_line_ids = fields.Many2many('account.payment.method.line', compute='_compute_payment_method_line_fields')
    payment_method_code = fields.Char(related='payment_method_line_id.code')

    # == Payment difference fields ==
    payment_difference = fields.Monetary(
        compute='_compute_payment_difference')
    payment_difference_handling = fields.Selection(
        string="Payment Difference Handling",
        selection=[('open', 'Keep open'), ('reconcile', 'Mark as fully paid')],
        compute='_compute_payment_difference_handling',
        store=True,
        readonly=False,
    )
    writeoff_account_id = fields.Many2one(
        comodel_name='account.account',
        string="Difference Account",
        copy=False,
        check_company=True,
    )
    writeoff_label = fields.Char(string='Journal Item Label', default='Write-Off',
        help='Change label of the counterpart that will hold the payment difference')
    writeoff_is_exchange_account = fields.Boolean(
        compute='_compute_writeoff_is_exchange_account',
    )
    show_payment_difference = fields.Boolean(compute='_compute_show_payment_difference')

    # == Display purpose fields ==
    show_partner_bank_account = fields.Boolean(
        compute='_compute_show_require_partner_bank') # Used to know whether the field `partner_bank_id` should be displayed
    require_partner_bank_account = fields.Boolean(
        compute='_compute_show_require_partner_bank') # used to know whether the field `partner_bank_id` should be required
    country_code = fields.Char(related='company_id.account_fiscal_country_id.code', readonly=True)
    duplicate_payment_ids = fields.Many2many(comodel_name='account.payment', compute='_compute_duplicate_moves')
    is_register_payment_on_draft = fields.Boolean(compute='_compute_is_register_payment_on_draft')
    actionable_errors = fields.Json(compute='_compute_actionable_errors')

    # == trust check ==
    untrusted_bank_ids = fields.Many2many('res.partner.bank', compute='_compute_trust_values')
    total_payments_amount = fields.Integer(compute='_compute_trust_values')
    untrusted_payments_count = fields.Integer(compute='_compute_trust_values')
    missing_account_partners = fields.Many2many('res.partner', compute='_compute_trust_values')

    # -------------------------------------------------------------------------
    # HELPERS
    # -------------------------------------------------------------------------

    @api.model
    def _get_communication(self, lines):
        ''' Helper to compute the communication based on lines.
        :param lines:           A recordset of the `account.move.line`'s that will be reconciled.
        :return:                A string representing a communication to be set on payment.
        '''
        if len(lines.move_id) == 1:
            move = lines.move_id
            label = move.payment_reference or move.ref or move.name
        elif any(move.is_outbound() for move in lines.move_id):
            # outgoing payments references should use moves references
            labels = {move.payment_reference or move.ref or move.name for move in lines.move_id}
            return ', '.join(sorted(filter(lambda l: l, labels)))
        else:
            label = self.company_id.get_next_batch_payment_communication()
        return label

    @api.model
    def _get_batch_available_journals(self, batch_result):
        """ Helper to compute the available journals based on the batch.

        :param batch_result:    A batch computed by '_compute_batches'.
        :return:                A recordset of account.journal.
        """
        payment_type = batch_result['payment_values']['payment_type']
        company = batch_result['lines'].company_id
        journals = self.env['account.journal'].search([
            *self.env['account.journal']._check_company_domain(company),
            ('type', 'in', ('bank', 'cash', 'credit')),
        ])
        if payment_type == 'inbound':
            return journals.filtered('inbound_payment_method_line_ids')
        else:
            return journals.filtered('outbound_payment_method_line_ids')

    @api.model
    def _get_batch_journal(self, batch_result):
        """ Helper to compute the journal based on the batch.

        :param batch_result:    A batch computed by '_compute_batches'.
        :return:                An account.journal record.
        """
        payment_values = batch_result['payment_values']
        foreign_currency_id = payment_values['currency_id']
        partner_bank_id = payment_values['partner_bank_id']
        company = min(batch_result['lines'].company_id, key=lambda c: len(c.parent_ids))

        currency_domain = [('currency_id', '=', foreign_currency_id)]
        partner_bank_domain = [('bank_account_id', '=', partner_bank_id)]

        default_domain = [
            *self.env['account.journal']._check_company_domain(company),
            ('type', 'in', ('bank', 'cash', 'credit')),
            ('id', 'in', self.available_journal_ids.ids)
        ]

        if partner_bank_id:
            extra_domains = (
                currency_domain + partner_bank_domain,
                partner_bank_domain,
                currency_domain,
                [],
            )
        else:
            extra_domains = (
                currency_domain,
                [],
            )

        for extra_domain in extra_domains:
            journal = self.env['account.journal'].search(default_domain + extra_domain, limit=1)
            if journal:
                return journal

        return self.env['account.journal']

    @api.model
    def _get_batch_available_partner_banks(self, batch_result, journal):
        payment_values = batch_result['payment_values']

        # A specific bank account is set on the journal. The user must use this one.
        if payment_values['payment_type'] == 'inbound':
            # Receiving money on a bank account linked to the journal.
            return journal.bank_account_id
        else:
            company = min(batch_result['lines'].company_id, key=lambda c: len(c.sudo().parent_ids))
            # Sending money to a bank account owned by a partner.
            return batch_result['lines'].partner_id.bank_ids.filtered(lambda x: x.company_id.id in (False, company.id))._origin

    @api.model
    def _get_line_batch_key(self, line):
        ''' Turn the line passed as parameter to a dictionary defining on which way the lines
        will be grouped together.
        :return: A python dictionary.
        '''
        move = line.move_id

        partner_bank_account = self.env['res.partner.bank']
        if move.is_invoice(include_receipts=True):
            partner_bank_account = move.partner_bank_id._origin

        return {
            'partner_id': line.partner_id.id,
            'account_id': line.account_id.id,
            'currency_id': line.currency_id.id,
            'partner_bank_id': partner_bank_account.id,
            'partner_type': 'customer' if line.account_type == 'asset_receivable' else 'supplier',
        }

    @api.model
    def _get_wizard_values_from_batch(self, batch_result):
        ''' Extract values from the batch passed as parameter (see '_compute_batches')
        to be mounted in the wizard view.
        :param batch_result:    A batch computed by '_compute_batches'.
        :return:                A dictionary containing valid fields
        '''
        payment_values = batch_result['payment_values']
        lines = batch_result['lines']
        company = min(lines.company_id, key=lambda c: len(c.sudo().parent_ids)) if not self._from_sibling_companies(lines) else lines.company_id.root_id

        source_amount = abs(sum(lines.mapped('amount_residual')))
        if payment_values['currency_id'] == company.currency_id.id:
            source_amount_currency = source_amount
        else:
            source_amount_currency = abs(sum(lines.mapped('amount_residual_currency')))

        return {
            'company_id': company.id,
            'partner_id': payment_values['partner_id'],
            'partner_type': payment_values['partner_type'],
            'payment_type': payment_values['payment_type'],
            'source_currency_id': payment_values['currency_id'],
            'source_amount': source_amount,
            'source_amount_currency': source_amount_currency,
        }

    @api.model
    def _from_sibling_companies(self, lines):
        return len(lines.company_id) > 1 and not any(c.root_id in lines.company_id for c in lines.company_id)

    # -------------------------------------------------------------------------
    # COMPUTE METHODS
    # -------------------------------------------------------------------------

    @api.depends('early_payment_discount_mode', 'can_edit_wizard', 'can_group_payments', 'group_payment', 'payment_method_line_id')
    def _compute_show_payment_difference(self):
        for wizard in self:
            wizard.show_payment_difference = (
                wizard.payment_difference != 0.0
                and not wizard.early_payment_discount_mode
                and wizard.can_edit_wizard
                and (not wizard.can_group_payments or wizard.group_payment)
                and wizard.payment_method_line_id.payment_account_id
            )

    @api.depends('line_ids')
    def _compute_batches(self):
        ''' Group the account.move.line linked to the wizard together.
        Lines are grouped if they share 'partner_id','account_id','currency_id' & 'partner_type' and if
        0 or 1 partner_bank_id can be determined for the group.

        Computes a list of batches, each one containing:
            * payment_values:   A dictionary of payment values.
            * moves:        An account.move recordset.
        '''
        for wizard in self:
            lines = wizard.line_ids._origin

            if len(lines.company_id.root_id) > 1:
                raise UserError(_("You can't create payments for entries belonging to different companies."))
            if not lines:
                raise UserError(_("You can't open the register payment wizard without at least one receivable/payable line."))

            batches = defaultdict(lambda: {'lines': self.env['account.move.line']})
            banks_per_partner = defaultdict(lambda: {'inbound': OrderedSet(), 'outbound': OrderedSet()})
            for line in lines:
                batch_key = self._get_line_batch_key(line)
                vals = batches[frozendict(batch_key)]
                vals['payment_values'] = batch_key
                vals['lines'] += line
                banks_per_partner[batch_key['partner_id']]['inbound' if line.balance > 0.0 else 'outbound'].add(
                    batch_key['partner_bank_id']
                )

            partner_unique_inbound = {p for p, b in banks_per_partner.items() if len(b['inbound']) == 1}
            partner_unique_outbound = {p for p, b in banks_per_partner.items() if len(b['outbound']) == 1}

            # Compute 'payment_type'.
            batch_vals = []
            seen_keys = set()
            for i, key in enumerate(list(batches)):
                if key in seen_keys:
                    continue
                vals = batches[key]
                lines = vals['lines']
                merge = (
                    key['partner_id'] in partner_unique_inbound
                    and key['partner_id'] in partner_unique_outbound
                )
                if merge:
                    for other_key in list(batches)[i + 1:]:
                        if other_key in seen_keys:
                            continue
                        other_vals = batches[other_key]
                        if all(
                            other_vals['payment_values'][k] == v
                            for k, v in vals['payment_values'].items()
                            if k not in ('partner_bank_id', 'payment_type')
                        ):
                            # add the lines in this batch and mark as seen
                            lines += other_vals['lines']
                            seen_keys.add(other_key)
                balance = sum(lines.mapped('balance'))
                vals['payment_values']['payment_type'] = 'inbound' if balance > 0.0 else 'outbound'
                if merge:
                    partner_banks = banks_per_partner[key['partner_id']]
                    vals['payment_values']['partner_bank_id'] = next(iter(partner_banks[vals['payment_values']['payment_type']]))
                    vals['lines'] = lines
                batch_vals.append(vals)

            wizard.batches = batch_vals

    @api.depends('payment_method_line_id', 'line_ids', 'group_payment', 'partner_bank_id')
    def _compute_trust_values(self):
        for wizard in self:
            untrusted_payments_count = 0
            untrusted_accounts = self.env['res.partner.bank']
            missing_account_partners = self.env['res.partner']

            total_payment_count = len(wizard.batches)
            if not wizard.group_payment:
                total_amount_values = wizard._get_total_amounts_to_pay(wizard.batches)
                total_payment_count = len(total_amount_values['lines'])

            # Validate batches; if require_partner_bank_account and the account isn't setup and trusted, we do not allow the payment
            for batch in wizard.batches:
                # Use the currently selected partner_bank_id if in edit mode, otherwise use batch account
                batch_account = wizard.partner_bank_id or wizard._get_batch_account(batch)
                if wizard.require_partner_bank_account:
                    if not batch_account:
                        missing_account_partners += batch['lines'].partner_id
                    elif not batch_account.allow_out_payment:
                        untrusted_payments_count += 1 if wizard.group_payment else len(batch['lines'].filtered(lambda line: line in total_amount_values['lines']))
                        untrusted_accounts |= batch_account

            wizard.update({
                'total_payments_amount': total_payment_count,
                'untrusted_payments_count': untrusted_payments_count,
                'untrusted_bank_ids': untrusted_accounts or False,
                'missing_account_partners': missing_account_partners or False,
            })

    @api.depends('line_ids')
    def _compute_from_lines(self):
        ''' Load initial values from the account.moves passed through the context. '''
        for wizard in self:
            batch_result = wizard.batches[0]
            wizard_values_from_batch = wizard._get_wizard_values_from_batch(batch_result)

            if len(wizard.batches) == 1:
                # == Single batch to be mounted on the view ==
                wizard.update(wizard_values_from_batch)

                wizard.can_edit_wizard = True
            else:
                # == Multiple batches: The wizard is not editable  ==
                lines = sum((batch_result['lines'] for batch_result in wizard.batches), self.env['account.move.line'])
                company = min(lines.company_id, key=lambda c: len(c.parent_ids)) if not self._from_sibling_companies(lines) else lines.company_id.root_id
                wizard.update({
                    'company_id': company.id,
                    'partner_id': False,
                    'partner_type': False,
                    'payment_type': wizard_values_from_batch['payment_type'],
                    'source_currency_id': False,
                    'source_amount': False,
                    'source_amount_currency': False,
                })

                wizard.can_edit_wizard = False

    @api.depends('batches', 'amount')
    def _compute_can_group_payments(self):
        for wizard in self:
            if len(wizard.batches) == 1:
                lines = wizard.batches[0]['lines']
                wizard.can_group_payments = (
                    len(lines) != 1
                    and not (len(lines.move_id) == 1 and lines.move_id.is_invoice(include_receipts=True))
                )
            else:
                total_amounts_to_pay = wizard._get_total_amounts_to_pay(wizard.batches)
                wizard.can_group_payments = any(
                    len(batch_result['lines'].filtered(lambda line: line in total_amounts_to_pay['lines'])) != 1 for batch_result in wizard.batches
                )

    @api.depends('can_edit_wizard', 'amount')
    def _compute_communication(self):
        # The communication can't be computed in '_compute_from_lines' because
        # it's a compute editable field and then, should be computed in a separated method.
        for wizard in self:
            if wizard.can_edit_wizard and wizard.installments_mode == 'full' or wizard.custom_user_amount:
                lines = wizard.line_ids
            else:
                lines = wizard._get_total_amounts_to_pay(wizard.batches)['lines']
            wizard.communication = wizard._get_communication(lines)

    @api.depends('can_edit_wizard')
    def _compute_group_payment(self):
        for wizard in self:
            if wizard.can_edit_wizard:
                wizard.group_payment = len(wizard.batches[0]['lines'].move_id) == 1
            else:
                wizard.group_payment = False

    @api.depends('journal_id')
    def _compute_currency_id(self):
        for wizard in self:
            wizard.currency_id = wizard.journal_id.currency_id or wizard.source_currency_id or wizard.company_id.currency_id

    @api.depends('payment_type', 'company_id', 'can_edit_wizard')
    def _compute_available_journal_ids(self):
        for wizard in self:
            available_journals = self.env['account.journal']
            for batch in wizard.batches:
                available_journals |= wizard._get_batch_available_journals(batch)
            wizard.available_journal_ids = [Command.set(available_journals.ids)]

    @api.depends('available_journal_ids')
    def _compute_journal_id(self):
        for wizard in self:
            if wizard.journal_id in wizard.available_journal_ids:
                continue
            move_payment_method_lines = wizard.line_ids.move_id.preferred_payment_method_line_id
            if move_payment_method_lines and len(move_payment_method_lines) == 1:
                wizard.journal_id = move_payment_method_lines.journal_id
            elif wizard.can_edit_wizard:
                batch = wizard.batches[0]
                wizard.journal_id = wizard._get_batch_journal(batch)
            else:
                wizard.journal_id = self.env['account.journal'].search([
                    *self.env['account.journal']._check_company_domain(wizard.company_id),
                    ('type', 'in', ('bank', 'cash', 'credit')),
                    ('id', 'in', self.available_journal_ids.ids)
                ], limit=1)

    @api.depends('can_edit_wizard', 'journal_id')
    def _compute_available_partner_bank_ids(self):
        for wizard in self:
            if wizard.can_edit_wizard:
                batch = wizard.batches[0]
                wizard.available_partner_bank_ids = wizard._get_batch_available_partner_banks(batch, wizard.journal_id)
            else:
                wizard.available_partner_bank_ids = None

    @api.depends('journal_id', 'available_partner_bank_ids')
    def _compute_partner_bank_id(self):
        for wizard in self:
            if wizard.can_edit_wizard:
                batch = wizard.batches[0]
                partner_bank_id = batch['payment_values']['partner_bank_id']
                available_partner_banks = wizard.available_partner_bank_ids._origin
                if partner_bank_id and partner_bank_id in available_partner_banks.ids:
                    wizard.partner_bank_id = self.env['res.partner.bank'].browse(partner_bank_id)
                else:
                    wizard.partner_bank_id = available_partner_banks[:1]
            else:
                wizard.partner_bank_id = None

    @api.depends('payment_type', 'journal_id', 'currency_id')
    def _compute_payment_method_line_fields(self):
        for wizard in self:
            if wizard.journal_id:
                wizard.available_payment_method_line_ids = wizard.journal_id._get_available_payment_method_lines(wizard.payment_type)
            else:
                wizard.available_payment_method_line_ids = False

    @api.depends('payment_type', 'journal_id')
    def _compute_payment_method_line_id(self):
        for wizard in self:
            if wizard.journal_id:
                available_payment_method_lines = wizard.journal_id._get_available_payment_method_lines(wizard.payment_type)
            else:
                available_payment_method_lines = False

            if available_payment_method_lines and wizard.payment_method_line_id in available_payment_method_lines:
                continue

            # Select the first available one by default.
            if available_payment_method_lines:
                move_payment_method_lines = wizard.line_ids.move_id.preferred_payment_method_line_id
                if len(move_payment_method_lines) == 1 and move_payment_method_lines.id in available_payment_method_lines.ids:
                    wizard.payment_method_line_id = move_payment_method_lines
                else:
                    wizard.payment_method_line_id = available_payment_method_lines[0]._origin
            else:
                wizard.payment_method_line_id = False

    @api.depends('payment_method_line_id')
    def _compute_show_require_partner_bank(self):
        """ Computes if the destination bank account must be displayed in the payment form view. By default, it
        won't be displayed but some modules might change that, depending on the payment type."""
        for wizard in self:
            if wizard.journal_id.type == 'cash':
                wizard.show_partner_bank_account = False
            else:
                wizard.show_partner_bank_account = wizard.payment_method_line_id.code in self.env['account.payment']._get_method_codes_using_bank_account()
            wizard.require_partner_bank_account = wizard.payment_method_line_id.code in self.env['account.payment']._get_method_codes_needing_bank_account()

    @api.depends('line_ids')
    def _compute_actionable_errors(self):
        for wizard in self:
            actionable_errors = {}
            if unpaid_matched_payments := wizard.line_ids.move_id.reconciled_payment_ids.filtered(lambda p: p.state == 'in_process'):
                actionable_errors['unpaid_matched_payments'] = {
                    'message': self.env._("There are payments in progress. Make sure you don't pay twice."),
                    'action_text': self.env._("Check them"),
                    'action': unpaid_matched_payments._get_records_action(name=self.env._("Payments")),
                    'level': 'danger',
                }
            wizard.actionable_errors = actionable_errors

    def _convert_to_wizard_currency(self, installments):
        self.ensure_one()
        total_per_currency = defaultdict(lambda: {
            'amount_residual': 0.0,
            'amount_residual_currency': 0.0,
        })
        for installment in installments:
            line = installment['line']
            total_per_currency[line.currency_id]['amount_residual'] += installment['amount_residual']
            total_per_currency[line.currency_id]['amount_residual_currency'] += installment['amount_residual_currency']

        total_amount = 0.0
        wizard_curr = self.currency_id
        comp_curr = self.company_currency_id
        for currency, amounts in total_per_currency.items():
            amount_residual = amounts['amount_residual']
            amount_residual_currency = amounts['amount_residual_currency']
            if currency == wizard_curr:
                # Same currency
                total_amount += amount_residual_currency
            elif currency != comp_curr and wizard_curr == comp_curr:
                # Foreign currency on source line but the company currency one on the opposite line.
                total_amount += currency._convert(amount_residual_currency, comp_curr, self.company_id, self.payment_date)
            elif currency == comp_curr and wizard_curr != comp_curr:
                # Company currency on source line but a foreign currency one on the opposite line.
                total_amount += comp_curr._convert(amount_residual, wizard_curr, self.company_id, self.payment_date)
            else:
                # Foreign currency on payment different than the one set on the journal entries.
                total_amount += comp_curr._convert(amount_residual, wizard_curr, self.company_id, self.payment_date)
        return total_amount

    def _get_total_amounts_to_pay(self, batch_results):
        self.ensure_one()
        next_payment_date = self._get_next_payment_date_in_context()
        amount_per_line_common = []
        amount_per_line_by_default = []
        amount_per_line_full_amount = []
        amount_per_line_for_difference = []
        epd_applied = False
        first_installment_mode = False
        all_lines = self.env['account.move.line']
        for batch_result in batch_results:
            all_lines |= batch_result['lines']
        all_lines = all_lines.sorted(key=lambda line: (line.move_id, line.date_maturity or date.max))
        for lines in all_lines.grouped('move_id').values():
            installments = lines._get_installments_data(payment_currency=self.currency_id, payment_date=self.payment_date, next_payment_date=next_payment_date)
            last_installment_mode = False
            for installment in installments:
                line = installment['line']
                if installment['type'] == 'early_payment_discount':
                    epd_applied = True
                    amount_per_line_by_default.append(installment)
                    amount_per_line_for_difference.append({
                        **installment,
                        'amount_residual_currency': line.amount_residual_currency,
                        'amount_residual': line.amount_residual,
                    })
                    continue

                # Installments.
                # In case of overdue, all of them are sum as a default amount to be paid.
                # The next installment is added for the difference.
                if (
                    line.display_type == 'payment_term'
                    and installment['type'] in ('overdue', 'next', 'before_date')
                ):
                    if installment['type'] == 'overdue':
                        amount_per_line_common.append(installment)
                    elif installment['type'] == 'before_date':
                        amount_per_line_common.append(installment)
                        first_installment_mode = 'before_date'
                    elif installment['type'] == 'next':
                        if last_installment_mode in ('next', 'overdue', 'before_date'):
                            amount_per_line_full_amount.append(installment)
                        elif not last_installment_mode:
                            amount_per_line_common.append(installment)
                            # if we have several moves and one of them has as first installment, a 'next', we want
                            # the whole batches to have a mode of 'next', overriding an 'overdue' on another move
                            first_installment_mode = 'next'
                    last_installment_mode = installment['type']
                    first_installment_mode = first_installment_mode or last_installment_mode
                    continue

                amount_per_line_common.append(installment)

        common = self._convert_to_wizard_currency(amount_per_line_common)
        by_default = self._convert_to_wizard_currency(amount_per_line_by_default)
        for_difference = self._convert_to_wizard_currency(amount_per_line_for_difference)
        full_amount = self._convert_to_wizard_currency(amount_per_line_full_amount)

        lines = self.env['account.move.line']
        for value in amount_per_line_common + amount_per_line_by_default:
            lines |= value['line']

        return {
            # default amount shown in the wizard (different from full for installments)
            'amount_by_default': abs(common + by_default),
            'full_amount': abs(common + by_default + full_amount),
            # for_difference is used to compute the difference for the Early Payment Discount
            'amount_for_difference': abs(common + for_difference),
            'full_amount_for_difference': abs(common + for_difference + full_amount),
            'epd_applied': epd_applied,
            'installment_mode': first_installment_mode,
            'lines': lines,
        }

    @api.onchange('amount')
    def _onchange_amount(self):
        if not self.can_edit_wizard or not self.currency_id:
            return

        total_amount_values = self._get_total_amounts_to_pay(self.batches)
        is_custom_user_amount = all(
            not self.currency_id.is_zero(self.amount - total_amount_values[amount_field])
            for amount_field in ('amount_by_default', 'amount_for_difference', 'full_amount', 'full_amount_for_difference')
        )
        if is_custom_user_amount:
            self.custom_user_amount = self.amount
            self.custom_user_currency_id = self.currency_id
        else:
            self.custom_user_amount = None
            self.custom_user_currency_id = None

    @api.onchange('currency_id')
    def _onchange_currency_id(self):
        if not self.can_edit_wizard or not self.currency_id or not self.payment_date or not self.custom_user_amount:
            return

        if self.custom_user_amount:
            self.custom_user_amount = self.amount = self.custom_user_currency_id._convert(
                from_amount=self.custom_user_amount,
                to_currency=self.currency_id,
                date=self.payment_date,
                company=self.company_id,
            )

    @api.onchange('payment_date')
    def _onchange_payment_date(self):
        if not self.can_edit_wizard or not self.currency_id or not self.payment_date or not self.custom_user_amount:
            return

        self.amount = self.custom_user_amount

    @api.depends('can_edit_wizard', 'source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date', 'installments_mode')
    def _compute_amount(self):
        for wizard in self:
            if not wizard.journal_id or not wizard.currency_id or not wizard.payment_date or wizard.custom_user_amount:
                wizard.amount = wizard.amount
            else:
                total_amount_values = wizard._get_total_amounts_to_pay(wizard.batches)
                wizard.amount = total_amount_values['amount_by_default']

    @api.depends('amount')
    def _compute_installments_mode(self):
        for wizard in self:
            if not wizard.journal_id or not wizard.currency_id:
                wizard.installments_mode = wizard.installments_mode
            else:
                total_amount_values = wizard._get_total_amounts_to_pay(wizard.batches)
                if wizard.currency_id.compare_amounts(wizard.amount, total_amount_values['full_amount']) == 0:
                    wizard.installments_mode = 'full'
                elif wizard.currency_id.compare_amounts(wizard.amount, total_amount_values['amount_by_default']) == 0:
                    wizard.installments_mode = total_amount_values['installment_mode']
                else:
                    wizard.installments_mode = 'full'

    @api.depends('installments_mode')
    def _compute_installments_switch_values(self):
        for wizard in self:
            if not wizard.journal_id or not wizard.currency_id:
                wizard.installments_switch_amount = wizard.installments_switch_amount
                wizard.installments_switch_html = wizard.installments_switch_html
            else:
                total_amount_values = wizard._get_total_amounts_to_pay(wizard.batches)
                html_lines = []
                if wizard.installments_mode == 'full':
                    is_full_match = (
                        wizard.currency_id.is_zero(total_amount_values['full_amount'] - wizard.amount)
                        and wizard.currency_id.is_zero(total_amount_values['full_amount'] - total_amount_values['amount_by_default'])
                    )
                    wizard.installments_switch_amount = 0.0 if is_full_match else total_amount_values['amount_by_default']
                    if not is_full_match and not wizard.currency_id.is_zero(wizard.amount):
                        switch_message = (
                            _("Consider paying the amount with %(btn_start)searly payment discount%(btn_end)s instead.")
                            if total_amount_values['epd_applied']
                            else _("Consider paying in %(btn_start)sinstallments%(btn_end)s instead.")
                        )
                        html_lines += [
                            _("This is the full amount."),
                            switch_message,
                        ]
                elif wizard.installments_mode == 'overdue':
                    wizard.installments_switch_amount = total_amount_values['full_amount']
                    html_lines += [
                        _("This is the overdue amount."),
                        _("Consider paying the %(btn_start)sfull amount%(btn_end)s."),
                    ]
                elif wizard.installments_mode == 'before_date':
                    wizard.installments_switch_amount = total_amount_values['full_amount']
                    next_payment_date = self._get_next_payment_date_in_context()
                    html_lines += [
                        _("Total for the installments before %(date)s.", date=(next_payment_date or fields.Date.context_today(self))),
                        _("Consider paying the %(btn_start)sfull amount%(btn_end)s."),
                    ]
                elif wizard.installments_mode == 'next':
                    wizard.installments_switch_amount = total_amount_values['full_amount']
                    html_lines += [
                        _("This is the next unreconciled installment."),
                        _("Consider paying the %(btn_start)sfull amount%(btn_end)s."),
                    ]
                else:
                    wizard.installments_switch_amount = wizard.installments_switch_amount

                if wizard.custom_user_amount:
                    wizard.installments_switch_html = None
                else:
                    wizard.installments_switch_html = markupsafe.Markup('<br/>').join(html_lines) % {
                        'btn_start': markupsafe.Markup('<span class="installments_switch_button btn btn-link p-0 align-baseline">'),
                        'btn_end': markupsafe.Markup('</span>'),
                    }

    @api.depends('can_edit_wizard', 'payment_date', 'currency_id', 'amount')
    def _compute_early_payment_discount_mode(self):
        for wizard in self:
            if not wizard.journal_id or not wizard.currency_id or not wizard.payment_date:
                wizard.early_payment_discount_mode = wizard.early_payment_discount_mode
            else:
                total_amount_values = wizard._get_total_amounts_to_pay(wizard.batches)
                wizard.early_payment_discount_mode = (
                    total_amount_values['epd_applied']
                    and (
                        wizard.currency_id.compare_amounts(wizard.amount, total_amount_values['amount_by_default']) == 0
                        or wizard.currency_id.compare_amounts(wizard.amount, total_amount_values['full_amount']) == 0
                    )
                )

    @api.depends('can_edit_wizard', 'amount', 'installments_mode')
    def _compute_payment_difference(self):
        for wizard in self:
            if wizard.payment_date:
                total_amount_values = wizard._get_total_amounts_to_pay(wizard.batches)
                if wizard.installments_mode in ('overdue', 'next', 'before_date'):
                    wizard.payment_difference = total_amount_values['amount_for_difference'] - wizard.amount
                elif wizard.installments_mode == 'full':
                    wizard.payment_difference = total_amount_values['full_amount_for_difference'] - wizard.amount
                else:
                    wizard.payment_difference = total_amount_values['amount_for_difference'] - wizard.amount
            else:
                wizard.payment_difference = 0.0

    @api.depends('can_edit_wizard', 'writeoff_account_id', 'payment_difference_handling', 'currency_id')
    def _compute_writeoff_is_exchange_account(self):
        for wizard in self:
            wizard.writeoff_is_exchange_account = all((
                wizard.can_edit_wizard,
                wizard.payment_difference_handling == 'reconcile',
                wizard.currency_id != wizard.source_currency_id,
                wizard.writeoff_account_id,
                wizard.writeoff_account_id in (
                    wizard.company_id.expense_currency_exchange_account_id,
                    wizard.company_id.income_currency_exchange_account_id,
                ),
            ))

    @api.depends('early_payment_discount_mode')
    def _compute_payment_difference_handling(self):
        for wizard in self:
            if wizard.can_edit_wizard:
                wizard.payment_difference_handling = 'reconcile' if wizard.early_payment_discount_mode else 'open'
            else:
                wizard.payment_difference_handling = False

    @api.depends('early_payment_discount_mode')
    def _compute_hide_writeoff_section(self):
        for wizard in self:
            wizard.hide_writeoff_section = wizard.early_payment_discount_mode

    @api.depends('partner_bank_id', 'amount', 'currency_id', 'payment_method_line_id', 'payment_type', 'communication')
    def _compute_qr_code(self):
        for pay in self:
            qr_html = False
            if pay.partner_bank_id \
               and pay.partner_bank_id.allow_out_payment \
               and pay.payment_method_line_id.code == 'manual' \
               and pay.payment_type == 'outbound' \
               and pay.amount \
               and pay.currency_id:
                b64_qr = pay.partner_bank_id.build_qr_code_base64(
                    amount=pay.amount,
                    free_communication=pay.communication,
                    structured_communication=pay.communication,
                    currency=pay.currency_id,
                    debtor_partner=pay.partner_id,
                )
                if b64_qr:
                    qr_html = f'''
                        <img class="border border-dark rounded" src="{b64_qr}"/>
                        <br/>
                        <strong>{_('Scan me with your banking app.')}</strong>
                    '''
            pay.qr_code = qr_html

    @api.depends('partner_id', 'amount', 'payment_date', 'payment_type', 'line_ids')
    def _compute_duplicate_moves(self):
        for wizard in self:
            if wizard.can_edit_wizard:
                wizard.duplicate_payment_ids = self._fetch_duplicate_reference().get(0, self.env['account.payment'])
            else:
                wizard.duplicate_payment_ids = self.env['account.payment']

    @api.depends('line_ids')
    def _compute_is_register_payment_on_draft(self):
        for wizard in self:
            wizard.is_register_payment_on_draft = any(l.parent_state == 'draft' for l in wizard.line_ids)

    def _fetch_duplicate_reference(self, matching_states=('draft', 'posted')):
        """ Retrieve move ids for possible duplicates of payments. Duplicates moves:
        - Have the same partner_id, amount and date as the payment
        - Are not reconciled
        - Represent a credit in the same account receivable or a debit in the same account payable as the payment, or
        - Represent a credit in outstanding receipts or debit in outstanding payments, so bank statement lines with an
         outstanding counterpart can be matched, or
        - Are in the suspense account
        """
        dummy = self.env['account.payment'].new({
            "company_id": self.company_id,
            "partner_id": self.partner_id,
            "date": self.payment_date,
            "amount": self.amount,
            "payment_type": self.payment_type,
        })
        return dummy._fetch_duplicate_reference(matching_states)
    # -------------------------------------------------------------------------
    # LOW-LEVEL METHODS
    # -------------------------------------------------------------------------

    @api.model
    def default_get(self, fields):
        # OVERRIDE
        res = super().default_get(fields)

        if 'line_ids' in fields and 'line_ids' not in res:

            # Retrieve moves to pay from the context.

            if self.env.context.get('active_model') == 'account.move':
                lines = self.env['account.move'].browse(self.env.context.get('active_ids', [])).line_ids
            elif self.env.context.get('active_model') == 'account.move.line':
                lines = self.env['account.move.line'].browse(self.env.context.get('active_ids', []))
            else:
                raise UserError(_(
                    "The register payment wizard should only be called on account.move or account.move.line records."
                ))

            if 'journal_id' in res and not self.env['account.journal'].browse(res['journal_id']).filtered_domain([
                *self.env['account.journal']._check_company_domain(lines.company_id),
                ('type', 'in', ('bank', 'cash', 'credit')),
            ]):
                # default can be inherited from the list view, should be computed instead
                del res['journal_id']

            # Keep lines having a residual amount to pay.
            available_lines = self.env['account.move.line']
            valid_account_types = self.env['account.payment']._get_valid_payment_account_types()
            for line in lines:

                if line.account_type not in valid_account_types:
                    continue
                if line.currency_id:
                    if line.currency_id.is_zero(line.amount_residual_currency):
                        continue
                else:
                    if line.company_currency_id.is_zero(line.amount_residual):
                        continue
                available_lines |= line

            # Check.
            if not available_lines:
                raise UserError(_("There's nothing left to pay for the selected journal items, so no payment registration is necessary. You've got your finances under control like a boss!"))
            if len(lines.company_id.root_id) > 1:
                raise UserError(_("You can't create payments for entries belonging to different companies."))
            if self._from_sibling_companies(lines) and lines.company_id.root_id not in self.env.user.company_ids:
                raise UserError(_("You can't create payments for entries belonging to different branches without access to parent company."))
            if len(set(available_lines.mapped('account_type'))) > 1:
                raise UserError(_("You can't register payments for both inbound and outbound moves at the same time."))

            res['line_ids'] = [(6, 0, available_lines.ids)]

        return res

    # -------------------------------------------------------------------------
    # BUSINESS METHODS
    # -------------------------------------------------------------------------

    def _create_payment_vals_from_wizard(self, batch_result):
        payment_vals = {
            'date': self.payment_date,
            'amount': self.amount,
            'payment_type': self.payment_type,
            'partner_type': self.partner_type,
            'memo': self.communication,
            'journal_id': self.journal_id.id,
            'company_id': self.company_id.id,
            'currency_id': self.currency_id.id,
            'partner_id': self.partner_id.id,
            'partner_bank_id': self.partner_bank_id.id,
            'payment_method_line_id': self.payment_method_line_id.id,
            'destination_account_id': self.line_ids[0].account_id.id,
            'write_off_line_vals': [],
        }

        if self.payment_difference_handling == 'reconcile':
            if self.early_payment_discount_mode:
                epd_aml_values_list = []
                for aml in batch_result['lines']:
                    if aml.move_id._is_eligible_for_early_payment_discount(self.currency_id, self.payment_date):
                        epd_aml_values_list.append({
                            'aml': aml,
                            'amount_currency': -aml.amount_residual_currency,
                            'balance': aml.currency_id._convert(-aml.amount_residual_currency, aml.company_currency_id, date=self.payment_date),
                        })

                open_amount_currency = self.payment_difference * (-1 if self.payment_type == 'outbound' else 1)
                open_balance = self.currency_id._convert(open_amount_currency, self.company_id.currency_id, self.company_id, self.payment_date)
                early_payment_values = self.env['account.move']._get_invoice_counterpart_amls_for_early_payment_discount(epd_aml_values_list, open_balance)
                for aml_values_list in early_payment_values.values():
                    payment_vals['write_off_line_vals'] += aml_values_list

            elif not self.currency_id.is_zero(self.payment_difference):

                if self.writeoff_is_exchange_account:
                    # Force the rate when computing the 'balance' only when the payment has a foreign currency.
                    # If not, the rate is forced during the reconciliation to put the difference directly on the
                    # exchange difference.
                    if self.currency_id != self.company_currency_id:
                        payment_vals['force_balance'] = sum(batch_result['lines'].mapped('amount_residual'))
                else:
                    if self.payment_type == 'inbound':
                        # Receive money.
                        write_off_amount_currency = self.payment_difference
                    else:  # if self.payment_type == 'outbound':
                        # Send money.
                        write_off_amount_currency = -self.payment_difference

                    payment_vals['write_off_line_vals'].append({
                        'name': self.writeoff_label,
                        'account_id': self.writeoff_account_id.id,
                        'partner_id': self.partner_id.id,
                        'currency_id': self.currency_id.id,
                        'amount_currency': write_off_amount_currency,
                        'balance': self.currency_id._convert(write_off_amount_currency, self.company_id.currency_id, self.company_id, self.payment_date),
                    })

        return payment_vals

    def _create_payment_vals_from_batch(self, batch_result):
        batch_values = self._get_wizard_values_from_batch(batch_result)

        if batch_values['payment_type'] == 'inbound':
            partner_bank_id = self.journal_id.bank_account_id.id
        else:
            partner_bank_id = batch_result['payment_values']['partner_bank_id']

        payment_method_line = self.payment_method_line_id

        if batch_values['payment_type'] != payment_method_line.payment_type:
            payment_method_line = self.journal_id._get_available_payment_method_lines(batch_values['payment_type'])[:1]

        payment_vals = {
            'date': self.payment_date,
            'amount': batch_values['source_amount_currency'],
            'payment_type': batch_values['payment_type'],
            'partner_type': batch_values['partner_type'],
            'memo': self._get_communication(batch_result['lines']),
            'journal_id': self.journal_id.id,
            'company_id': self.company_id.id,
            'currency_id': batch_values['source_currency_id'],
            'partner_id': batch_values['partner_id'],
            'payment_method_line_id': payment_method_line.id,
            'destination_account_id': batch_result['lines'][0].account_id.id,
            'write_off_line_vals': [],
        }

        # In case it is false, we don't add it to the create vals so that
        # _compute_partner_bank_id is executed at payment creation
        if partner_bank_id:
            payment_vals['partner_bank_id'] = partner_bank_id

        total_amount_values = self._get_total_amounts_to_pay([batch_result])
        total_amount = total_amount_values['amount_by_default']
        currency = self.env['res.currency'].browse(batch_values['source_currency_id'])
        if total_amount_values['epd_applied']:
            payment_vals['amount'] = total_amount

            epd_aml_values_list = []
            for aml in batch_result['lines']:
                if aml.move_id._is_eligible_for_early_payment_discount(currency, self.payment_date):
                    epd_aml_values_list.append({
                        'aml': aml,
                        'amount_currency': -aml.amount_residual_currency,
                        'balance': currency._convert(-aml.amount_residual_currency, aml.company_currency_id, self.company_id, self.payment_date),
                    })

            open_amount_currency = (batch_values['source_amount_currency'] - total_amount) * (-1 if batch_values['payment_type'] == 'outbound' else 1)
            open_balance = currency._convert(open_amount_currency, aml.company_currency_id, self.company_id, self.payment_date)
            early_payment_values = self.env['account.move']\
                ._get_invoice_counterpart_amls_for_early_payment_discount(epd_aml_values_list, open_balance)
            for aml_values_list in early_payment_values.values():
                payment_vals['write_off_line_vals'] += aml_values_list

        return payment_vals

    def _init_payments(self, to_process, edit_mode=False):
        """ Create the payments.

        :param to_process:  A list of python dictionary, one for each payment to create, containing:
                            * create_vals:  The values used for the 'create' method.
                            * to_reconcile: The journal items to perform the reconciliation.
                            * batch:        A python dict containing everything you want about the source journal items
                                            to which a payment will be created (see '_compute_batches').
        :param edit_mode:   Is the wizard in edition mode.
        """

        payments = self.env['account.payment']\
            .with_context(skip_invoice_sync=True)\
            .create([x['create_vals'] for x in to_process])

        for payment, vals in zip(payments, to_process):
            vals['payment'] = payment

            # If payments are made using a currency different than the source one, ensure the balance match exactly in
            # order to fully paid the source journal items.
            # For example, suppose a new currency B having a rate 100:1 regarding the company currency A.
            # If you try to pay 12.15A using 0.12B, the computed balance will be 12.00A for the payment instead of 12.15A.
            if edit_mode and payment.move_id:
                lines = vals['to_reconcile']

                # Batches are made using the same currency so making 'lines.currency_id' is ok.
                if payment.currency_id != lines.currency_id:
                    liquidity_lines, counterpart_lines, writeoff_lines = payment._seek_for_lines()
                    source_balance = abs(sum(lines.mapped('amount_residual')))
                    if liquidity_lines[0].balance:
                        payment_rate = liquidity_lines[0].amount_currency / liquidity_lines[0].balance
                    else:
                        payment_rate = 0.0
                    source_balance_converted = abs(source_balance) * payment_rate

                    # Translate the balance into the payment currency is order to be able to compare them.
                    # In case in both have the same value (12.15 * 0.01 ~= 0.12 in our example), it means the user
                    # attempt to fully paid the source lines and then, we need to manually fix them to get a perfect
                    # match.
                    payment_balance = abs(sum(counterpart_lines.mapped('balance')))
                    payment_amount_currency = abs(sum(counterpart_lines.mapped('amount_currency')))
                    if not payment.currency_id.is_zero(source_balance_converted - payment_amount_currency):
                        continue

                    delta_balance = source_balance - payment_balance

                    # Balance are already the same.
                    if self.company_currency_id.is_zero(delta_balance):
                        continue

                    # Fix the balance but make sure to peek the liquidity and counterpart lines first.
                    debit_lines = (liquidity_lines + counterpart_lines).filtered('debit')
                    credit_lines = (liquidity_lines + counterpart_lines).filtered('credit')

                    if debit_lines and credit_lines:
                        payment.move_id.write({'line_ids': [
                            (1, debit_lines[0].id, {'debit': debit_lines[0].debit + delta_balance}),
                            (1, credit_lines[0].id, {'credit': credit_lines[0].credit + delta_balance}),
                        ]})
        return payments

    def _post_payments(self, to_process, edit_mode=False):
        """ Post the newly created payments.

        :param to_process:  A list of python dictionary, one for each payment to create, containing:
                            * create_vals:  The values used for the 'create' method.
                            * to_reconcile: The journal items to perform the reconciliation.
                            * batch:        A python dict containing everything you want about the source journal items
                                            to which a payment will be created (see '_compute_batches').
        :param edit_mode:   Is the wizard in edition mode.
        """
        payments = self.env['account.payment']
        for vals in to_process:
            payments |= vals['payment']
        payments.with_context(skip_sale_auto_invoice_send=True).action_post()

    def _reconcile_payments(self, to_process, edit_mode=False):
        """ Reconcile the payments.

        :param to_process:  A list of python dictionary, one for each payment to create, containing:
                            * create_vals:  The values used for the 'create' method.
                            * to_reconcile: The journal items to perform the reconciliation.
                            * batch:        A python dict containing everything you want about the source journal items
                                            to which a payment will be created (see '_compute_batches').
        :param edit_mode:   Is the wizard in edition mode.
        """
        domain = [
            ('parent_state', '=', 'posted'),
            ('account_type', 'in', self.env['account.payment']._get_valid_payment_account_types()),
            ('reconciled', '=', False),
        ]
        for vals in to_process:
            payment = vals['payment']
            payment_lines = payment.move_id.line_ids.filtered_domain(domain)
            lines = vals['to_reconcile']
            extra_context = {'forced_rate_from_register_payment': vals['rate']} if 'rate' in vals else {}

            for account in payment_lines.account_id:
                (payment_lines + lines)\
                    .with_context(**extra_context)\
                    .filtered_domain([
                        ('account_id', '=', account.id),
                        ('reconciled', '=', False),
                    ])\
                    .reconcile()
            lines.move_id.matched_payment_ids += payment

    def _create_payments(self):
        self.ensure_one()
        batches = []
        # Skip batches that are not valid (bank account not setup or not trusted but required)
        for batch in self.batches:
            batch_account = self._get_batch_account(batch)
            if self.require_partner_bank_account and (not batch_account or not batch_account.allow_out_payment):
                continue
            batches.append(batch)

        if not batches:
            raise UserError(_(
                "To record payments with %(payment_method)s, the recipient bank account must be manually validated. You should go on the partner bank account in order to validate it.",
                payment_method=self.payment_method_line_id.name,
            ))

        first_batch_result = batches[0]
        edit_mode = self.can_edit_wizard and (len(first_batch_result['lines']) == 1 or self.group_payment)
        to_process = []

        if edit_mode:
            payment_vals = self._create_payment_vals_from_wizard(first_batch_result)
            to_process_values = {
                'create_vals': payment_vals,
                'to_reconcile': first_batch_result['lines'],
                'batch': first_batch_result,
            }

            # Force the rate during the reconciliation to put the difference directly on the
            # exchange difference.
            if self.writeoff_is_exchange_account and self.currency_id == self.company_currency_id:
                total_batch_residual = sum(first_batch_result['lines'].mapped('amount_residual_currency'))
                to_process_values['rate'] = abs(total_batch_residual / self.amount) if self.amount else 0.0

            to_process.append(to_process_values)
        else:
            if not self.group_payment:
                # Don't group payments: Create one batch per move.
                lines_to_pay = self._get_total_amounts_to_pay(batches)['lines'] if self.installments_mode in ('next', 'overdue', 'before_date') else self.line_ids
                new_batches = []
                for batch_result in batches:
                    sub_batches = {}
                    for line in batch_result['lines']:
                        if line not in lines_to_pay:
                            continue
                        if line.move_id.id in sub_batches:
                            sub_batches[line.move_id.id]['lines'] += line
                        else:
                            sub_batches[line.move_id.id] = {
                                **batch_result,
                                'payment_values': {
                                    **batch_result['payment_values'],
                                    'payment_type': 'inbound' if line.balance > 0 else 'outbound'
                                },
                                'lines': line,
                            }
                    new_batches.extend(sub_batches.values())
                batches = new_batches

            for batch_result in batches:
                to_process.append({
                    'create_vals': self._create_payment_vals_from_batch(batch_result),
                    'to_reconcile': batch_result['lines'],
                    'batch': batch_result,
                })

        lines = sum((batch_result['lines'] for batch_result in batches), self.env['account.move.line'])
        from_sibling_companies = self._from_sibling_companies(lines)
        if from_sibling_companies and lines.company_id.root_id not in self.env.companies:
            # Payment made for sibling companies, we don't want to redirect to the payments
            # to avoid access error, as it will be created as parent company.
            self.env(context={**self.env.context, "dont_redirect_to_payments": True})

        wizard = self.sudo() if from_sibling_companies else self

        # Prevent default_ context keys to interfere with account.payment context (eg: ``default_partner_bank_id``
        # transfered from ``account.payment.register`` wizard to ``account.payment`` creation.
        payments = wizard.with_context(clean_context(self.env.context))._init_payments(to_process, edit_mode=edit_mode)
        wizard._post_payments(to_process, edit_mode=edit_mode)
        wizard._reconcile_payments(to_process, edit_mode=edit_mode)
        return payments.sudo(flag=False)

    def _get_next_payment_date_in_context(self):
        if active_domain := self.env.context.get('active_domain'):
            for domain_elem in active_domain:
                if isinstance(domain_elem, (list, tuple)) and domain_elem[0] == 'next_payment_date' and len(domain_elem) == 3 and isinstance(domain_elem[2], str):
                    return fields.Date.to_date(domain_elem[2])
        return False

    def action_create_payments(self):
        if self.is_register_payment_on_draft:
            self.payment_difference_handling = 'open'
        payments = self._create_payments()

        if self.env.context.get('dont_redirect_to_payments'):
            return True

        action = {
            'name': _('Payments'),
            'type': 'ir.actions.act_window',
            'res_model': 'account.payment',
            'context': {'create': False},
        }
        if len(payments) == 1:
            action.update({
                'view_mode': 'form',
                'res_id': payments.id,
            })
        else:
            action.update({
                'view_mode': 'list,form',
                'domain': [('id', 'in', payments.ids)],
            })
        return action

    def _get_batch_account(self, batch_result):
        # Get the batch bank account
        partner_bank_id = batch_result['payment_values']['partner_bank_id']
        available_partner_banks = self._get_batch_available_partner_banks(batch_result, self.journal_id)
        if partner_bank_id and partner_bank_id in available_partner_banks.ids:
            return self.env['res.partner.bank'].browse(partner_bank_id)
        else:
            return available_partner_banks[:1]

    def action_open_untrusted_bank_accounts(self):
        self.ensure_one()
        if len(self.untrusted_bank_ids) == 1:
            action = {
                "view_mode": "form",
                "res_model": "res.partner.bank",
                "type": "ir.actions.act_window",
                "res_id": self.untrusted_bank_ids.id,
                "views": [[self.env.ref("account.view_partner_bank_form_inherit_account").id, "form"]],
            }
        else:
            action = {
                "type": "ir.actions.act_window",
                "res_model": "res.partner.bank",
                "views": [[False, "list"], [self.env.ref("account.view_partner_bank_form_inherit_account").id, "form"]],
                "domain": [["id", "in", self.untrusted_bank_ids.ids]],
            }

        return action

    def action_open_missing_account_partners(self):
        self.ensure_one()
        vals = {}
        if len(self.missing_account_partners) > 1:
            listview_id = self.env.ref('account.partner_missing_account_list_view').id
            vals['views'] = [(listview_id, 'list'), (False, "form")]
        return self.missing_account_partners._get_records_action(**vals)
