Tower: upload at_accounting 18.0.1.7 (via marketplace)
This commit is contained in:
683
addons/at_accounting/wizard/account_reconcile_wizard.py
Normal file
683
addons/at_accounting/wizard/account_reconcile_wizard.py
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
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):
|
||||||
|
""" 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')
|
||||||
|
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="[('deprecated', '=', False), ('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='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('move_line_ids')
|
||||||
|
def _compute_single_currency_mode(self):
|
||||||
|
for wizard in self:
|
||||||
|
wizard.single_currency_mode = len(wizard.move_line_ids.currency_id - wizard.company_currency_id) <= 1
|
||||||
|
|
||||||
|
@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):
|
||||||
|
""" We only display the allow partial checkbox if there is both credit and debit lines involved. """
|
||||||
|
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('account_id')
|
||||||
|
def _compute_is_rec_pay_account(self):
|
||||||
|
for wizard in self:
|
||||||
|
wizard.is_rec_pay_account = wizard.account_id.account_type in ('asset_receivable', 'liability_payable')
|
||||||
|
|
||||||
|
@api.depends('is_rec_pay_account')
|
||||||
|
def _compute_to_partner_id(self):
|
||||||
|
for wizard in self:
|
||||||
|
if wizard.is_rec_pay_account:
|
||||||
|
partners = wizard.move_line_ids.partner_id
|
||||||
|
wizard.to_partner_id = partners if len(partners) == 1 else None
|
||||||
|
else:
|
||||||
|
wizard.to_partner_id = None
|
||||||
|
|
||||||
|
@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 # stop the computation, no possible write-off => force partials
|
||||||
|
|
||||||
|
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 # stop the computation, no possible write-off => force partials
|
||||||
|
|
||||||
|
# Compute write-off amounts
|
||||||
|
most_recent_line = max(amls, key=lambda aml: aml.date)
|
||||||
|
if not most_recent_line.amount_currency:
|
||||||
|
rate = rate_lower_bound = rate_upper_bound = 0.0
|
||||||
|
elif most_recent_line.currency_id == reco_currency:
|
||||||
|
rate = abs(most_recent_line.balance / most_recent_line.amount_currency)
|
||||||
|
# By estimating the rate from the most recent line, we are exposing ourselves to an estimation error because
|
||||||
|
# balance is amount_currency * rate, rounded to the nearest precision of the company currency. We now compute
|
||||||
|
# the lower/upper bounds of the estimated rate in order to determine which other AMLs share the same rate.
|
||||||
|
# For AMLs that share the same rate, we will use the existing amount_residual instead of recomputing it
|
||||||
|
# based on the amount_residual_currency and the rate.
|
||||||
|
rate_tolerance = amls.company_currency_id.rounding / 2 / abs(most_recent_line.amount_currency)
|
||||||
|
rate_lower_bound = rate - rate_tolerance
|
||||||
|
rate_upper_bound = rate + rate_tolerance
|
||||||
|
else:
|
||||||
|
rate = self.env['res.currency']._get_conversion_rate(reco_currency, amls.company_currency_id, amls.company_id, most_recent_line.date)
|
||||||
|
rate_lower_bound = rate_upper_bound = rate
|
||||||
|
|
||||||
|
# If an AML's rate is close enough to the reconciliation rate that it could be the same,
|
||||||
|
# use the `amount_residual` instead of computing `amount_residual_currency / rate` and rounding.
|
||||||
|
# We do this for the case where a single rate is used for all reconciled AMLs so we want to avoid
|
||||||
|
# creating an exchange diff because that would be weird.
|
||||||
|
amls_where_amounts_at_correct_rate = {
|
||||||
|
aml
|
||||||
|
for aml, residual_values in residual_amounts.items()
|
||||||
|
if (
|
||||||
|
aml.currency_id == reco_currency
|
||||||
|
and abs(aml.balance) >= aml.company_currency_id.round(abs(aml.amount_currency) * rate_lower_bound)
|
||||||
|
and abs(aml.balance) <= aml.company_currency_id.round(abs(aml.amount_currency) * rate_upper_bound)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
wizard.amount_currency = sum(
|
||||||
|
residual_values[wizard.reco_currency_id]['residual']
|
||||||
|
for residual_values in residual_amounts.values()
|
||||||
|
if residual_values
|
||||||
|
)
|
||||||
|
amount_raw = sum(
|
||||||
|
(
|
||||||
|
residual_values[amls.company_currency_id]['residual']
|
||||||
|
if aml in amls_where_amounts_at_correct_rate else
|
||||||
|
residual_values[wizard.reco_currency_id]['residual'] * rate
|
||||||
|
)
|
||||||
|
for aml, residual_values in residual_amounts.items()
|
||||||
|
if residual_values
|
||||||
|
)
|
||||||
|
wizard.amount = amls.company_currency_id.round(amount_raw)
|
||||||
|
wizard.force_partials = False
|
||||||
|
|
||||||
|
@api.depends('move_line_ids')
|
||||||
|
def _compute_edit_mode_amount_currency(self):
|
||||||
|
for wizard in self:
|
||||||
|
if wizard.edit_mode:
|
||||||
|
wizard.edit_mode_amount_currency = wizard.amount_currency
|
||||||
|
else:
|
||||||
|
wizard.edit_mode_amount_currency = 0.0
|
||||||
|
|
||||||
|
@api.depends('edit_mode_amount_currency')
|
||||||
|
def _compute_edit_mode_amount(self):
|
||||||
|
for wizard in self:
|
||||||
|
if wizard.edit_mode:
|
||||||
|
single_line = wizard.move_line_ids
|
||||||
|
rate = abs(single_line.amount_currency / single_line.balance) if single_line.balance else 0.0
|
||||||
|
wizard.edit_mode_amount = single_line.company_currency_id.round(wizard.edit_mode_amount_currency / rate) if rate else 0.0
|
||||||
|
else:
|
||||||
|
wizard.edit_mode_amount = 0.0
|
||||||
|
|
||||||
|
@api.depends('move_line_ids')
|
||||||
|
def _compute_edit_mode_reco_currency(self):
|
||||||
|
for wizard in self:
|
||||||
|
if wizard.edit_mode:
|
||||||
|
wizard.edit_mode_reco_currency_id = wizard.move_line_ids.currency_id
|
||||||
|
else:
|
||||||
|
wizard.edit_mode_reco_currency_id = False
|
||||||
|
|
||||||
|
@api.depends('move_line_ids')
|
||||||
|
def _compute_edit_mode(self):
|
||||||
|
for wizard in self:
|
||||||
|
wizard.edit_mode = len(wizard.move_line_ids) == 1
|
||||||
|
|
||||||
|
@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),
|
||||||
|
('counterpart_type', 'not in', ('sale', 'purchase')),
|
||||||
|
]
|
||||||
|
query = self.env['account.reconcile.model']._where_calc(domain)
|
||||||
|
reco_model_ids = [r[0] for r in self.env.execute_query(SQL("""
|
||||||
|
SELECT account_reconcile_model.id
|
||||||
|
FROM %s
|
||||||
|
JOIN account_reconcile_model_line line ON line.model_id = account_reconcile_model.id
|
||||||
|
WHERE %s
|
||||||
|
GROUP BY account_reconcile_model.id
|
||||||
|
HAVING COUNT(account_reconcile_model.id) = 1
|
||||||
|
""", query.from_clause, query.where_clause or SQL("TRUE")))]
|
||||||
|
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
|
||||||
|
|
||||||
|
# ==== Python constrains ====
|
||||||
|
@api.constrains('edit_mode_amount_currency')
|
||||||
|
def _check_min_max_edit_mode_amount_currency(self):
|
||||||
|
for wizard in self:
|
||||||
|
if wizard.edit_mode:
|
||||||
|
if wizard.edit_mode_amount_currency == 0.0:
|
||||||
|
raise UserError(_('The amount of the write-off of a single line cannot be 0.'))
|
||||||
|
is_debit_line = wizard.move_line_ids.balance > 0.0 or wizard.move_line_ids.amount_currency > 0.0
|
||||||
|
if is_debit_line and wizard.edit_mode_amount_currency < 0.0:
|
||||||
|
raise UserError(_('The amount of the write-off of a single debit line should be strictly positive.'))
|
||||||
|
elif not is_debit_line and wizard.edit_mode_amount_currency > 0.0:
|
||||||
|
raise UserError(_('The amount of the write-off of a single credit line should be strictly negative.'))
|
||||||
|
|
||||||
|
# ==== 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), self.journal_id)
|
||||||
|
if lock_dates:
|
||||||
|
return lock_dates[-1][0] + timedelta(days=1)
|
||||||
|
|
||||||
|
def _compute_write_off_taxes_data(self, partner):
|
||||||
|
""" 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 ],
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
AccountTax = self.env['account.tax']
|
||||||
|
amount_currency = self.edit_mode_amount_currency or self.amount_currency
|
||||||
|
amount = self.edit_mode_amount or self.amount
|
||||||
|
rate = abs(amount_currency / amount)
|
||||||
|
tax_type = self.tax_id.type_tax_use if self.tax_id else None
|
||||||
|
is_refund = (tax_type == 'sale' and amount_currency > 0.0) or (tax_type == 'purchase' and amount_currency < 0.0)
|
||||||
|
base_line = AccountTax._prepare_base_line_for_taxes_computation(
|
||||||
|
self,
|
||||||
|
partner_id=partner,
|
||||||
|
currency_id=self.reco_currency_id,
|
||||||
|
tax_ids=self.tax_id,
|
||||||
|
price_unit=amount_currency,
|
||||||
|
quantity=1.0,
|
||||||
|
account_id=self.account_id,
|
||||||
|
is_refund=is_refund,
|
||||||
|
rate=rate,
|
||||||
|
special_mode='total_included',
|
||||||
|
)
|
||||||
|
base_lines = [base_line]
|
||||||
|
AccountTax._add_tax_details_in_base_lines(base_lines, self.company_id)
|
||||||
|
AccountTax._round_base_lines_tax_details(base_lines, self.company_id)
|
||||||
|
AccountTax._add_accounting_data_in_base_lines_tax_details(base_lines, self.company_id, include_caba_tags=True)
|
||||||
|
tax_results = AccountTax._prepare_tax_lines(base_lines, self.company_id)
|
||||||
|
_base_line, 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['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_amount_currency = base_to_update['amount_currency']
|
||||||
|
base_amount = 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):
|
||||||
|
if not partner:
|
||||||
|
partner = self.env['res.partner']
|
||||||
|
to_partner = self.to_partner_id if self.is_rec_pay_account else partner
|
||||||
|
tax_data = self._compute_write_off_taxes_data(to_partner) if self.tax_id else None
|
||||||
|
amount_currency = self.edit_mode_amount_currency or self.amount_currency
|
||||||
|
amount = self.edit_mode_amount or self.amount
|
||||||
|
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': -amount_currency,
|
||||||
|
'balance': -amount,
|
||||||
|
}),
|
||||||
|
Command.create({
|
||||||
|
'name': self.label,
|
||||||
|
'account_id': self.account_id.id,
|
||||||
|
'partner_id': to_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': amount_currency if not tax_data else tax_data['base_amount_currency'],
|
||||||
|
'balance': 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': to_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,
|
||||||
|
'checked': not 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 = self.edit_mode or (self.is_write_off_required and not self.allow_partials)
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user