1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/account_accountant/wizard/account_reconcile_wizard.py
2024-12-10 09:04:09 +07:00

560 lines
26 KiB
Python

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
from odoo.tools.misc import formatLang
class AccountReconcileWizard(models.TransientModel):
""" This wizard is used to reconcile selected account.move.line. """
_name = 'account.reconcile.wizard'
_description = 'Account reconciliation wizard'
_check_company_auto = True
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if 'move_line_ids' not in fields_list:
return res
if self.env.context.get('active_model') != 'account.move.line' or not self.env.context.get('active_ids'):
raise UserError(_('This can only be used on journal items'))
move_line_ids = self.env['account.move.line'].browse(self.env.context['active_ids'])
accounts = move_line_ids.account_id
if len(accounts) > 2:
raise UserError(_(
'You can only reconcile entries with up to two different accounts: %s',
', '.join(accounts.mapped('display_name')),
))
shadowed_aml_values = None
if len(accounts) == 2:
shadowed_aml_values = {
aml: {'account_id': move_line_ids[0].account_id}
for aml in move_line_ids.filtered(lambda line: line.account_id != move_line_ids[0].account_id)
}
move_line_ids._check_amls_exigibility_for_reconciliation(shadowed_aml_values=shadowed_aml_values)
res['move_line_ids'] = [Command.set(move_line_ids.ids)]
return res
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')
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="[('deprecated', '=', False), ('internal_group', '!=', 'off_balance')]")
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='Check if you are not certain of all the information of the counterpart.')
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='Is an account transfer required to reconcile',
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='Is the date violating the lock date of moves',
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 wizard in self:
wizard.company_id = wizard.move_line_ids[0].company_id
@api.depends('reco_currency_id', 'company_currency_id')
def _compute_single_currency_mode(self):
for wizard in self:
wizard.single_currency_mode = wizard.reco_currency_id == wizard.company_currency_id
@api.depends('force_partials')
def _compute_allow_partials(self):
for wizard in self:
wizard.allow_partials = wizard.display_allow_partials and wizard.force_partials
@api.depends('move_line_ids')
def _compute_display_allow_partials(self):
for wizard in self:
wizard.display_allow_partials = has_debit_line = has_credit_line = False
for aml in wizard.move_line_ids:
if aml.balance > 0.0 or aml.amount_currency > 0.0:
has_debit_line = True
elif aml.balance < 0.0 or aml.amount_currency < 0.0:
has_credit_line = True
if has_debit_line and has_credit_line:
wizard.display_allow_partials = True
break
@api.depends('move_line_ids', 'journal_id', 'tax_id')
def _compute_date(self):
for wizard in self:
highest_date = max(aml.date for aml in wizard.move_line_ids)
temp_move = self.env['account.move'].new({'journal_id': wizard.journal_id.id})
wizard.date = temp_move._get_accounting_date(highest_date, bool(wizard.tax_id))
@api.depends('company_id')
def _compute_journal_id(self):
for wizard in self:
wizard.journal_id = self.env['account.journal'].search([
*self.env['account.journal']._check_company_domain(wizard.company_id),
('type', '=', 'general')
], limit=1)
@api.depends('amount', 'amount_currency')
def _compute_is_write_off_required(self):
""" We need a write-off if the balance is not 0 and if we don't allow partial reconciliation."""
for wizard in self:
wizard.is_write_off_required = not wizard.company_currency_id.is_zero(wizard.amount) \
or (wizard.reco_currency_id and not wizard.reco_currency_id.is_zero(wizard.amount_currency))
@api.depends('move_line_ids')
def _compute_reco_wizard_data(self):
""" Compute various data needed for the reco wizard.
1. The currency to use for the reconciliation:
- if only one foreign currency is present in move lines we use it, unless the reco_account is not
payable nor receivable,
- if no foreign currency or more than 1 are used we use the company's default currency.
2. The account the reconciliation will happen on.
3. Transfer data.
4. Write-off amounts.
"""
def get_transfer_data(move_lines):
amounts_per_account = defaultdict(float)
for line in move_lines:
amounts_per_account[line.account_id] += line.amount_residual
if abs(amounts_per_account[accounts[0]]) < abs(amounts_per_account[accounts[1]]):
transfer_from_account, transfer_to_account = accounts[0], accounts[1]
else:
transfer_from_account, transfer_to_account = accounts[1], accounts[0]
amls_to_transfer = amls.filtered(lambda aml: aml.account_id == transfer_from_account)
transfer_foreign_curr = amls.currency_id - amls.company_currency_id
if len(transfer_foreign_curr) == 1:
transfer_currency = transfer_foreign_curr
transfer_amount_currency = sum(aml.amount_currency for aml in amls_to_transfer)
else:
transfer_currency = amls.company_currency_id
transfer_amount_currency = sum(aml.balance for aml in amls_to_transfer)
if transfer_amount_currency == 0.0 and transfer_currency != amls.company_currency_id:
# handle the transfer of exchange diff
transfer_currency = amls.company_currency_id
transfer_amount_currency = sum(aml.balance for aml in amls_to_transfer)
amount_formatted = formatLang(self.env, abs(transfer_amount_currency), currency_obj=transfer_currency)
transfer_warning_message = _(
'An entry will transfer %(amount)s from %(from_account)s to %(to_account)s.',
amount=amount_formatted,
from_account=transfer_from_account.display_name if transfer_amount_currency < 0 else transfer_to_account.display_name,
to_account=transfer_to_account.display_name if transfer_amount_currency < 0 else transfer_from_account.display_name,
)
return {
'transfer_from_account_id': transfer_from_account,
'reco_account_id': transfer_to_account,
'transfer_warning_message': transfer_warning_message,
}
def get_reco_currency(amls, aml_values_map):
company_currency = amls.company_currency_id
foreign_currencies = amls.currency_id - company_currency
if len(foreign_currencies) == 0:
return company_currency
elif len(foreign_currencies) == 1:
return foreign_currencies
else:
lines_with_residuals = self.env['account.move.line']
for residual, residual_values in aml_values_map.items():
if residual_values['amount_residual'] or residual_values['amount_residual_currency']:
lines_with_residuals += residual
if lines_with_residuals and len(lines_with_residuals.currency_id - company_currency) > 1:
# there is more than one residual and more than one currency in them
return False
return (lines_with_residuals.currency_id - company_currency) or company_currency
for wizard in self:
amls = wizard.move_line_ids._origin
accounts = amls.account_id # there is only 1 or 2 possible accounts
wizard.reco_currency_id = False
wizard.amount_currency = wizard.amount = 0.0
wizard.force_partials = True
wizard.transfer_from_account_id = wizard.transfer_warning_message = False
wizard.is_transfer_required = len(accounts) == 2
if wizard.is_transfer_required:
wizard.update(get_transfer_data(amls))
else:
wizard.reco_account_id = accounts
# Compute the residual amounts for each account.
shadowed_aml_values = {
aml: {'account_id': wizard.reco_account_id}
for aml in amls
}
# Batch the amls all together to know what should be reconciled and when.
plan_list, all_amls = amls._optimize_reconciliation_plan([amls], shadowed_aml_values=shadowed_aml_values)
# Prefetch data
all_amls.move_id
all_amls.matched_debit_ids
all_amls.matched_credit_ids
# All residual amounts are collected and updated until the creation of partials in batch.
# This is done that way to minimize the orm time for fields invalidation/mark as recompute and
# re-computation.
aml_values_map = {
aml: {
'aml': aml,
'amount_residual': aml.amount_residual,
'amount_residual_currency': aml.amount_residual_currency,
}
for aml in all_amls
}
disable_partial_exchange_diff = bool(self.env['ir.config_parameter'].sudo().get_param('account.disable_partial_exchange_diff'))
plan = plan_list[0]
# residuals are subtracted from aml_values_map
amls\
.with_context(no_exchange_difference=self._context.get('no_exchange_difference') or disable_partial_exchange_diff) \
._prepare_reconciliation_plan(plan, aml_values_map, shadowed_aml_values=shadowed_aml_values)
reco_currency = get_reco_currency(amls, aml_values_map)
if not reco_currency:
continue
residual_amounts = {
aml: aml._prepare_move_line_residual_amounts(aml_values, reco_currency, shadowed_aml_values=shadowed_aml_values)
for aml, aml_values in aml_values_map.items()
}
if all(reco_currency in residual_values for residual_values in residual_amounts.values() if residual_values):
wizard.reco_currency_id = reco_currency
elif all(amls.company_currency_id in residual_values for residual_values in residual_amounts.values() if residual_values):
wizard.reco_currency_id = amls.company_currency_id
reco_currency = wizard.reco_currency_id
else:
continue
# Compute write-off amounts
most_recent_line = max(amls, key=lambda aml: aml.date)
if most_recent_line.currency_id == reco_currency:
rate = abs(most_recent_line.amount_currency / most_recent_line.balance) if most_recent_line.balance else 0.0
else:
rate = wizard.reco_currency_id._get_conversion_rate(amls.company_currency_id, reco_currency, amls.company_id, most_recent_line.date)
wizard.amount_currency = sum(
residual_values[wizard.reco_currency_id]['residual']
for residual_values in residual_amounts.values()
if residual_values
)
wizard.amount = amls.company_currency_id.round(wizard.amount_currency / rate) if rate else 0.0
wizard.force_partials = False
@api.depends('move_line_ids.move_id', 'date')
def _compute_lock_date_violated_warning_message(self):
for wizard in self:
date_after_lock = wizard._get_date_after_lock_date()
lock_date_violated_warning_message = None
if date_after_lock:
lock_date_violated_warning_message = _(
'The date you set violates the lock date of one of your entry. It will be overriden by the following date : %(replacement_date)s',
replacement_date=date_after_lock,
)
wizard.lock_date_violated_warning_message = lock_date_violated_warning_message
@api.depends('company_id')
def _compute_reco_model_autocomplete_ids(self):
""" Computes available reconcile models, we only take models that are of type 'writeoff_button'
and that have one (and only one) line.
"""
for wizard in self:
domain = [
('rule_type', '=', 'writeoff_button'),
('company_id', '=', wizard.company_id.id),
]
query = self.env['account.reconcile.model']._where_calc(domain)
tables, where_clause, where_params = query.get_sql()
query_str = f"""
SELECT account_reconcile_model.id
FROM {tables}
JOIN account_reconcile_model_line line ON line.model_id = account_reconcile_model.id
WHERE {where_clause}
GROUP BY account_reconcile_model.id
HAVING COUNT(account_reconcile_model.id) = 1
"""
self._cr.execute(query_str, where_params)
reco_model_ids = [r[0] for r in self._cr.fetchall()]
wizard.reco_model_autocomplete_ids = self.env['account.reconcile.model'].browse(reco_model_ids)
# ==== Onchange methods ====
@api.onchange('reco_model_id')
def _onchange_reco_model_id(self):
""" We prefill the write-off data with the reconcile model selected. """
if self.reco_model_id:
self.to_check = self.reco_model_id.to_check
self.label = self.reco_model_id.line_ids.label
self.tax_id = self.reco_model_id.line_ids.tax_ids[0] if self.reco_model_id.line_ids[0].tax_ids else None
self.journal_id = self.reco_model_id.line_ids.journal_id # we limited models to those with one and only one line
self.account_id = self.reco_model_id.line_ids.account_id
# ==== Actions methods ====
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 methods ====
def _get_date_after_lock_date(self):
self.ensure_one()
lock_dates = self.company_id._get_violated_lock_dates(self.date, bool(self.tax_id))
if lock_dates:
return lock_dates[-1][0] + timedelta(days=1)
def _compute_write_off_taxes_data(self, partner_id):
""" Computes the data needed to fill the write-off lines related to taxes.
:return: a dict of the form {
'base_amount': 100.0,
'base_amount_currency': 200.0,
'tax_lines_data': [{
'tax_amount': 21.0,
'tax_amount_currency': 42.0,
'tax_tag_ids': [tax_tags],
'tax_account_id': id_of_account,
} * nr of repartition lines of the self.tax_id ],
}
"""
rate = abs(self.amount_currency / self.amount)
tax_type = self.tax_id.type_tax_use if self.tax_id else None
is_refund = (tax_type == 'sale' and self.amount_currency > 0.0) or (tax_type == 'purchase' and self.amount_currency < 0.0)
tax_data = self.env['account.tax']._convert_to_tax_base_line_dict(
self,
partner=partner_id,
currency=self.reco_currency_id,
taxes=self.tax_id,
price_unit=self.amount_currency,
quantity=1.0,
account=self.account_id,
is_refund=is_refund,
rate=rate,
handle_price_include=True,
extra_context={'force_price_include': True},
)
tax_results = self.env['account.tax']._compute_taxes(
[tax_data],
include_caba_tags=True,
)
_tax_data, base_to_update = tax_results['base_lines_to_update'][0] # we can only have one baseline
tax_lines_data = []
for tax_line_vals in tax_results['tax_lines_to_add']:
tax_lines_data.append({
'tax_amount': tax_line_vals['tax_amount'],
'tax_amount_currency': tax_line_vals['tax_amount_currency'],
'tax_tag_ids': tax_line_vals['tax_tag_ids'],
'tax_account_id': tax_line_vals['account_id'],
})
base_amount_currency = base_to_update['price_subtotal']
base_amount = self.amount - sum(entry['tax_amount'] for entry in tax_lines_data)
return {
'base_amount': base_amount,
'base_amount_currency': base_amount_currency,
'base_tax_tag_ids': base_to_update['tax_tag_ids'],
'tax_lines_data': tax_lines_data,
}
def _create_write_off_lines(self, partner=None):
tax_data = self._compute_write_off_taxes_data(partner) if self.tax_id else None
partner_id = partner.id if partner else None
line_ids_commands = [
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': -self.amount_currency,
'balance': -self.amount,
}),
Command.create({
'name': self.label,
'account_id': self.account_id.id,
'partner_id': partner_id,
'currency_id': self.reco_currency_id.id,
'tax_ids': self.tax_id.ids,
'tax_tag_ids': None if not tax_data else tax_data['base_tax_tag_ids'],
'amount_currency': self.amount_currency if not tax_data else tax_data['base_amount_currency'],
'balance': self.amount if not tax_data else tax_data['base_amount'],
}),
]
# Add taxes lines to the write-off lines, one per repartition line
if tax_data:
for tax_datum in tax_data['tax_lines_data']:
line_ids_commands.append(Command.create({
'name': self.tax_id.name,
'account_id': tax_datum['tax_account_id'],
'partner_id': partner_id,
'currency_id': self.reco_currency_id.id,
'tax_tag_ids': tax_datum['tax_tag_ids'],
'amount_currency': tax_datum['tax_amount_currency'],
'balance': tax_datum['tax_amount'],
}))
return line_ids_commands
def create_write_off(self):
""" Create write-off move lines with the data provided in the wizard. """
self.ensure_one()
partners = self.move_line_ids.partner_id
partner = partners if len(partners) == 1 else None
write_off_vals = {
'journal_id': self.journal_id.id,
'company_id': self.company_id.id,
'date': self._get_date_after_lock_date() or self.date,
'to_check': self.to_check,
'line_ids': self._create_write_off_lines(partner=partner)
}
write_off_move = self.env['account.move'].with_context(
skip_invoice_sync=True,
skip_invoice_line_sync=True,
).create(write_off_vals)
write_off_move.action_post()
return write_off_move
def create_transfer(self):
""" Create transfer move.
We transfer lines squashed by partner and by currency to keep the partner ledger correct.
"""
self.ensure_one()
# we create one transfer per partner to keep
line_ids = []
lines_to_transfer = self.move_line_ids.filtered(lambda line: line.account_id == self.transfer_from_account_id)
for (partner, currency), lines_to_transfer_partner in groupby(lines_to_transfer, lambda l: (l.partner_id, l.currency_id)):
amount = sum(line.amount_residual for line in lines_to_transfer_partner)
amount_currency = sum(line.amount_residual_currency for line in lines_to_transfer_partner)
line_ids += [
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': amount_currency,
'balance': amount,
}),
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': -amount_currency,
'balance': -amount,
}),
]
transfer_vals = {
'journal_id': self.journal_id.id,
'company_id': self.company_id.id,
'date': self._get_date_after_lock_date() or self.date,
'line_ids': line_ids,
}
transfer_move = self.env['account.move'].create(transfer_vals)
transfer_move.action_post()
return transfer_move
def reconcile(self):
""" Reconcile selected moves, with a transfer and/or write-off move if necessary."""
self.ensure_one()
move_lines_to_reconcile = self.move_line_ids._origin
do_transfer = self.is_transfer_required
do_write_off = not self.allow_partials and self.is_write_off_required
if do_transfer:
transfer_move = self.create_transfer()
lines_to_transfer = move_lines_to_reconcile \
.filtered(lambda line: line.account_id == self.transfer_from_account_id)
transfer_line_from = transfer_move.line_ids \
.filtered(lambda line: line.account_id == self.transfer_from_account_id)
transfer_line_to = transfer_move.line_ids \
.filtered(lambda line: line.account_id == self.reco_account_id)
(lines_to_transfer + transfer_line_from).reconcile()
move_lines_to_reconcile = move_lines_to_reconcile - lines_to_transfer + transfer_line_to
if do_write_off:
write_off_move = self.create_write_off()
write_off_line_to_reconcile = write_off_move.line_ids[0]
move_lines_to_reconcile += write_off_line_to_reconcile
amls_plan = [[move_lines_to_reconcile, write_off_line_to_reconcile]]
else:
amls_plan = [move_lines_to_reconcile]
self.env['account.move.line']._reconcile_plan(amls_plan)
return move_lines_to_reconcile if not do_transfer else (move_lines_to_reconcile + transfer_move.line_ids)
def reconcile_open(self):
""" Reconcile selected move lines and open them in dedicated view. """
self.ensure_one()
return self.reconcile().open_reconcile_view()