Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,850 @@
# 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()