# Fusion Accounting - Manual Reconciliation Wizard
# Handles partial/full reconciliation of selected journal items with
# optional write-off, account transfers, and tax support.

from collections import defaultdict
from datetime import timedelta

from odoo import api, Command, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import groupby, SQL
from odoo.tools.misc import formatLang


class AccountReconcileWizard(models.TransientModel):
    """Interactive wizard for reconciling selected journal items,
    optionally generating write-off or account transfer entries."""

    _name = 'account.reconcile.wizard'
    _description = 'Account reconciliation wizard'
    _check_company_auto = True

    # -------------------------------------------------------------------------
    # Default Values
    # -------------------------------------------------------------------------

    @api.model
    def default_get(self, field_names):
        """Validate and load the selected journal items from the context."""
        result = super().default_get(field_names)
        if 'move_line_ids' not in field_names:
            return result

        active_model = self.env.context.get('active_model')
        active_ids = self.env.context.get('active_ids')
        if active_model != 'account.move.line' or not active_ids:
            raise UserError(_('This wizard can only be used on journal items.'))

        selected_lines = self.env['account.move.line'].browse(active_ids)
        involved_accounts = selected_lines.account_id

        if len(involved_accounts) > 2:
            raise UserError(_(
                'Reconciliation is limited to at most two accounts: %s',
                ', '.join(involved_accounts.mapped('display_name')),
            ))

        # When two accounts are involved, shadow the secondary account
        override_map = None
        if len(involved_accounts) == 2:
            primary_account = selected_lines[0].account_id
            override_map = {
                line: {'account_id': primary_account}
                for line in selected_lines
                if line.account_id != primary_account
            }

        selected_lines._check_amls_exigibility_for_reconciliation(
            shadowed_aml_values=override_map,
        )
        result['move_line_ids'] = [Command.set(selected_lines.ids)]
        return result

    # -------------------------------------------------------------------------
    # Field Declarations
    # -------------------------------------------------------------------------

    company_id = fields.Many2one(
        comodel_name='res.company',
        required=True,
        readonly=True,
        compute='_compute_company_id',
    )
    move_line_ids = fields.Many2many(
        comodel_name='account.move.line',
        string='Move lines to reconcile',
        required=True,
    )
    reco_account_id = fields.Many2one(
        comodel_name='account.account',
        string='Reconcile Account',
        compute='_compute_reco_wizard_data',
    )
    amount = fields.Monetary(
        string='Amount in company currency',
        currency_field='company_currency_id',
        compute='_compute_reco_wizard_data',
    )
    company_currency_id = fields.Many2one(
        comodel_name='res.currency',
        string='Company currency',
        related='company_id.currency_id',
    )
    amount_currency = fields.Monetary(
        string='Amount',
        currency_field='reco_currency_id',
        compute='_compute_reco_wizard_data',
    )
    reco_currency_id = fields.Many2one(
        comodel_name='res.currency',
        string='Currency to use for reconciliation',
        compute='_compute_reco_wizard_data',
    )
    edit_mode_amount = fields.Monetary(
        currency_field='company_currency_id',
        compute='_compute_edit_mode_amount',
    )
    edit_mode_amount_currency = fields.Monetary(
        string='Edit mode amount',
        currency_field='edit_mode_reco_currency_id',
        compute='_compute_edit_mode_amount_currency',
        store=True,
        readonly=False,
    )
    edit_mode_reco_currency_id = fields.Many2one(
        comodel_name='res.currency',
        compute='_compute_edit_mode_reco_currency',
    )
    edit_mode = fields.Boolean(
        compute='_compute_edit_mode',
    )
    single_currency_mode = fields.Boolean(
        compute='_compute_single_currency_mode',
    )
    allow_partials = fields.Boolean(
        string="Allow partials",
        compute='_compute_allow_partials',
        store=True,
        readonly=False,
    )
    force_partials = fields.Boolean(
        compute='_compute_reco_wizard_data',
    )
    display_allow_partials = fields.Boolean(
        compute='_compute_display_allow_partials',
    )
    date = fields.Date(
        string='Date',
        compute='_compute_date',
        store=True,
        readonly=False,
    )
    journal_id = fields.Many2one(
        comodel_name='account.journal',
        string='Journal',
        check_company=True,
        domain="[('type', '=', 'general')]",
        compute='_compute_journal_id',
        store=True,
        readonly=False,
        required=True,
        precompute=True,
    )
    account_id = fields.Many2one(
        comodel_name='account.account',
        string='Account',
        check_company=True,
        domain="[('account_type', '!=', 'off_balance')]",
    )
    is_rec_pay_account = fields.Boolean(
        compute='_compute_is_rec_pay_account',
    )
    to_partner_id = fields.Many2one(
        comodel_name='res.partner',
        string='Partner',
        check_company=True,
        compute='_compute_to_partner_id',
        store=True,
        readonly=False,
    )
    label = fields.Char(string='Label', default='Write-Off')
    tax_id = fields.Many2one(
        comodel_name='account.tax',
        string='Tax',
        default=False,
        check_company=True,
    )
    to_check = fields.Boolean(
        string='To Check',
        default=False,
        help='Flag this entry for review if information is uncertain.',
    )
    is_write_off_required = fields.Boolean(
        string='Is a write-off move required to reconcile',
        compute='_compute_is_write_off_required',
    )
    is_transfer_required = fields.Boolean(
        string='Is an account transfer required',
        compute='_compute_reco_wizard_data',
    )
    transfer_warning_message = fields.Char(
        string='Transfer warning message',
        compute='_compute_reco_wizard_data',
    )
    transfer_from_account_id = fields.Many2one(
        comodel_name='account.account',
        string='Account Transfer From',
        compute='_compute_reco_wizard_data',
    )
    lock_date_violated_warning_message = fields.Char(
        string='Lock date violation warning',
        compute='_compute_lock_date_violated_warning_message',
    )
    reco_model_id = fields.Many2one(
        comodel_name='account.reconcile.model',
        string='Reconciliation model',
        store=False,
        check_company=True,
    )
    reco_model_autocomplete_ids = fields.Many2many(
        comodel_name='account.reconcile.model',
        string='All reconciliation models',
        compute='_compute_reco_model_autocomplete_ids',
    )

    # -------------------------------------------------------------------------
    # Compute Methods
    # -------------------------------------------------------------------------

    @api.depends('move_line_ids.company_id')
    def _compute_company_id(self):
        for wiz in self:
            wiz.company_id = wiz.move_line_ids[0].company_id

    @api.depends('move_line_ids')
    def _compute_single_currency_mode(self):
        for wiz in self:
            foreign_currencies = wiz.move_line_ids.currency_id - wiz.company_currency_id
            wiz.single_currency_mode = len(foreign_currencies) <= 1

    @api.depends('force_partials')
    def _compute_allow_partials(self):
        for wiz in self:
            wiz.allow_partials = wiz.display_allow_partials and wiz.force_partials

    @api.depends('move_line_ids')
    def _compute_display_allow_partials(self):
        """Show the partial reconciliation checkbox only when both
        debit and credit items are selected."""
        for wiz in self:
            found_debit = found_credit = False
            for line in wiz.move_line_ids:
                if line.balance > 0.0 or line.amount_currency > 0.0:
                    found_debit = True
                elif line.balance < 0.0 or line.amount_currency < 0.0:
                    found_credit = True
                if found_debit and found_credit:
                    break
            wiz.display_allow_partials = found_debit and found_credit

    @api.depends('move_line_ids', 'journal_id', 'tax_id')
    def _compute_date(self):
        for wiz in self:
            latest_date = max(ln.date for ln in wiz.move_line_ids)
            temp_entry = self.env['account.move'].new({
                'journal_id': wiz.journal_id.id,
            })
            wiz.date = temp_entry._get_accounting_date(
                latest_date, bool(wiz.tax_id),
            )

    @api.depends('company_id')
    def _compute_journal_id(self):
        for wiz in self:
            wiz.journal_id = self.env['account.journal'].search([
                *self.env['account.journal']._check_company_domain(wiz.company_id),
                ('type', '=', 'general'),
            ], limit=1)

    @api.depends('account_id')
    def _compute_is_rec_pay_account(self):
        for wiz in self:
            wiz.is_rec_pay_account = (
                wiz.account_id.account_type
                in ('asset_receivable', 'liability_payable')
            )

    @api.depends('is_rec_pay_account')
    def _compute_to_partner_id(self):
        for wiz in self:
            if wiz.is_rec_pay_account:
                linked_partners = wiz.move_line_ids.partner_id
                wiz.to_partner_id = linked_partners if len(linked_partners) == 1 else None
            else:
                wiz.to_partner_id = None

    @api.depends('amount', 'amount_currency')
    def _compute_is_write_off_required(self):
        """A write-off is needed when the balance does not reach zero."""
        for wiz in self:
            company_zero = wiz.company_currency_id.is_zero(wiz.amount)
            reco_zero = (
                wiz.reco_currency_id.is_zero(wiz.amount_currency)
                if wiz.reco_currency_id else True
            )
            wiz.is_write_off_required = not company_zero or not reco_zero

    @api.depends('move_line_ids')
    def _compute_reco_wizard_data(self):
        """Compute reconciliation currency, transfer requirements,
        and write-off amounts from the selected journal items."""

        def _determine_transfer_info(lines, accts):
            """When two accounts are involved, decide which one to transfer from."""
            balance_per_acct = defaultdict(float)
            for ln in lines:
                balance_per_acct[ln.account_id] += ln.amount_residual
            # Transfer from the account with the smaller absolute balance
            if abs(balance_per_acct[accts[0]]) < abs(balance_per_acct[accts[1]]):
                src_acct, dst_acct = accts[0], accts[1]
            else:
                src_acct, dst_acct = accts[1], accts[0]

            transfer_lines = lines.filtered(lambda ln: ln.account_id == src_acct)
            foreign_curs = lines.currency_id - lines.company_currency_id
            if len(foreign_curs) == 1:
                xfer_currency = foreign_curs
                xfer_amount = sum(ln.amount_currency for ln in transfer_lines)
            else:
                xfer_currency = lines.company_currency_id
                xfer_amount = sum(ln.balance for ln in transfer_lines)

            if xfer_amount == 0.0 and xfer_currency != lines.company_currency_id:
                xfer_currency = lines.company_currency_id
                xfer_amount = sum(ln.balance for ln in transfer_lines)

            formatted_amt = formatLang(
                self.env, abs(xfer_amount), currency_obj=xfer_currency,
            )
            warning_msg = _(
                'An entry will transfer %(amount)s from %(from_account)s to %(to_account)s.',
                amount=formatted_amt,
                from_account=(src_acct.display_name if xfer_amount < 0
                              else dst_acct.display_name),
                to_account=(dst_acct.display_name if xfer_amount < 0
                            else src_acct.display_name),
            )
            return {
                'transfer_from_account_id': src_acct,
                'reco_account_id': dst_acct,
                'transfer_warning_message': warning_msg,
            }

        def _resolve_reco_currency(lines, residual_map):
            """Determine the best currency for reconciliation."""
            base_cur = lines.company_currency_id
            foreign_curs = lines.currency_id - base_cur
            if not foreign_curs:
                return base_cur
            if len(foreign_curs) == 1:
                return foreign_curs
            # Multiple foreign currencies - check which have residuals
            lines_with_balance = self.env['account.move.line']
            for ln, vals in residual_map.items():
                if vals['amount_residual'] or vals['amount_residual_currency']:
                    lines_with_balance += ln
                    if lines_with_balance and len(lines_with_balance.currency_id - base_cur) > 1:
                        return False
            return (lines_with_balance.currency_id - base_cur) or base_cur

        for wiz in self:
            amls = wiz.move_line_ids._origin
            accts = amls.account_id

            # Reset defaults
            wiz.reco_currency_id = False
            wiz.amount_currency = wiz.amount = 0.0
            wiz.force_partials = True
            wiz.transfer_from_account_id = wiz.transfer_warning_message = False
            wiz.is_transfer_required = len(accts) == 2

            if wiz.is_transfer_required:
                wiz.update(_determine_transfer_info(amls, accts))
            else:
                wiz.reco_account_id = accts

            # Shadow all items to the reconciliation account
            shadow_vals = {
                ln: {'account_id': wiz.reco_account_id}
                for ln in amls
            }

            # Build reconciliation plan
            plan_list, all_lines = amls._optimize_reconciliation_plan(
                [amls], shadowed_aml_values=shadow_vals,
            )

            # Prefetch for performance
            all_lines.move_id
            all_lines.matched_debit_ids
            all_lines.matched_credit_ids

            residual_map = {
                ln: {
                    'aml': ln,
                    'amount_residual': ln.amount_residual,
                    'amount_residual_currency': ln.amount_residual_currency,
                }
                for ln in all_lines
            }

            skip_exchange_diff = bool(
                self.env['ir.config_parameter'].sudo().get_param(
                    'account.disable_partial_exchange_diff',
                )
            )
            plan = plan_list[0]
            amls.with_context(
                no_exchange_difference=(
                    self.env.context.get('no_exchange_difference')
                    or skip_exchange_diff
                ),
            )._prepare_reconciliation_plan(
                plan, residual_map, shadowed_aml_values=shadow_vals,
            )

            resolved_cur = _resolve_reco_currency(amls, residual_map)
            if not resolved_cur:
                continue

            residual_amounts = {
                ln: ln._prepare_move_line_residual_amounts(
                    vals, resolved_cur, shadowed_aml_values=shadow_vals,
                )
                for ln, vals in residual_map.items()
            }

            # Verify all residuals are expressed in the resolved currency
            if all(
                resolved_cur in rv
                for rv in residual_amounts.values() if rv
            ):
                wiz.reco_currency_id = resolved_cur
            elif all(
                amls.company_currency_id in rv
                for rv in residual_amounts.values() if rv
            ):
                wiz.reco_currency_id = amls.company_currency_id
                resolved_cur = wiz.reco_currency_id
            else:
                continue

            # Compute write-off amounts using the most recent line's rate
            newest_line = max(amls, key=lambda ln: ln.date)
            if not newest_line.amount_currency:
                fx_rate = lower_bound = upper_bound = 0.0
            elif newest_line.currency_id == resolved_cur:
                fx_rate = abs(newest_line.balance / newest_line.amount_currency)
                tolerance = (
                    amls.company_currency_id.rounding / 2
                    / abs(newest_line.amount_currency)
                )
                lower_bound = fx_rate - tolerance
                upper_bound = fx_rate + tolerance
            else:
                fx_rate = self.env['res.currency']._get_conversion_rate(
                    resolved_cur, amls.company_currency_id,
                    amls.company_id, newest_line.date,
                )
                lower_bound = upper_bound = fx_rate

            # Identify lines at the correct rate to avoid spurious exchange diffs
            at_correct_rate = {
                ln
                for ln, rv in residual_amounts.items()
                if (
                    ln.currency_id == resolved_cur
                    and abs(ln.balance) >= ln.company_currency_id.round(
                        abs(ln.amount_currency) * lower_bound
                    )
                    and abs(ln.balance) <= ln.company_currency_id.round(
                        abs(ln.amount_currency) * upper_bound
                    )
                )
            }

            wiz.amount_currency = sum(
                rv[wiz.reco_currency_id]['residual']
                for rv in residual_amounts.values() if rv
            )
            raw_amount = sum(
                (
                    rv[amls.company_currency_id]['residual']
                    if ln in at_correct_rate
                    else rv[wiz.reco_currency_id]['residual'] * fx_rate
                )
                for ln, rv in residual_amounts.items() if rv
            )
            wiz.amount = amls.company_currency_id.round(raw_amount)
            wiz.force_partials = False

    @api.depends('move_line_ids')
    def _compute_edit_mode_amount_currency(self):
        for wiz in self:
            wiz.edit_mode_amount_currency = (
                wiz.amount_currency if wiz.edit_mode else 0.0
            )

    @api.depends('edit_mode_amount_currency')
    def _compute_edit_mode_amount(self):
        for wiz in self:
            if wiz.edit_mode:
                single_ln = wiz.move_line_ids
                conversion = (
                    abs(single_ln.amount_currency / single_ln.balance)
                    if single_ln.balance else 0.0
                )
                wiz.edit_mode_amount = (
                    single_ln.company_currency_id.round(
                        wiz.edit_mode_amount_currency / conversion
                    ) if conversion else 0.0
                )
            else:
                wiz.edit_mode_amount = 0.0

    @api.depends('move_line_ids')
    def _compute_edit_mode_reco_currency(self):
        for wiz in self:
            wiz.edit_mode_reco_currency_id = (
                wiz.move_line_ids.currency_id if wiz.edit_mode else False
            )

    @api.depends('move_line_ids')
    def _compute_edit_mode(self):
        for wiz in self:
            wiz.edit_mode = len(wiz.move_line_ids) == 1

    @api.depends('move_line_ids.move_id', 'date')
    def _compute_lock_date_violated_warning_message(self):
        for wiz in self:
            override_date = wiz._get_date_after_lock_date()
            if override_date:
                wiz.lock_date_violated_warning_message = _(
                    'The selected date conflicts with a lock date. '
                    'It will be adjusted to: %(replacement_date)s',
                    replacement_date=override_date,
                )
            else:
                wiz.lock_date_violated_warning_message = None

    @api.depends('company_id')
    def _compute_reco_model_autocomplete_ids(self):
        """Find reconciliation models of type write-off with exactly one line."""
        for wiz in self:
            filter_domain = [
                ('rule_type', '=', 'writeoff_button'),
                ('company_id', '=', wiz.company_id.id),
                ('counterpart_type', 'not in', ('sale', 'purchase')),
            ]
            q = self.env['account.reconcile.model']._where_calc(filter_domain)
            matching_ids = [
                row[0] for row in self.env.execute_query(SQL("""
                    SELECT arm.id
                    FROM %s
                    JOIN account_reconcile_model_line arml
                        ON arml.model_id = arm.id
                    WHERE %s
                    GROUP BY arm.id
                    HAVING COUNT(arm.id) = 1
                """, q.from_clause, q.where_clause or SQL("TRUE")))
            ]
            wiz.reco_model_autocomplete_ids = (
                self.env['account.reconcile.model'].browse(matching_ids)
            )

    # -------------------------------------------------------------------------
    # Onchange
    # -------------------------------------------------------------------------

    @api.onchange('reco_model_id')
    def _onchange_reco_model_id(self):
        """Pre-fill write-off details from the selected reconciliation model."""
        if self.reco_model_id:
            model_line = self.reco_model_id.line_ids
            self.to_check = self.reco_model_id.to_check
            self.label = model_line.label
            self.tax_id = model_line.tax_ids[0] if model_line[0].tax_ids else None
            self.journal_id = model_line.journal_id
            self.account_id = model_line.account_id

    # -------------------------------------------------------------------------
    # Constraints
    # -------------------------------------------------------------------------

    @api.constrains('edit_mode_amount_currency')
    def _check_min_max_edit_mode_amount_currency(self):
        for wiz in self:
            if not wiz.edit_mode:
                continue
            if wiz.edit_mode_amount_currency == 0.0:
                raise UserError(_(
                    'The write-off amount for a single line cannot be zero.'
                ))
            is_debit = (
                wiz.move_line_ids.balance > 0.0
                or wiz.move_line_ids.amount_currency > 0.0
            )
            if is_debit and wiz.edit_mode_amount_currency < 0.0:
                raise UserError(_(
                    'The write-off amount for a debit line must be positive.'
                ))
            if not is_debit and wiz.edit_mode_amount_currency > 0.0:
                raise UserError(_(
                    'The write-off amount for a credit line must be negative.'
                ))

    # -------------------------------------------------------------------------
    # Actions
    # -------------------------------------------------------------------------

    def _action_open_wizard(self):
        self.ensure_one()
        return {
            'name': _('Write-Off Entry'),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'account.reconcile.wizard',
            'target': 'new',
        }

    # -------------------------------------------------------------------------
    # Business Logic
    # -------------------------------------------------------------------------

    def _get_date_after_lock_date(self):
        """Return the first valid date if the current date violates a lock."""
        self.ensure_one()
        violated = self.company_id._get_violated_lock_dates(
            self.date, bool(self.tax_id), self.journal_id,
        )
        if violated:
            return violated[-1][0] + timedelta(days=1)

    def _compute_write_off_taxes_data(self, partner):
        """Calculate tax breakdown for the write-off entry, including
        base and per-repartition-line tax amounts."""
        TaxEngine = self.env['account.tax']
        wo_amount_currency = self.edit_mode_amount_currency or self.amount_currency
        wo_amount = self.edit_mode_amount or self.amount
        conversion_rate = abs(wo_amount_currency / wo_amount)

        tax_usage = self.tax_id.type_tax_use if self.tax_id else None
        is_refund_entry = (
            (tax_usage == 'sale' and wo_amount_currency > 0.0)
            or (tax_usage == 'purchase' and wo_amount_currency < 0.0)
        )

        base_input = TaxEngine._prepare_base_line_for_taxes_computation(
            self,
            partner_id=partner,
            currency_id=self.reco_currency_id,
            tax_ids=self.tax_id,
            price_unit=wo_amount_currency,
            quantity=1.0,
            account_id=self.account_id,
            is_refund=is_refund_entry,
            rate=conversion_rate,
            special_mode='total_included',
        )
        computation_lines = [base_input]
        TaxEngine._add_tax_details_in_base_lines(computation_lines, self.company_id)
        TaxEngine._round_base_lines_tax_details(computation_lines, self.company_id)
        TaxEngine._add_accounting_data_in_base_lines_tax_details(
            computation_lines, self.company_id, include_caba_tags=True,
        )
        tax_output = TaxEngine._prepare_tax_lines(computation_lines, self.company_id)

        _input_line, base_update = tax_output['base_lines_to_update'][0]
        tax_detail_list = []
        for tax_line_vals in tax_output['tax_lines_to_add']:
            tax_detail_list.append({
                'tax_amount': tax_line_vals['balance'],
                'tax_amount_currency': tax_line_vals['amount_currency'],
                'tax_tag_ids': tax_line_vals['tax_tag_ids'],
                'tax_account_id': tax_line_vals['account_id'],
            })

        base_amt_currency = base_update['amount_currency']
        base_amt = wo_amount - sum(d['tax_amount'] for d in tax_detail_list)

        return {
            'base_amount': base_amt,
            'base_amount_currency': base_amt_currency,
            'base_tax_tag_ids': base_update['tax_tag_ids'],
            'tax_lines_data': tax_detail_list,
        }

    def _create_write_off_lines(self, partner=None):
        """Build Command.create entries for the write-off journal entry."""
        if not partner:
            partner = self.env['res.partner']
        target_partner = self.to_partner_id if self.is_rec_pay_account else partner
        tax_info = (
            self._compute_write_off_taxes_data(target_partner)
            if self.tax_id else None
        )
        wo_amount_currency = self.edit_mode_amount_currency or self.amount_currency
        wo_amount = self.edit_mode_amount or self.amount

        line_commands = [
            # Counterpart on reconciliation account
            Command.create({
                'name': self.label or _('Write-Off'),
                'account_id': self.reco_account_id.id,
                'partner_id': partner.id,
                'currency_id': self.reco_currency_id.id,
                'amount_currency': -wo_amount_currency,
                'balance': -wo_amount,
            }),
            # Write-off on target account
            Command.create({
                'name': self.label,
                'account_id': self.account_id.id,
                'partner_id': target_partner.id,
                'currency_id': self.reco_currency_id.id,
                'tax_ids': self.tax_id.ids,
                'tax_tag_ids': (
                    tax_info['base_tax_tag_ids'] if tax_info else None
                ),
                'amount_currency': (
                    tax_info['base_amount_currency']
                    if tax_info else wo_amount_currency
                ),
                'balance': (
                    tax_info['base_amount'] if tax_info else wo_amount
                ),
            }),
        ]

        # Append one line per tax repartition
        if tax_info:
            for detail in tax_info['tax_lines_data']:
                line_commands.append(Command.create({
                    'name': self.tax_id.name,
                    'account_id': detail['tax_account_id'],
                    'partner_id': target_partner.id,
                    'currency_id': self.reco_currency_id.id,
                    'tax_tag_ids': detail['tax_tag_ids'],
                    'amount_currency': detail['tax_amount_currency'],
                    'balance': detail['tax_amount'],
                }))

        return line_commands

    def create_write_off(self):
        """Create and post a write-off journal entry."""
        self.ensure_one()
        linked_partners = self.move_line_ids.partner_id
        partner = linked_partners if len(linked_partners) == 1 else None

        entry_vals = {
            'journal_id': self.journal_id.id,
            'company_id': self.company_id.id,
            'date': self._get_date_after_lock_date() or self.date,
            'checked': not self.to_check,
            'line_ids': self._create_write_off_lines(partner=partner),
        }
        wo_move = self.env['account.move'].with_context(
            skip_invoice_sync=True,
            skip_invoice_line_sync=True,
        ).create(entry_vals)
        wo_move.action_post()
        return wo_move

    def create_transfer(self):
        """Create and post an account transfer entry, grouped by partner
        and currency to maintain correct partner ledger balances."""
        self.ensure_one()
        transfer_cmds = []
        source_lines = self.move_line_ids.filtered(
            lambda ln: ln.account_id == self.transfer_from_account_id,
        )

        for (partner, currency), grouped_lines in groupby(
            source_lines, lambda ln: (ln.partner_id, ln.currency_id),
        ):
            group_balance = sum(ln.amount_residual for ln in grouped_lines)
            group_amt_cur = sum(ln.amount_residual_currency for ln in grouped_lines)
            transfer_cmds.extend([
                Command.create({
                    'name': _('Transfer from %s', self.transfer_from_account_id.display_name),
                    'account_id': self.reco_account_id.id,
                    'partner_id': partner.id,
                    'currency_id': currency.id,
                    'amount_currency': group_amt_cur,
                    'balance': group_balance,
                }),
                Command.create({
                    'name': _('Transfer to %s', self.reco_account_id.display_name),
                    'account_id': self.transfer_from_account_id.id,
                    'partner_id': partner.id,
                    'currency_id': currency.id,
                    'amount_currency': -group_amt_cur,
                    'balance': -group_balance,
                }),
            ])

        xfer_move = self.env['account.move'].create({
            'journal_id': self.journal_id.id,
            'company_id': self.company_id.id,
            'date': self._get_date_after_lock_date() or self.date,
            'line_ids': transfer_cmds,
        })
        xfer_move.action_post()
        return xfer_move

    def reconcile(self):
        """Execute reconciliation with optional transfer and/or write-off."""
        self.ensure_one()
        target_lines = self.move_line_ids._origin
        needs_transfer = self.is_transfer_required
        needs_writeoff = (
            self.edit_mode
            or (self.is_write_off_required and not self.allow_partials)
        )

        # Handle account transfer if two accounts are involved
        if needs_transfer:
            xfer_move = self.create_transfer()
            from_lines = target_lines.filtered(
                lambda ln: ln.account_id == self.transfer_from_account_id,
            )
            xfer_from_lines = xfer_move.line_ids.filtered(
                lambda ln: ln.account_id == self.transfer_from_account_id,
            )
            xfer_to_lines = xfer_move.line_ids.filtered(
                lambda ln: ln.account_id == self.reco_account_id,
            )
            (from_lines + xfer_from_lines).reconcile()
            target_lines = target_lines - from_lines + xfer_to_lines

        # Handle write-off if balance is non-zero
        if needs_writeoff:
            wo_move = self.create_write_off()
            wo_counterpart = wo_move.line_ids[0]
            target_lines += wo_counterpart
            reconcile_plan = [[target_lines, wo_counterpart]]
        else:
            reconcile_plan = [target_lines]

        self.env['account.move.line']._reconcile_plan(reconcile_plan)

        if needs_transfer:
            return target_lines + xfer_move.line_ids
        return target_lines

    def reconcile_open(self):
        """Reconcile and open the result in the reconciliation view."""
        self.ensure_one()
        return self.reconcile().open_reconcile_view()
