[REF] account: journal items reconciliation (part 1/2)
This commits refactors the code of the reconciliation, in order to facilitate the process of complex use cases, namely multi-currencies or cash-basis-taxes related (see details below). It also prepares the code for a second refactoring where we will save on each journal items the amount_currency and currency_id field (even in case of operation made in company currency), also in the sake of simplification. 1) Multi-currencies: - the account.partial.reconcile model now will have dedicated columns to specify the amount of the partial reconciliation in the debit_line_id currency and the credit_line_id currency. That comes in handy when dealing with journal items having different secondary currencies, but also allows some simplification. - residual_amount_currency computation changed accordingly - moved models account.full.reconcile and account.partial.reconcile in their dedicated .py file 2) Cash basis taxes - cash basis entries now handle correctly rounding errors to make sure the exact amount of gets reported when the reconciliation becomes full. - the account for the base amount of cash basis entries has to be set, now, on the company instead of on each cash basis tax. - that new 'property' field can be set at the CoA installation via the field property_cash_basis_base_account_id of account.chart.template, or going through the accounting settings. Was task task: 2243420 Was PR #50308 Related: odoo/upgrade#1121 Related: odoo/enterprise#10252 Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
This commit is contained in:
@@ -12,6 +12,8 @@ from . import account_reconcile_model
|
||||
from . import account_payment_term
|
||||
from . import account_move
|
||||
from . import account_analytic_default
|
||||
from . import account_partial_reconcile
|
||||
from . import account_full_reconcile
|
||||
from . import account_payment
|
||||
from . import account_bank_statement
|
||||
from . import chart_template
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class AccountFullReconcile(models.Model):
|
||||
_name = "account.full.reconcile"
|
||||
_description = "Full Reconcile"
|
||||
|
||||
name = fields.Char(string='Number', required=True, copy=False, default=lambda self: self.env['ir.sequence'].next_by_code('account.reconcile'))
|
||||
partial_reconcile_ids = fields.One2many('account.partial.reconcile', 'full_reconcile_id', string='Reconciliation Parts')
|
||||
reconciled_line_ids = fields.One2many('account.move.line', 'full_reconcile_id', string='Matched Journal Items')
|
||||
exchange_move_id = fields.Many2one('account.move')
|
||||
|
||||
def unlink(self):
|
||||
""" When removing a full reconciliation, we need to revert the eventual journal entries we created to book the
|
||||
fluctuation of the foreign currency's exchange rate.
|
||||
We need also to reconcile together the origin currency difference line and its reversal in order to completely
|
||||
cancel the currency difference entry on the partner account (otherwise it will still appear on the aged balance
|
||||
for example).
|
||||
"""
|
||||
# Avoid cyclic unlink calls when removing partials.
|
||||
if not self:
|
||||
return True
|
||||
|
||||
moves_to_reverse = self.exchange_move_id
|
||||
|
||||
res = super().unlink()
|
||||
|
||||
# Reverse all exchange moves at once.
|
||||
today = fields.Date.today()
|
||||
default_values_list = [{
|
||||
'date': today,
|
||||
'ref': _('Reversal of: %s') % move.name,
|
||||
} for move in moves_to_reverse]
|
||||
moves_to_reverse._reverse_moves(default_values_list, cancel=True)
|
||||
|
||||
return res
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,612 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
from datetime import date
|
||||
|
||||
|
||||
class AccountPartialReconcile(models.Model):
|
||||
_name = "account.partial.reconcile"
|
||||
_description = "Partial Reconcile"
|
||||
|
||||
# ==== Reconciliation fields ====
|
||||
debit_move_id = fields.Many2one(
|
||||
comodel_name='account.move.line',
|
||||
index=True, required=True)
|
||||
credit_move_id = fields.Many2one(
|
||||
comodel_name='account.move.line',
|
||||
index=True, required=True)
|
||||
full_reconcile_id = fields.Many2one(
|
||||
comodel_name='account.full.reconcile',
|
||||
string="Full Reconcile", copy=False)
|
||||
|
||||
# ==== Currency fields ====
|
||||
company_currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
string="Company Currency",
|
||||
related='company_id.currency_id',
|
||||
help="Utility field to express amount currency")
|
||||
debit_currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
store=True,
|
||||
compute='_compute_debit_currency_id',
|
||||
string="Currency of the debit journal item.")
|
||||
credit_currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
store=True,
|
||||
compute='_compute_credit_currency_id',
|
||||
string="Currency of the credit journal item.")
|
||||
|
||||
# ==== Amount fields ====
|
||||
amount = fields.Monetary(
|
||||
currency_field='company_currency_id',
|
||||
help="Always positive amount concerned by this matching expressed in the company currency.")
|
||||
debit_amount_currency = fields.Monetary(
|
||||
currency_field='debit_currency_id',
|
||||
help="Always positive amount concerned by this matching expressed in the debit line foreign currency.")
|
||||
credit_amount_currency = fields.Monetary(
|
||||
currency_field='credit_currency_id',
|
||||
help="Always positive amount concerned by this matching expressed in the credit line foreign currency.")
|
||||
|
||||
# ==== Other fields ====
|
||||
company_id = fields.Many2one(
|
||||
comodel_name='res.company',
|
||||
string="Company", store=True, readonly=False,
|
||||
related='debit_move_id.company_id')
|
||||
max_date = fields.Date(
|
||||
string="Max Date of Matched Lines", store=True,
|
||||
compute='_compute_max_date',
|
||||
help="Technical field used to determine at which date this reconciliation needs to be shown on the "
|
||||
"aged receivable/payable reports.")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CONSTRAINT METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.constrains('debit_currency_id', 'credit_currency_id')
|
||||
def _check_required_computed_currencies(self):
|
||||
bad_partials = self.filtered(lambda partial: not partial.debit_currency_id or not partial.credit_currency_id)
|
||||
if bad_partials:
|
||||
raise ValidationError(_("Missing foreign currencies on partials having ids: %s") % bad_partials.ids)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# COMPUTE METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('debit_move_id.date', 'credit_move_id.date')
|
||||
def _compute_max_date(self):
|
||||
for partial in self:
|
||||
partial.max_date = max(
|
||||
partial.debit_move_id.date,
|
||||
partial.credit_move_id.date
|
||||
)
|
||||
|
||||
@api.depends('debit_move_id')
|
||||
def _compute_debit_currency_id(self):
|
||||
for partial in self:
|
||||
partial.debit_currency_id = partial.debit_move_id.currency_id \
|
||||
or partial.debit_move_id.company_currency_id
|
||||
|
||||
@api.depends('credit_move_id')
|
||||
def _compute_credit_currency_id(self):
|
||||
for partial in self:
|
||||
partial.credit_currency_id = partial.credit_move_id.currency_id \
|
||||
or partial.credit_move_id.company_currency_id
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# LOW-LEVEL METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def unlink(self):
|
||||
# OVERRIDE to unlink full reconcile linked to the current partials
|
||||
# and reverse the tax cash basis journal entries.
|
||||
|
||||
# Avoid cyclic unlink calls when removing the partials that could remove some full reconcile
|
||||
# and then, loop again and again.
|
||||
if not self:
|
||||
return True
|
||||
|
||||
# Reverse all exchange moves at once.
|
||||
moves_to_reverse = self.env['account.move'].search([('tax_cash_basis_rec_id', 'in', self.ids)])
|
||||
today = fields.Date.today()
|
||||
default_values_list = [{
|
||||
'date': move.date if move.date > (move.company_id.period_lock_date or date.min) else today,
|
||||
'ref': _('Reversal of: %s') % move.name,
|
||||
} for move in moves_to_reverse]
|
||||
moves_to_reverse._reverse_moves(default_values_list, cancel=True)
|
||||
|
||||
# Unlink partials then the full in this order to avoid a recursive call to the same partials.
|
||||
full_to_unlink = self.full_reconcile_id
|
||||
res = super().unlink()
|
||||
full_to_unlink.unlink()
|
||||
return res
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# RECONCILIATION METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _collect_tax_cash_basis_values(self):
|
||||
''' Collect all information needed to create the tax cash basis journal entries on the current partials.
|
||||
:return: A dictionary mapping each move_id to the result of 'account_move._collect_tax_cash_basis_values'.
|
||||
Also, add the 'partials' keys being a list of dictionary, one for each partial to process:
|
||||
* partial: The account.partial.reconcile record.
|
||||
* percentage: The reconciled percentage represented by the partial.
|
||||
* payment_rate: The applied rate of this partial.
|
||||
'''
|
||||
tax_cash_basis_values_per_move = {}
|
||||
|
||||
if not self:
|
||||
return {}
|
||||
|
||||
for partial in self:
|
||||
for move in {partial.debit_move_id.move_id, partial.credit_move_id.move_id}:
|
||||
|
||||
# Collect data about cash basis.
|
||||
if move.id not in tax_cash_basis_values_per_move:
|
||||
tax_cash_basis_values_per_move[move.id] = move._collect_tax_cash_basis_values()
|
||||
|
||||
# Nothing to process on the move.
|
||||
if not tax_cash_basis_values_per_move.get(move.id):
|
||||
continue
|
||||
move_values = tax_cash_basis_values_per_move[move.id]
|
||||
|
||||
# Check the cash basis configuration only when at least one cash basis tax entry need to be created.
|
||||
journal = partial.company_id.tax_cash_basis_journal_id
|
||||
|
||||
if not journal:
|
||||
raise UserError(_("There is no tax cash basis journal defined for the '%s' company.\n"
|
||||
"Configure it in Accounting/Configuration/Settings") % partial.company_id.display_name)
|
||||
|
||||
partial_amount = 0.0
|
||||
partial_amount_currency = 0.0
|
||||
rate_amount = 0.0
|
||||
rate_amount_currency = 0.0
|
||||
if partial.debit_move_id.move_id == move:
|
||||
partial_amount += partial.amount
|
||||
partial_amount_currency += partial.debit_amount_currency
|
||||
rate_amount -= partial.credit_move_id.balance
|
||||
rate_amount_currency -= partial.credit_move_id.amount_currency
|
||||
if partial.credit_move_id.move_id == move:
|
||||
partial_amount += partial.amount
|
||||
partial_amount_currency += partial.credit_amount_currency
|
||||
rate_amount += partial.debit_move_id.balance
|
||||
rate_amount_currency += partial.debit_move_id.amount_currency
|
||||
|
||||
if move_values['currency'] == move.company_id.currency_id:
|
||||
# Percentage made on company's currency.
|
||||
percentage = partial_amount / move_values['total_balance']
|
||||
else:
|
||||
# Percentage made on foreign currency.
|
||||
percentage = partial_amount_currency / move_values['total_amount_currency']
|
||||
|
||||
if rate_amount:
|
||||
payment_rate = rate_amount_currency / rate_amount
|
||||
else:
|
||||
payment_rate = 0.0
|
||||
|
||||
partial_vals = {
|
||||
'partial': partial,
|
||||
'percentage': percentage,
|
||||
'payment_rate': payment_rate,
|
||||
}
|
||||
|
||||
# Add partials.
|
||||
move_values.setdefault('partials', [])
|
||||
move_values['partials'].append(partial_vals)
|
||||
|
||||
# Clean-up moves having nothing to process.
|
||||
return {k: v for k, v in tax_cash_basis_values_per_move.items() if v}
|
||||
|
||||
@api.model
|
||||
def _prepare_cash_basis_base_line_vals(self, base_line, balance, amount_currency):
|
||||
''' Prepare the values to be used to create the cash basis journal items for the tax base line
|
||||
passed as parameter.
|
||||
|
||||
:param base_line: An account.move.line being the base of some taxes.
|
||||
:param balance: The balance to consider for this line.
|
||||
:param amount_currency: The balance in foreign currency to consider for this line.
|
||||
:return: A python dictionary that could be passed to the create method of
|
||||
account.move.line.
|
||||
'''
|
||||
account = base_line.company_id.account_cash_basis_base_account_id or base_line.account_id
|
||||
return {
|
||||
'name': base_line.move_id.name,
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
'amount_currency': amount_currency,
|
||||
'currency_id': base_line.currency_id.id,
|
||||
'partner_id': base_line.partner_id.id,
|
||||
'account_id': account.id,
|
||||
'tax_ids': [(6, 0, base_line.tax_ids.ids)],
|
||||
'tax_tag_ids': [(6, 0, base_line.tax_tag_ids.ids)],
|
||||
'tax_exigible': True,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _prepare_cash_basis_counterpart_base_line_vals(self, cb_base_line_vals):
|
||||
''' Prepare the move line used as a counterpart of the line created by
|
||||
_prepare_cash_basis_base_line_vals.
|
||||
|
||||
:param cb_base_line_vals: The line returned by _prepare_cash_basis_base_line_vals.
|
||||
:return: A python dictionary that could be passed to the create method of
|
||||
account.move.line.
|
||||
'''
|
||||
return {
|
||||
'name': cb_base_line_vals['name'],
|
||||
'debit': cb_base_line_vals['credit'],
|
||||
'credit': cb_base_line_vals['debit'],
|
||||
'account_id': cb_base_line_vals['account_id'],
|
||||
'amount_currency': -cb_base_line_vals['amount_currency'],
|
||||
'currency_id': cb_base_line_vals['currency_id'],
|
||||
'partner_id': cb_base_line_vals['partner_id'],
|
||||
'tax_exigible': True,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _prepare_cash_basis_tax_line_vals(self, tax_line, balance, amount_currency):
|
||||
''' Prepare the move line corresponding to a tax in the cash basis entry.
|
||||
|
||||
:param tax_line: An account.move.line record being a tax line.
|
||||
:param balance: The balance to consider for this line.
|
||||
:param amount_currency: The balance in foreign currency to consider for this line.
|
||||
:return: A python dictionary that could be passed to the create method of
|
||||
account.move.line.
|
||||
'''
|
||||
return {
|
||||
'name': tax_line.name,
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
'tax_base_amount': tax_line.tax_base_amount,
|
||||
'tax_repartition_line_id': tax_line.tax_repartition_line_id.id,
|
||||
'tax_ids': [(6, 0, tax_line.tax_ids.ids)],
|
||||
'tax_tag_ids': [(6, 0, tax_line.tax_tag_ids.ids)],
|
||||
'account_id': tax_line.tax_repartition_line_id.account_id.id or tax_line.account_id.id,
|
||||
'amount_currency': amount_currency,
|
||||
'currency_id': tax_line.currency_id.id,
|
||||
'partner_id': tax_line.partner_id.id,
|
||||
'tax_exigible': True,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _prepare_cash_basis_counterpart_tax_line_vals(self, tax_line, cb_tax_line_vals):
|
||||
''' Prepare the move line used as a counterpart of the line created by
|
||||
_prepare_cash_basis_tax_line_vals.
|
||||
|
||||
:param tax_line: An account.move.line record being a tax line.
|
||||
:param cb_tax_line_vals: The result of _prepare_cash_basis_counterpart_tax_line_vals.
|
||||
:return: A python dictionary that could be passed to the create method of
|
||||
account.move.line.
|
||||
'''
|
||||
return {
|
||||
'name': cb_tax_line_vals['name'],
|
||||
'debit': cb_tax_line_vals['credit'],
|
||||
'credit': cb_tax_line_vals['debit'],
|
||||
'account_id': tax_line.account_id.id,
|
||||
'amount_currency': -cb_tax_line_vals['amount_currency'],
|
||||
'currency_id': cb_tax_line_vals['currency_id'],
|
||||
'partner_id': cb_tax_line_vals['partner_id'],
|
||||
'tax_exigible': True,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_cash_basis_base_line_grouping_key_from_vals(self, base_line_vals):
|
||||
''' Get the grouping key of a cash basis base line that hasn't yet been created.
|
||||
:param base_line_vals: The values to create a new account.move.line record.
|
||||
:return: The grouping key as a tuple.
|
||||
'''
|
||||
return (
|
||||
base_line_vals['currency_id'],
|
||||
base_line_vals['partner_id'],
|
||||
base_line_vals['account_id'],
|
||||
tuple(base_line_vals['tax_ids'][0][2]), # Decode [(6, 0, [...])] command
|
||||
tuple(base_line_vals['tax_tag_ids'][0][2]), # Decode [(6, 0, [...])] command
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_cash_basis_base_line_grouping_key_from_record(self, base_line, account=None):
|
||||
''' Get the grouping key of a journal item being a base line.
|
||||
:param base_line: An account.move.line record.
|
||||
:param account: Optional account to shadow the current base_line one.
|
||||
:return: The grouping key as a tuple.
|
||||
'''
|
||||
return (
|
||||
base_line.currency_id.id,
|
||||
base_line.partner_id.id,
|
||||
(account or base_line.account_id).id,
|
||||
tuple(base_line.tax_ids.ids),
|
||||
tuple(base_line.tax_tag_ids.ids),
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_cash_basis_tax_line_grouping_key_from_vals(self, tax_line_vals):
|
||||
''' Get the grouping key of a cash basis tax line that hasn't yet been created.
|
||||
:param tax_line_vals: The values to create a new account.move.line record.
|
||||
:return: The grouping key as a tuple.
|
||||
'''
|
||||
return (
|
||||
tax_line_vals['currency_id'],
|
||||
tax_line_vals['partner_id'],
|
||||
tax_line_vals['account_id'],
|
||||
tuple(tax_line_vals['tax_ids'][0][2]), # Decode [(6, 0, [...])] command
|
||||
tuple(tax_line_vals['tax_tag_ids'][0][2]), # Decode [(6, 0, [...])] command
|
||||
tax_line_vals['tax_repartition_line_id'],
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_cash_basis_tax_line_grouping_key_from_record(self, tax_line, account=None):
|
||||
''' Get the grouping key of a journal item being a tax line.
|
||||
:param tax_line: An account.move.line record.
|
||||
:param account: Optional account to shadow the current tax_line one.
|
||||
:return: The grouping key as a tuple.
|
||||
'''
|
||||
return (
|
||||
tax_line.currency_id.id,
|
||||
tax_line.partner_id.id,
|
||||
(account or tax_line.account_id).id,
|
||||
tuple(tax_line.tax_ids.ids),
|
||||
tuple(tax_line.tax_tag_ids.ids),
|
||||
tax_line.tax_repartition_line_id.id,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _fix_cash_basis_full_balance_coverage(self, move_values, partial_values, pending_cash_basis_lines, partial_lines_to_create):
|
||||
''' This method is used to ensure the full coverage of the current move when it becomes fully paid.
|
||||
For example, suppose a line of 0.03 paid 50-50. Without this method, each cash basis entry will report
|
||||
0.03 / 0.5 = 0.015 ~ 0.02 per cash entry on the tax report as base amount, for a total of 0.04.
|
||||
This is wrong because we expect 0.03.on the tax report as base amount. This is wrong because we expect 0.03.
|
||||
|
||||
:param move_values: The collected values about cash basis for the current move.
|
||||
:param partial_values: The collected values about cash basis for the current partial.
|
||||
:param pending_cash_basis_lines: The previously generated lines during this reconciliation but not yet created.
|
||||
:param partial_lines_to_create: The generated lines for the current and last partial making the move fully paid.
|
||||
'''
|
||||
residual_amount_per_group = {}
|
||||
move = move_values['move']
|
||||
|
||||
# ==========================================================================
|
||||
# Part 1:
|
||||
# Add the balance of all journal items that are not tax exigible in order to
|
||||
# ensure the exact balance will be report on the Tax Report.
|
||||
# This part is needed when the move will be fully paid after the current
|
||||
# reconciliation.
|
||||
# ==========================================================================
|
||||
|
||||
balance_field = 'balance' if move_values['currency'] == move.company_id.currency_id else 'amount_currency'
|
||||
|
||||
for line in move_values['to_process_lines']:
|
||||
if line.tax_repartition_line_id:
|
||||
# Tax line.
|
||||
grouping_key = self._get_cash_basis_tax_line_grouping_key_from_record(
|
||||
line,
|
||||
account=line.tax_repartition_line_id.account_id,
|
||||
)
|
||||
residual_amount_per_group.setdefault(grouping_key, 0.0)
|
||||
residual_amount_per_group[grouping_key] += line[balance_field]
|
||||
|
||||
elif line.tax_ids:
|
||||
# Base line.
|
||||
grouping_key = self._get_cash_basis_base_line_grouping_key_from_record(
|
||||
line,
|
||||
account=line.company_id.account_cash_basis_base_account_id,
|
||||
)
|
||||
residual_amount_per_group.setdefault(grouping_key, 0.0)
|
||||
residual_amount_per_group[grouping_key] += line[balance_field]
|
||||
|
||||
# ==========================================================================
|
||||
# Part 2:
|
||||
# Subtract all previously created cash basis journal items during previous
|
||||
# reconciliation.
|
||||
# ==========================================================================
|
||||
|
||||
previous_tax_cash_basis_moves = self.env['account.move'].search([
|
||||
'|',
|
||||
('tax_cash_basis_rec_id', 'in', self.ids),
|
||||
('tax_cash_basis_move_id', '=', move.id),
|
||||
])
|
||||
for line in previous_tax_cash_basis_moves.line_ids:
|
||||
if line.tax_repartition_line_id:
|
||||
# Tax line.
|
||||
grouping_key = self._get_cash_basis_tax_line_grouping_key_from_record(line)
|
||||
elif line.tax_ids:
|
||||
# Base line.
|
||||
grouping_key = self._get_cash_basis_base_line_grouping_key_from_record(line)
|
||||
else:
|
||||
continue
|
||||
|
||||
if grouping_key not in residual_amount_per_group:
|
||||
# The grouping_key is unknown regarding the current lines.
|
||||
# Maybe this move has been created before migration and then,
|
||||
# we are not able to ensure the full coverage of the balance.
|
||||
return
|
||||
|
||||
residual_amount_per_group[grouping_key] -= line[balance_field]
|
||||
|
||||
# ==========================================================================
|
||||
# Part 3:
|
||||
# Subtract all pending cash basis journal items that will be created during
|
||||
# this reconciliation.
|
||||
# ==========================================================================
|
||||
|
||||
for grouping_key, balance in pending_cash_basis_lines:
|
||||
residual_amount_per_group[grouping_key] -= balance
|
||||
|
||||
# ==========================================================================
|
||||
# Part 4:
|
||||
# Fix the current cash basis journal items in progress by replacing the
|
||||
# balance by the residual one.
|
||||
# ==========================================================================
|
||||
|
||||
for grouping_key, aggregated_vals in partial_lines_to_create.items():
|
||||
line_vals = aggregated_vals['vals']
|
||||
|
||||
if move_values['currency'] == move.company_id.currency_id:
|
||||
balance = residual_amount_per_group[grouping_key]
|
||||
line_vals.update({
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
})
|
||||
else:
|
||||
amount_currency = residual_amount_per_group[grouping_key]
|
||||
balance = partial_values['payment_rate'] and amount_currency / partial_values['payment_rate'] or 0.0
|
||||
line_vals.update({
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
'amount_currency': amount_currency,
|
||||
})
|
||||
|
||||
def _create_tax_cash_basis_moves(self):
|
||||
''' Create the tax cash basis journal entries.
|
||||
:return: The newly created journal entries.
|
||||
'''
|
||||
tax_cash_basis_values_per_move = self._collect_tax_cash_basis_values()
|
||||
|
||||
moves_to_create = []
|
||||
to_reconcile_after = []
|
||||
for move_values in tax_cash_basis_values_per_move.values():
|
||||
move = move_values['move']
|
||||
pending_cash_basis_lines = []
|
||||
|
||||
for i, partial_values in enumerate(move_values['partials']):
|
||||
partial = partial_values['partial']
|
||||
is_last_partial = i == len(move_values['partials']) - 1
|
||||
|
||||
# Init the journal entry.
|
||||
move_vals = {
|
||||
'move_type': 'entry',
|
||||
'date': partial.max_date,
|
||||
'ref': move.name,
|
||||
'journal_id': partial.company_id.tax_cash_basis_journal_id.id,
|
||||
'line_ids': [],
|
||||
'tax_cash_basis_rec_id': partial.id,
|
||||
'tax_cash_basis_move_id': move.id,
|
||||
}
|
||||
|
||||
# Tracking of lines grouped all together.
|
||||
# Used to reduce the number of generated lines and to avoid rounding issues.
|
||||
partial_lines_to_create = {}
|
||||
|
||||
for line in move_values['to_process_lines']:
|
||||
|
||||
# ==========================================================================
|
||||
# Compute the balance of the current line on the cash basis entry.
|
||||
# This balance is a percentage representing the part of the journal entry
|
||||
# that is actually paid by the current partial.
|
||||
# ==========================================================================
|
||||
|
||||
if move_values['currency'] == move.company_id.currency_id:
|
||||
# Percentage expressed in the company's currency.
|
||||
balance = line.company_currency_id.round(line.balance * partial_values['percentage'])
|
||||
amount_currency = 0.0
|
||||
else:
|
||||
# Percentage expressed in the foreign currency.
|
||||
amount_currency = line.currency_id.round(line.amount_currency * partial_values['percentage'])
|
||||
balance = partial_values['payment_rate'] and amount_currency / partial_values['payment_rate'] or 0.0
|
||||
|
||||
# ==========================================================================
|
||||
# Prepare the mirror cash basis journal item of the current line.
|
||||
# Group them all together as much as possible to reduce the number of
|
||||
# generated journal items.
|
||||
# Also track the computed balance in order to avoid rounding issues when
|
||||
# the journal entry will be fully paid. At that case, we expect the exact
|
||||
# amount of each line has been covered by the cash basis journal entries
|
||||
# and well reported in the Tax Report.
|
||||
# ==========================================================================
|
||||
|
||||
if line.tax_repartition_line_id:
|
||||
# Tax line.
|
||||
|
||||
cb_tax_line_vals = self._prepare_cash_basis_tax_line_vals(line, balance, amount_currency)
|
||||
grouping_key = self._get_cash_basis_tax_line_grouping_key_from_vals(cb_tax_line_vals)
|
||||
partial_lines_to_create[grouping_key] = {
|
||||
'tax_line': line,
|
||||
'vals': cb_tax_line_vals,
|
||||
}
|
||||
|
||||
elif line.tax_ids:
|
||||
# Base line.
|
||||
|
||||
cb_base_line_vals = self._prepare_cash_basis_base_line_vals(line, balance, amount_currency)
|
||||
grouping_key = self._get_cash_basis_base_line_grouping_key_from_vals(cb_base_line_vals)
|
||||
|
||||
if grouping_key in partial_lines_to_create:
|
||||
aggregated_vals = partial_lines_to_create[grouping_key]['vals']
|
||||
balance = aggregated_vals['debit'] - aggregated_vals['credit']
|
||||
balance += cb_base_line_vals['debit'] - cb_base_line_vals['credit']
|
||||
|
||||
aggregated_vals.update({
|
||||
'debit': balance if balance > 0.0 else 0.0,
|
||||
'credit': -balance if balance < 0.0 else 0.0,
|
||||
})
|
||||
aggregated_vals['amount_currency'] += cb_base_line_vals['amount_currency']
|
||||
else:
|
||||
partial_lines_to_create[grouping_key] = {
|
||||
'vals': cb_base_line_vals,
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# Ensure the full coverage by replacing the balance of the journal items
|
||||
# created by the last partial.
|
||||
# ==========================================================================
|
||||
|
||||
if move_values['is_fully_paid'] and is_last_partial:
|
||||
self._fix_cash_basis_full_balance_coverage(
|
||||
move_values,
|
||||
partial_values,
|
||||
pending_cash_basis_lines,
|
||||
partial_lines_to_create,
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Create the counterpart journal items.
|
||||
# ==========================================================================
|
||||
|
||||
# To be able to retrieve the correct matching between the tax lines to reconcile
|
||||
# later, the lines will be created using a specific sequence.
|
||||
sequence = 0
|
||||
|
||||
for grouping_key, aggregated_vals in partial_lines_to_create.items():
|
||||
line_vals = aggregated_vals['vals']
|
||||
line_vals['sequence'] = sequence
|
||||
|
||||
if move_values['currency'] == move.company_id.currency_id:
|
||||
pending_cash_basis_lines.append((grouping_key, line_vals['debit'] - line_vals['credit']))
|
||||
else:
|
||||
pending_cash_basis_lines.append((grouping_key, line_vals['amount_currency']))
|
||||
|
||||
if 'tax_repartition_line_id' in line_vals:
|
||||
# Tax line.
|
||||
|
||||
tax_line = aggregated_vals['tax_line']
|
||||
counterpart_line_vals = self._prepare_cash_basis_counterpart_tax_line_vals(tax_line, line_vals)
|
||||
counterpart_line_vals['sequence'] = sequence + 1
|
||||
|
||||
if tax_line.account_id.reconcile:
|
||||
move_index = len(moves_to_create)
|
||||
to_reconcile_after.append((tax_line, move_index, counterpart_line_vals['sequence']))
|
||||
|
||||
else:
|
||||
# Base line.
|
||||
|
||||
counterpart_line_vals = self._prepare_cash_basis_counterpart_base_line_vals(line_vals)
|
||||
counterpart_line_vals['sequence'] = sequence + 1
|
||||
|
||||
sequence += 2
|
||||
|
||||
move_vals['line_ids'] += [(0, 0, counterpart_line_vals), (0, 0, line_vals)]
|
||||
|
||||
moves_to_create.append(move_vals)
|
||||
|
||||
moves = self.env['account.move'].create(moves_to_create)
|
||||
moves.post()
|
||||
|
||||
# Reconcile the tax lines being on a reconcile tax basis transfer account.
|
||||
for line, move_index, sequence in to_reconcile_after:
|
||||
counterpart_line = moves[move_index].line_ids.filtered(lambda line: line.sequence == sequence)
|
||||
|
||||
# When dealing with tiny amounts, the line could have a zero amount and then, be already reconciled.
|
||||
if counterpart_line.reconciled:
|
||||
continue
|
||||
|
||||
(line + counterpart_line).reconcile()
|
||||
|
||||
return moves
|
||||
@@ -88,12 +88,6 @@ class AccountTax(models.Model):
|
||||
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
|
||||
comodel_name='account.account',
|
||||
help="Account used to transition the tax amount for cash basis taxes. It will contain the tax amount as long as the original invoice has not been reconciled ; at reconciliation, this amount cancelled on this account and put on the regular tax account.")
|
||||
cash_basis_base_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
check_company=True,
|
||||
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
|
||||
string='Base Tax Received Account',
|
||||
help='Account that will be set on lines created in cash basis journal entry and used to keep track of the tax base amount.')
|
||||
invoice_repartition_line_ids = fields.One2many(string="Repartition for Invoices", comodel_name="account.tax.repartition.line", inverse_name="invoice_tax_id", copy=True, help="Repartition when the tax is used on an invoice")
|
||||
refund_repartition_line_ids = fields.One2many(string="Repartition for Refund Invoices", comodel_name="account.tax.repartition.line", inverse_name="refund_tax_id", copy=True, help="Repartition when the tax is used on a refund")
|
||||
country_id = fields.Many2one(string='Country', comodel_name='res.country', related='company_id.country_id', help="Technical field used to restrict the domain of account tags for tax repartition lines created for this tax.")
|
||||
|
||||
@@ -121,6 +121,12 @@ class AccountChartTemplate(models.Model):
|
||||
property_tax_payable_account_id = fields.Many2one('account.account.template', string="Tax current account (payable)")
|
||||
property_tax_receivable_account_id = fields.Many2one('account.account.template', string="Tax current account (receivable)")
|
||||
property_advance_tax_payment_account_id = fields.Many2one('account.account.template', string="Advance tax payment account")
|
||||
property_cash_basis_base_account_id = fields.Many2one(
|
||||
comodel_name='account.account.template',
|
||||
domain=[('deprecated', '=', False)],
|
||||
string="Base Tax Received Account",
|
||||
help="Account that will be set on lines created in cash basis journal entry and used to keep track of the "
|
||||
"tax base amount.")
|
||||
|
||||
@api.model
|
||||
def _prepare_transfer_account_template(self, prefix=None):
|
||||
@@ -249,6 +255,7 @@ class AccountChartTemplate(models.Model):
|
||||
'default_cash_difference_income_account_id': acc_template_ref.get(self.default_cash_difference_income_account_id.id, False),
|
||||
'default_cash_difference_expense_account_id': acc_template_ref.get(self.default_cash_difference_expense_account_id.id, False),
|
||||
'account_journal_suspense_account_id': acc_template_ref.get(self.account_journal_suspense_account_id.id),
|
||||
'account_cash_basis_base_account_id': acc_template_ref.get(self.property_cash_basis_base_account_id.id),
|
||||
})
|
||||
|
||||
if not company.account_journal_suspense_account_id:
|
||||
@@ -555,10 +562,9 @@ class AccountChartTemplate(models.Model):
|
||||
|
||||
# writing account values after creation of accounts
|
||||
for key, value in generated_tax_res['account_dict']['account.tax'].items():
|
||||
if value['cash_basis_transition_account_id'] or value['cash_basis_base_account_id']:
|
||||
if value['cash_basis_transition_account_id']:
|
||||
AccountTaxObj.browse(key).write({
|
||||
'cash_basis_transition_account_id': account_ref.get(value['cash_basis_transition_account_id'], False),
|
||||
'cash_basis_base_account_id': account_ref.get(value['cash_basis_base_account_id'], False),
|
||||
})
|
||||
|
||||
AccountTaxRepartitionLineObj = self.env['account.tax.repartition.line']
|
||||
@@ -855,11 +861,6 @@ class AccountTaxTemplate(models.Model):
|
||||
string="Cash Basis Transition Account",
|
||||
domain=[('deprecated', '=', False)],
|
||||
help="Account used to transition the tax amount for cash basis taxes. It will contain the tax amount as long as the original invoice has not been reconciled ; at reconciliation, this amount cancelled on this account and put on the regular tax account.")
|
||||
cash_basis_base_account_id = fields.Many2one(
|
||||
'account.account.template',
|
||||
domain=[('deprecated', '=', False)],
|
||||
string='Base Tax Received Account',
|
||||
help='Account that will be set on lines created in cash basis journal entry and used to keep track of the tax base amount.')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_company_uniq', 'unique(name, type_tax_use, chart_template_id)', 'Tax names must be unique !'),
|
||||
@@ -946,7 +947,6 @@ class AccountTaxTemplate(models.Model):
|
||||
# Since the accounts have not been created yet, we have to wait before filling these fields
|
||||
todo_dict['account.tax'][tax.id] = {
|
||||
'cash_basis_transition_account_id': template.cash_basis_transition_account_id.id,
|
||||
'cash_basis_base_account_id': template.cash_basis_base_account_id.id,
|
||||
}
|
||||
|
||||
# We also have to delay the assignation of accounts to repartition lines
|
||||
|
||||
@@ -55,7 +55,6 @@ class ResCompany(models.Model):
|
||||
transfer_account_code_prefix = fields.Char(string='Prefix of the transfer accounts')
|
||||
account_sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax")
|
||||
account_purchase_tax_id = fields.Many2one('account.tax', string="Default Purchase Tax")
|
||||
tax_cash_basis_journal_id = fields.Many2one('account.journal', string="Cash Basis Journal")
|
||||
tax_calculation_rounding_method = fields.Selection([
|
||||
('round_per_line', 'Round per Line'),
|
||||
('round_globally', 'Round Globally'),
|
||||
@@ -113,6 +112,17 @@ class ResCompany(models.Model):
|
||||
# Technical field to hide country specific fields in company form view
|
||||
country_code = fields.Char(related='country_id.code')
|
||||
|
||||
# Cash basis taxes
|
||||
tax_cash_basis_journal_id = fields.Many2one(
|
||||
comodel_name='account.journal',
|
||||
string="Cash Basis Journal")
|
||||
account_cash_basis_base_account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
domain=[('deprecated', '=', False)],
|
||||
string="Base Tax Received Account",
|
||||
help="Account that will be set on lines created in cash basis journal entry and used to keep track of the "
|
||||
"tax base amount.")
|
||||
|
||||
@api.constrains('account_opening_move_id', 'fiscalyear_last_day', 'fiscalyear_last_month')
|
||||
def _check_fiscalyear_last_day(self):
|
||||
# if the user explicitly chooses the 29th of February we allow it:
|
||||
|
||||
@@ -73,6 +73,12 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_snailmail_account = fields.Boolean(string="Snailmail")
|
||||
tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility', readonly=False)
|
||||
tax_cash_basis_journal_id = fields.Many2one('account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal", readonly=False)
|
||||
account_cash_basis_base_account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
string="Base Tax Received Account",
|
||||
readonly=False,
|
||||
related='company_id.account_cash_basis_base_account_id',
|
||||
domain=[('deprecated', '=', False)])
|
||||
|
||||
qr_code = fields.Boolean(string='Display SEPA QR-code', related='company_id.qr_code', readonly=False)
|
||||
invoice_is_print = fields.Boolean(string='Print', related='company_id.invoice_is_print', readonly=False)
|
||||
|
||||
@@ -59,19 +59,7 @@ var ShowPaymentLineWidget = AbstractField.extend({
|
||||
var content = info.content[v];
|
||||
var options = {
|
||||
content: function () {
|
||||
var $content = $(QWeb.render('PaymentPopOver', {
|
||||
name: content.name,
|
||||
journal_name: content.journal_name,
|
||||
date: content.date,
|
||||
amount: content.amount,
|
||||
currency: content.currency,
|
||||
position: content.position,
|
||||
payment_id: content.payment_id,
|
||||
payment_method_name: content.payment_method_name,
|
||||
move_id: content.move_id,
|
||||
ref: content.ref,
|
||||
account_payment_id: content.account_payment_id,
|
||||
}));
|
||||
var $content = $(QWeb.render('PaymentPopOver', content));
|
||||
var unreconcile_button = $content.filter('.js_unreconcile_payment').on('click', self._onRemoveMoveReconcile.bind(self));
|
||||
|
||||
$content.filter('.js_open_payment').on('click', self._onOpenPayment.bind(self));
|
||||
@@ -145,13 +133,13 @@ var ShowPaymentLineWidget = AbstractField.extend({
|
||||
*/
|
||||
_onRemoveMoveReconcile: function (event) {
|
||||
var self = this;
|
||||
var paymentId = parseInt($(event.target).attr('payment-id'));
|
||||
if (paymentId !== undefined && !isNaN(paymentId)){
|
||||
var moveId = parseInt($(event.target).attr('move-id'));
|
||||
var partialId = parseInt($(event.target).attr('partial-id'));
|
||||
if (partialId !== undefined && !isNaN(partialId)){
|
||||
this._rpc({
|
||||
model: 'account.move.line',
|
||||
method: 'remove_move_reconcile',
|
||||
args: [paymentId],
|
||||
context: {'move_id': this.res_id},
|
||||
model: 'account.move',
|
||||
method: 'js_remove_outstanding_partial',
|
||||
args: [moveId, partialId],
|
||||
}).then(function () {
|
||||
self.trigger_up('reload');
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary js_unreconcile_payment float-left" t-att-payment-id="payment_id" style="margin-top:5px; margin-bottom:5px;" groups="account.group_account_invoice">Unreconcile</button>
|
||||
<button class="btn btn-sm btn-primary js_unreconcile_payment float-left" t-att-partial-id="partial_id" t-att-payment-id="payment_id" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;" groups="account.group_account_invoice">Unreconcile</button>
|
||||
<button class="btn btn-sm btn-secondary js_open_payment float-right" t-att-payment-id="account_payment_id" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;">View</button>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ QUnit.module('account', {
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
payments_widget: '{"content": [{"digits": [69, 2], "currency": "$", "amount": 555.0, "name": "Customer Payment: INV/2017/0004", "date": "2017-04-25", "position": "before", "ref": "BNK1/2017/0003 (INV/2017/0004)", "payment_id": 22, "move_id": 10, "journal_name": "Bank"}], "outstanding": false, "title": "Less Payment"}',
|
||||
payments_widget: '{"content": [{"digits": [69, 2], "currency": "$", "amount": 555.0, "name": "Customer Payment: INV/2017/0004", "date": "2017-04-25", "position": "before", "ref": "BNK1/2017/0003 (INV/2017/0004)", "payment_id": 22, "move_id": 10, "partial_id": 38, "journal_name": "Bank"}], "outstanding": false, "title": "Less Payment"}',
|
||||
outstanding_credits_debits_widget: '{"content": [{"digits": [69, 2], "currency": "$", "amount": 100.0, "journal_name": "INV/2017/0004", "position": "before", "id": 20}], "move_id": 4, "outstanding": true, "title": "Outstanding credits"}',
|
||||
}]
|
||||
},
|
||||
@@ -26,7 +26,7 @@ QUnit.module('account', {
|
||||
QUnit.module('Reconciliation');
|
||||
|
||||
QUnit.test('Reconciliation form field', async function (assert) {
|
||||
assert.expect(6);
|
||||
assert.expect(5);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
@@ -38,9 +38,8 @@ QUnit.module('account', {
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'remove_move_reconcile') {
|
||||
assert.deepEqual(args.args, [22], "should call remove_move_reconcile {warning: required focus}");
|
||||
assert.deepEqual(args.kwargs, {context: {"move_id": 1}}, "should call remove_move_reconcile {warning: required focus}");
|
||||
if (args.method === 'js_remove_outstanding_partial') {
|
||||
assert.deepEqual(args.args, [10, 38], "should call js_remove_outstanding_partial {warning: required focus}");
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (args.method === 'js_assign_outstanding_line') {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_account_move_reconcile
|
||||
from . import test_account_move_payments_widget
|
||||
from . import test_account_move_out_invoice
|
||||
from . import test_account_move_out_refund
|
||||
from . import test_account_move_in_invoice
|
||||
|
||||
@@ -374,6 +374,24 @@ class AccountTestNoChartCommon(SavepointCaseWithUserDemo):
|
||||
'code': 'GJT0',
|
||||
})
|
||||
|
||||
# Exchange journal.
|
||||
cls.exchange_journal0 = cls.env['account.journal'].create({
|
||||
'name': 'Exchange Difference - (test)',
|
||||
'type': 'general',
|
||||
'code': 'EXCH',
|
||||
'default_debit_account_id': cls.env['account.account'].create({
|
||||
'name': 'Foreign Exchange Gain - (test)',
|
||||
'code': 'NC4410',
|
||||
'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
|
||||
}).id,
|
||||
'default_credit_account_id': cls.env['account.account'].create({
|
||||
'name': 'Foreign Exchange Loss - (test)',
|
||||
'code': 'NC6410',
|
||||
'user_type_id': cls.env.ref('account.data_account_type_expenses').id,
|
||||
}).id,
|
||||
})
|
||||
cls.env.user.company_id.currency_exchange_journal_id = cls.exchange_journal0
|
||||
|
||||
@classmethod
|
||||
def setUpAdditionalAccounts(cls):
|
||||
""" Set up some addionnal accounts: expenses, revenue, ... """
|
||||
@@ -504,6 +522,27 @@ class AccountTestNoChartCommonMultiCompany(AccountTestNoChartCommon):
|
||||
'company_id': cls.company_B.id
|
||||
})
|
||||
|
||||
# Exchange journal.
|
||||
cls.exchange_journal0 = cls.env['account.journal'].create({
|
||||
'name': 'Exchange Difference - (test)',
|
||||
'type': 'general',
|
||||
'code': 'EXCH',
|
||||
'company_id': cls.company_B.id,
|
||||
'default_debit_account_id': cls.env['account.account'].create({
|
||||
'name': 'Foreign Exchange Gain - (test)',
|
||||
'code': 'NC4410',
|
||||
'user_type_id': cls.env.ref('account.data_account_type_revenue').id,
|
||||
'company_id': cls.company_B.id
|
||||
}).id,
|
||||
'default_credit_account_id': cls.env['account.account'].create({
|
||||
'name': 'Foreign Exchange Loss - (test)',
|
||||
'code': 'NC6410',
|
||||
'user_type_id': cls.env.ref('account.data_account_type_expenses').id,
|
||||
'company_id': cls.company_B.id
|
||||
}).id,
|
||||
})
|
||||
cls.company_B.currency_exchange_journal_id = cls.exchange_journal0
|
||||
|
||||
@classmethod
|
||||
def setUpAdditionalAccounts(cls):
|
||||
""" Set up some addionnal accounts: expenses, revenue, ... """
|
||||
@@ -1072,6 +1111,8 @@ class TestAccountReconciliationCommon(AccountTestCommon):
|
||||
'user_type_id': cls.env.ref('account.data_account_type_current_assets').id,
|
||||
'company_id': cls.company.id,
|
||||
})
|
||||
cls.company.account_cash_basis_base_account_id = cls.tax_base_amount_account.id
|
||||
|
||||
|
||||
# Journals
|
||||
cls.purchase_journal = cls.env['account.journal'].create({
|
||||
@@ -1100,7 +1141,6 @@ class TestAccountReconciliationCommon(AccountTestCommon):
|
||||
'amount': 20,
|
||||
'tax_exigibility': 'on_payment',
|
||||
'cash_basis_transition_account_id': cls.tax_waiting_account.id,
|
||||
'cash_basis_base_account_id': cls.tax_base_amount_account.id,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
import json
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestAccountMovePaymentsWidget(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.receivable_account = cls.company_data['default_account_receivable']
|
||||
cls.payable_account = cls.company_data['default_account_payable']
|
||||
|
||||
cls.currency_data_2 = cls.setup_multi_currency_data(default_values={
|
||||
'name': 'Stars',
|
||||
'symbol': '☆',
|
||||
'currency_unit_label': 'Stars',
|
||||
'currency_subunit_label': 'Little Stars',
|
||||
}, rate2016=6.0, rate2017=4.0)
|
||||
|
||||
cls.curr_1 = cls.company_data['currency']
|
||||
cls.curr_2 = cls.currency_data['currency']
|
||||
cls.curr_3 = cls.currency_data_2['currency']
|
||||
|
||||
cls.payment_2016_curr_1 = cls.env['account.move'].create({
|
||||
'date': '2016-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': 0.0, 'currency_id': False, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
],
|
||||
})
|
||||
cls.payment_2016_curr_1.post()
|
||||
|
||||
cls.payment_2016_curr_2 = cls.env['account.move'].create({
|
||||
'date': '2016-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -1550.0, 'currency_id': cls.curr_2.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 1550.0, 'currency_id': cls.curr_2.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
],
|
||||
})
|
||||
cls.payment_2016_curr_2.post()
|
||||
|
||||
cls.payment_2017_curr_2 = cls.env['account.move'].create({
|
||||
'date': '2017-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -950.0, 'currency_id': cls.curr_2.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 950.0, 'currency_id': cls.curr_2.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
],
|
||||
})
|
||||
cls.payment_2017_curr_2.post()
|
||||
|
||||
cls.payment_2016_curr_3 = cls.env['account.move'].create({
|
||||
'date': '2016-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -3050.0, 'currency_id': cls.curr_3.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 3050.0, 'currency_id': cls.curr_3.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
],
|
||||
})
|
||||
cls.payment_2016_curr_3.post()
|
||||
|
||||
cls.payment_2017_curr_3 = cls.env['account.move'].create({
|
||||
'date': '2017-01-01',
|
||||
'line_ids': [
|
||||
(0, 0, {'debit': 0.0, 'credit': 500.0, 'amount_currency': -1950.0, 'currency_id': cls.curr_3.id, 'account_id': cls.receivable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
(0, 0, {'debit': 500.0, 'credit': 0.0, 'amount_currency': 1950.0, 'currency_id': cls.curr_3.id, 'account_id': cls.payable_account.id, 'partner_id': cls.partner_a.id}),
|
||||
],
|
||||
})
|
||||
cls.payment_2017_curr_3.post()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HELPERS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _test_all_outstanding_payments(self, invoice, expected_amounts):
|
||||
''' Check the outstanding payments widget before/after the reconciliation.
|
||||
:param invoice: An account.move record.
|
||||
:param expected_amounts: A map <move_id> -> <amount>
|
||||
'''
|
||||
|
||||
# Check suggested outstanding payments.
|
||||
to_reconcile_payments_widget_vals = json.loads(invoice.invoice_outstanding_credits_debits_widget)
|
||||
|
||||
self.assertTrue(to_reconcile_payments_widget_vals)
|
||||
|
||||
current_amounts = {vals['move_id']: vals['amount'] for vals in to_reconcile_payments_widget_vals['content']}
|
||||
self.assertDictEqual(current_amounts, expected_amounts)
|
||||
|
||||
# Reconcile
|
||||
pay_term_lines = invoice.line_ids\
|
||||
.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
|
||||
to_reconcile = self.env['account.move'].browse(list(current_amounts.keys()))\
|
||||
.line_ids\
|
||||
.filtered(lambda line: line.account_id == pay_term_lines.account_id)
|
||||
(pay_term_lines + to_reconcile).reconcile()
|
||||
|
||||
# Check payments after reconciliation.
|
||||
reconciled_payments_widget_vals = json.loads(invoice.invoice_payments_widget)
|
||||
|
||||
self.assertTrue(reconciled_payments_widget_vals)
|
||||
|
||||
current_amounts = {vals['move_id']: vals['amount'] for vals in reconciled_payments_widget_vals['content']}
|
||||
self.assertDictEqual(current_amounts, expected_amounts)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# TESTS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def test_outstanding_payments_single_currency(self):
|
||||
''' Test the outstanding payments widget on invoices having the same currency
|
||||
as the company one.
|
||||
'''
|
||||
|
||||
# Customer invoice of 2500.0 in curr_1.
|
||||
out_invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.curr_1.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 2500.0})],
|
||||
})
|
||||
out_invoice.post()
|
||||
|
||||
# Vendor bill of 2500.0 in curr_1.
|
||||
in_invoice = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.curr_1.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 2500.0})],
|
||||
})
|
||||
in_invoice.post()
|
||||
|
||||
expected_amounts = {
|
||||
self.payment_2016_curr_1.id: 500.0,
|
||||
self.payment_2016_curr_2.id: 500.0,
|
||||
self.payment_2017_curr_2.id: 500.0,
|
||||
self.payment_2016_curr_3.id: 500.0,
|
||||
self.payment_2017_curr_3.id: 500.0,
|
||||
}
|
||||
|
||||
self._test_all_outstanding_payments(out_invoice, expected_amounts)
|
||||
self._test_all_outstanding_payments(in_invoice, expected_amounts)
|
||||
|
||||
def test_outstanding_payments_foreign_currency(self):
|
||||
''' Test the outstanding payments widget on invoices having a foreign currency. '''
|
||||
|
||||
# Customer invoice of 2500.0 in curr_1.
|
||||
out_invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.curr_2.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 7500.0})],
|
||||
})
|
||||
out_invoice.post()
|
||||
|
||||
# Vendor bill of 2500.0 in curr_1.
|
||||
in_invoice = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.curr_2.id,
|
||||
'invoice_line_ids': [(0, 0, {'name': '/', 'price_unit': 7500.0})],
|
||||
})
|
||||
in_invoice.post()
|
||||
|
||||
expected_amounts = {
|
||||
self.payment_2016_curr_1.id: 1500.0,
|
||||
self.payment_2016_curr_2.id: 1550.0,
|
||||
self.payment_2017_curr_2.id: 950.0,
|
||||
self.payment_2016_curr_3.id: 1500.0,
|
||||
self.payment_2017_curr_3.id: 1000.0,
|
||||
}
|
||||
|
||||
self._test_all_outstanding_payments(out_invoice, expected_amounts)
|
||||
self._test_all_outstanding_payments(in_invoice, expected_amounts)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -195,23 +195,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
self.assertEqual(aml.amount_residual, 0, 'The journal item should be totally reconciled')
|
||||
self.assertEqual(aml.amount_residual_currency, 0, 'The journal item should be totally reconciled')
|
||||
|
||||
move_list_vals = [
|
||||
('2', 728.35, 795.05, self.currency_swiss_id),
|
||||
('3', -4.46, 0, False),
|
||||
('4', 0.32, 0, False),
|
||||
('5', 14.72, 16.20, self.currency_swiss_id),
|
||||
('6', -737.10, -811.25, self.currency_swiss_id),
|
||||
]
|
||||
move_ids = []
|
||||
for name, amount, amount_currency, currency_id in move_list_vals:
|
||||
move_ids.append(create_move(name, amount, amount_currency, currency_id))
|
||||
aml_recs = self.env['account.move.line'].search([('move_id', 'in', move_ids), ('account_id', '=', self.account_rcv.id), ('reconciled', '=', False)])
|
||||
aml_recs.reconcile(self.account_rsa, self.bank_journal_usd)
|
||||
for aml in aml_recs:
|
||||
self.assertTrue(aml.reconciled, 'The journal item should be totally reconciled')
|
||||
self.assertEqual(aml.amount_residual, 0, 'The journal item should be totally reconciled')
|
||||
self.assertEqual(aml.amount_residual_currency, 0, 'The journal item should be totally reconciled')
|
||||
|
||||
def test_manual_reconcile_wizard_same_account(self):
|
||||
move_ids = self.env['account.move']
|
||||
debit_line_vals = {
|
||||
@@ -478,7 +461,7 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
self.assertAlmostEqual(inv.amount_residual, 0)
|
||||
|
||||
# Unreconcile invoice and check residual
|
||||
credit_aml.with_context(invoice_id=inv.id).remove_move_reconcile()
|
||||
credit_aml.remove_move_reconcile()
|
||||
self.assertAlmostEqual(inv.amount_residual, 111)
|
||||
|
||||
def test_revert_payment_and_reconcile(self):
|
||||
@@ -705,636 +688,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
|
||||
self.assertTrue(receivable_line.matched_debit_ids)
|
||||
|
||||
def test_reconciliation_cash_basis01(self):
|
||||
# Simulates an expense made up by 2 lines
|
||||
# one is subject to a cash basis tax
|
||||
# the other is not subject to tax
|
||||
|
||||
company = self.company
|
||||
company.tax_cash_basis_journal_id = self.cash_basis_journal
|
||||
|
||||
purchase_move = self.env['account.move'].create({
|
||||
'journal_id': self.purchase_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {'account_id': self.account_rsa.id, 'debit': 0.0, 'credit': 100.0}),
|
||||
(0, 0, {'account_id': self.account_rsa.id, 'debit': 0.0, 'credit': 50.0}),
|
||||
(0, 0, {'account_id': self.expense_account.id, 'debit': 50.0, 'credit': 0.0}),
|
||||
(0, 0, {'account_id': self.expense_account.id, 'debit': 83.33, 'credit': 0.0, 'tax_ids': [(4, self.tax_cash_basis.id)], 'tax_exigible': False}),
|
||||
(0, 0, {
|
||||
'account_id': self.tax_waiting_account.id,
|
||||
'debit': 16.67,
|
||||
'credit': 0.0,
|
||||
'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_base_amount': 83.33,
|
||||
'tax_exigible': False
|
||||
}),
|
||||
],
|
||||
})
|
||||
purchase_move.post()
|
||||
|
||||
payment_move = self.env['account.move'].create({
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'line_ids': [
|
||||
(0, 0, {'account_id': self.account_rsa.id, 'debit': 150.0, 'credit': 0.0}),
|
||||
(0, 0, {'account_id': self.account_euro.id, 'debit': 0.0, 'credit': 150.0}),
|
||||
],
|
||||
})
|
||||
payment_move.post()
|
||||
|
||||
(purchase_move + payment_move).invalidate_cache(['line_ids'])
|
||||
to_reconcile = (purchase_move + payment_move).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
to_reconcile.reconcile()
|
||||
|
||||
cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)])
|
||||
|
||||
self.assertEqual(len(cash_basis_moves), 2)
|
||||
self.assertTrue(cash_basis_moves.exists())
|
||||
|
||||
# check reconciliation in Payable account
|
||||
purchase_line_ids = purchase_move.line_ids.sorted()
|
||||
payment_line_ids = payment_move.line_ids.sorted()
|
||||
self.assertTrue(purchase_line_ids[0].full_reconcile_id.exists())
|
||||
self.assertEqual(purchase_line_ids[0].full_reconcile_id.reconciled_line_ids,
|
||||
purchase_line_ids[0] + purchase_line_ids[1] + payment_line_ids[0])
|
||||
|
||||
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
|
||||
# check reconciliation in the tax waiting account
|
||||
self.assertTrue(purchase_line_ids[4].full_reconcile_id.exists())
|
||||
self.assertEqual(purchase_line_ids[4].full_reconcile_id.reconciled_line_ids,
|
||||
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_line_ids[4])
|
||||
|
||||
self.assertEqual(len(cash_basis_aml_ids), 8)
|
||||
|
||||
# check amounts
|
||||
cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.amount_total == 33.34)
|
||||
cash_basis_move2 = cash_basis_moves.filtered(lambda m: m.amount_total == 66.66)
|
||||
|
||||
self.assertTrue(cash_basis_move1.exists())
|
||||
self.assertTrue(cash_basis_move2.exists())
|
||||
|
||||
# For first move
|
||||
move_lines = cash_basis_move1.line_ids
|
||||
base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account)
|
||||
self.assertEqual(len(base_amount_tax_lines), 2)
|
||||
self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 27.78)
|
||||
self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 27.78)
|
||||
|
||||
self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
|
||||
5.56)
|
||||
self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit,
|
||||
5.56)
|
||||
|
||||
# For second move
|
||||
move_lines = cash_basis_move2.line_ids
|
||||
base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account)
|
||||
self.assertEqual(len(base_amount_tax_lines), 2)
|
||||
self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 55.55)
|
||||
self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 55.55)
|
||||
|
||||
self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
|
||||
11.11)
|
||||
self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit,
|
||||
11.11)
|
||||
|
||||
def test_reconciliation_cash_basis02(self):
|
||||
# Simulates an invoice made up by 2 lines
|
||||
# both subjected to cash basis taxes
|
||||
# with 2 payment terms
|
||||
# And partial payment not martching any payment term
|
||||
|
||||
company = self.company
|
||||
company.tax_cash_basis_journal_id = self.cash_basis_journal
|
||||
tax_cash_basis10percent = self.tax_cash_basis.copy({'amount': 10})
|
||||
tax_waiting_account10 = self.tax_waiting_account.copy({
|
||||
'name': 'TAX WAIT 10',
|
||||
'code': 'TWAIT1',
|
||||
})
|
||||
|
||||
purchase_move = self.env['account.move'].create({
|
||||
'journal_id': self.purchase_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {'account_id': self.account_rsa.id, 'debit': 0.0, 'credit': 50.0}),
|
||||
(0, 0, {'account_id': self.account_rsa.id, 'debit': 0.0, 'credit': 105.0}),
|
||||
(0, 0, {'name': 'expenseTaxed 10%', 'account_id': self.expense_account.id, 'debit': 50.0, 'credit': 0.0, 'tax_ids': [(4, tax_cash_basis10percent.id)], 'tax_exigible': False}),
|
||||
(0, 0, {
|
||||
'name': 'TaxLine0',
|
||||
'account_id': tax_waiting_account10.id,
|
||||
'debit': 5.0,
|
||||
'credit': 0.0,
|
||||
'tax_repartition_line_id': tax_cash_basis10percent.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_exigible': False,
|
||||
'tax_base_amount': 50.00,
|
||||
}),
|
||||
(0, 0, {'name': 'expenseTaxed 20%', 'account_id': self.expense_account.id, 'debit': 83.33, 'credit': 0.0, 'tax_ids': [(4, self.tax_cash_basis.id)], 'tax_exigible': False}),
|
||||
(0, 0, {
|
||||
'name': 'TaxLine1',
|
||||
'account_id': self.tax_waiting_account.id,
|
||||
'debit': 16.67,
|
||||
'credit': 0.0,
|
||||
'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_exigible': False,
|
||||
'tax_base_amount': 83.33,
|
||||
}),
|
||||
],
|
||||
})
|
||||
purchase_move.post()
|
||||
|
||||
payment_move0 = self.env['account.move'].create({
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'line_ids': [
|
||||
(0, 0, {'account_id': self.account_rsa.id, 'debit': 40.0, 'credit': 0.0}),
|
||||
(0, 0, {'account_id': self.account_euro.id, 'debit': 0.0, 'credit': 40.0}),
|
||||
],
|
||||
})
|
||||
payment_move0.post()
|
||||
|
||||
payment_move1 = self.env['account.move'].create({
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'line_ids': [
|
||||
(0, 0, {'account_id': self.account_rsa.id, 'debit': 115.0, 'credit': 0.0}),
|
||||
(0, 0, {'account_id': self.account_euro.id, 'debit': 0.0, 'credit': 115.0}),
|
||||
],
|
||||
})
|
||||
payment_move1.post()
|
||||
|
||||
(purchase_move + payment_move0 + payment_move1).post()
|
||||
(purchase_move + payment_move0).mapped('line_ids').sorted().filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
|
||||
(purchase_move + payment_move1).mapped('line_ids').sorted().filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
|
||||
|
||||
cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)])
|
||||
|
||||
self.assertEqual(len(cash_basis_moves), 3)
|
||||
self.assertTrue(cash_basis_moves.exists())
|
||||
|
||||
# check reconciliation in Payable account
|
||||
purchase_line_ids = purchase_move.line_ids.sorted()
|
||||
self.assertTrue(purchase_line_ids[0].full_reconcile_id.exists())
|
||||
self.assertEqual(purchase_line_ids[0].full_reconcile_id.reconciled_line_ids,
|
||||
(purchase_move + payment_move0 + payment_move1).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable'))
|
||||
|
||||
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
|
||||
|
||||
# check reconciliation in the tax waiting account
|
||||
self.assertTrue(purchase_line_ids[3].full_reconcile_id.exists())
|
||||
self.assertEqual(purchase_line_ids[3].full_reconcile_id.reconciled_line_ids,
|
||||
cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + purchase_line_ids[3])
|
||||
|
||||
self.assertTrue(purchase_line_ids[5].full_reconcile_id.exists())
|
||||
self.assertEqual(purchase_line_ids[5].full_reconcile_id.reconciled_line_ids,
|
||||
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + purchase_line_ids[5])
|
||||
|
||||
self.assertEqual(len(cash_basis_aml_ids), 24)
|
||||
|
||||
# check amounts
|
||||
expected_move_amounts = [
|
||||
{'base_20': 56.45, 'tax_20': 11.29, 'base_10': 33.87, 'tax_10': 3.39},
|
||||
{'base_20': 5.38, 'tax_20': 1.08, 'base_10': 3.23, 'tax_10': 0.32},
|
||||
{'base_20': 21.50, 'tax_20': 4.30, 'base_10': 12.90, 'tax_10': 1.29},
|
||||
]
|
||||
|
||||
index = 0
|
||||
for cb_move in cash_basis_moves:
|
||||
expected = expected_move_amounts[index]
|
||||
move_lines = cb_move.line_ids
|
||||
base_amount_tax_lines20per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '20%' in l.name)
|
||||
base_amount_tax_lines10per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '10%' in l.name)
|
||||
self.assertEqual(len(base_amount_tax_lines20per), 2)
|
||||
|
||||
self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('credit')), expected['base_20'])
|
||||
self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('debit')), expected['base_20'])
|
||||
|
||||
self.assertEqual(len(base_amount_tax_lines10per), 2)
|
||||
self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('credit')), expected['base_10'])
|
||||
self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('debit')), expected['base_10'])
|
||||
|
||||
self.assertAlmostEqual(
|
||||
(move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
|
||||
.filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
|
||||
expected['tax_20']
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
(move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
|
||||
.filtered(lambda l: 'TaxLine1' in l.name).debit,
|
||||
expected['tax_20']
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(
|
||||
(move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
|
||||
.filtered(lambda l: l.account_id == tax_waiting_account10).credit,
|
||||
expected['tax_10']
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
(move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
|
||||
.filtered(lambda l: 'TaxLine0' in l.name).debit,
|
||||
expected['tax_10']
|
||||
)
|
||||
index += 1
|
||||
|
||||
def test_reconciliation_cash_basis_fx_01(self):
|
||||
"""
|
||||
Company's Currency EUR
|
||||
|
||||
Having issued an invoice at date Nov-21-2018 as:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
Expenses 5,301.00 USD 106,841.65 0.00
|
||||
Taxes 848.16 USD 17,094.66 0.00
|
||||
Payables -6,149.16 USD 0.00 123,936.31
|
||||
|
||||
On Dec-20-2018 user issues an FX Journal Entry as:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
Payables 0.00 USD 167.86 0.00
|
||||
FX Gains 0.00 USD 0.00 167.86
|
||||
|
||||
On Same day user records a payment for:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
Payables 6,149.16 USD 123,768.45 0.00
|
||||
Bank -6,149.16 USD 0.00 123,768.45
|
||||
|
||||
And then reconciles the Payables Items which shall render only one Tax
|
||||
Cash Basis Journal Entry because of the actual payment, i.e.
|
||||
amount_currency != 0:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
Tax Base Acc. 0.00 USD 106,841.65 0.00
|
||||
Tax Base Acc. 0.00 USD 0.00 106,841.65
|
||||
Creditable Taxes 848.16 USD 17,094.66 0.00
|
||||
Taxes -848.16 USD 0.00 17,094.66
|
||||
"""
|
||||
|
||||
company = self.company
|
||||
company.country_id = self.ref('base.us')
|
||||
company.tax_cash_basis_journal_id = self.cash_basis_journal
|
||||
|
||||
# Purchase
|
||||
purchase_move = self.env['account.move'].create({
|
||||
'journal_id': self.purchase_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'name': 'expenseTaxed',
|
||||
'account_id': self.expense_account.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'tax_ids': [(4, self.tax_cash_basis.id)],
|
||||
'tax_exigible': False,
|
||||
'debit': 106841.65, 'credit': 0.0, 'amount_currency': 5301.00,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'TaxLine',
|
||||
'account_id': self.tax_waiting_account.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_exigible': False,
|
||||
'tax_base_amount': 106841.65,
|
||||
'debit': 17094.66, 'credit': 0.0, 'amount_currency': 848.16,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Payable',
|
||||
'account_id': self.account_rsa.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 0.0, 'credit': 123936.31, 'amount_currency': -6149.16,
|
||||
}),
|
||||
],
|
||||
})
|
||||
purchase_move.post()
|
||||
|
||||
# FX 01 Move
|
||||
fx_move_01 = self.env['account.move'].create({
|
||||
'journal_id': self.fx_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': self.account_rsa.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 167.86, 'credit': 0.0, 'amount_currency': 0.00,
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': self.diff_income_account.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 0.0, 'credit': 167.86, 'amount_currency': 0.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
fx_move_01.post()
|
||||
|
||||
# Payment Move
|
||||
payment_move = self.env['account.move'].create({
|
||||
'journal_id': self.bank_journal_usd.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': self.account_rsa.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 123768.45, 'credit': 0.0, 'amount_currency': 6149.16,
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': self.account_usd.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 0.0, 'credit': 123768.45, 'amount_currency': -6149.16,
|
||||
}),
|
||||
],
|
||||
})
|
||||
payment_move.post()
|
||||
|
||||
to_reconcile = (
|
||||
(purchase_move + payment_move + fx_move_01).mapped('line_ids')
|
||||
.filtered(lambda l: l.account_id.internal_type == 'payable'))
|
||||
to_reconcile.reconcile()
|
||||
|
||||
# check reconciliation in Payable account
|
||||
purchase_line_ids = purchase_move.line_ids.sorted()
|
||||
fx_move_01_line_ids = fx_move_01.line_ids.sorted()
|
||||
payment_line_ids = payment_move.line_ids.sorted()
|
||||
self.assertTrue(purchase_line_ids[2].full_reconcile_id.exists())
|
||||
self.assertEqual(
|
||||
purchase_line_ids[2].full_reconcile_id.reconciled_line_ids,
|
||||
purchase_line_ids[2] + fx_move_01_line_ids[0] + payment_line_ids[0])
|
||||
|
||||
# check cash basis
|
||||
cash_basis_moves = self.env['account.move'].search(
|
||||
[('journal_id', '=', self.cash_basis_journal.id)])
|
||||
|
||||
self.assertEqual(len(cash_basis_moves), 1)
|
||||
|
||||
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
|
||||
self.assertEqual(len(cash_basis_aml_ids), 4)
|
||||
|
||||
# check amounts
|
||||
cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.currency_id.is_zero(sum(line.credit for line in m.line_ids) - 123936.31))
|
||||
|
||||
self.assertTrue(cash_basis_move1.exists())
|
||||
|
||||
# For first move
|
||||
move_lines = cash_basis_moves.line_ids
|
||||
base_amount_tax_lines = move_lines.filtered(
|
||||
lambda l: l.account_id == self.tax_base_amount_account)
|
||||
self.assertEqual(len(base_amount_tax_lines), 2)
|
||||
self.assertAlmostEqual(
|
||||
sum(base_amount_tax_lines.mapped('credit')), 106841.65)
|
||||
self.assertAlmostEqual(
|
||||
sum(base_amount_tax_lines.mapped('debit')), 106841.65)
|
||||
|
||||
self.assertAlmostEqual(
|
||||
(move_lines - base_amount_tax_lines)
|
||||
.filtered(lambda l: l.account_id == self.tax_waiting_account)
|
||||
.credit, 17094.66)
|
||||
self.assertAlmostEqual(
|
||||
(move_lines - base_amount_tax_lines)
|
||||
.filtered(lambda l: l.account_id == self.tax_final_account)
|
||||
.debit, 17094.66)
|
||||
|
||||
def test_reconciliation_cash_basis_fx_02(self):
|
||||
"""
|
||||
Company's Currency EUR
|
||||
|
||||
Having issued an invoice at date Nov-21-2018 as:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
Expenses 5,301.00 USD 106,841.65 0.00
|
||||
Taxes 848.16 USD 17,094.66 0.00
|
||||
Payables -6,149.16 USD 0.00 123,936.31
|
||||
|
||||
On Nov-30-2018 user issues an FX Journal Entry as:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
FX Losses 0.00 USD 1.572.96 0.00
|
||||
Payables 0.00 USD 0.00 1.572.96
|
||||
|
||||
On Dec-20-2018 user issues an FX Journal Entry as:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
Payables 0.00 USD 1.740.82 0.00
|
||||
FX Gains 0.00 USD 0.00 1.740.82
|
||||
|
||||
On Same day user records a payment for:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
Payables 6,149.16 USD 123,768.45 0.00
|
||||
Bank -6,149.16 USD 0.00 123,768.45
|
||||
|
||||
And then reconciles the Payables Items which shall render only one Tax
|
||||
Cash Basis Journal Entry because of the actual payment, i.e.
|
||||
amount_currency != 0:
|
||||
|
||||
Accounts Amount Currency Debit(EUR) Credit(EUR)
|
||||
---------------------------------------------------------------------
|
||||
Tax Base Acc. 0.00 USD 106,841.65 0.00
|
||||
Tax Base Acc. 0.00 USD 0.00 106,841.65
|
||||
Creditable Taxes 848.16 USD 17,094.66 0.00
|
||||
Taxes -848.16 USD 0.00 17,094.66
|
||||
"""
|
||||
|
||||
company = self.company
|
||||
company.country_id = self.ref('base.us')
|
||||
company.tax_cash_basis_journal_id = self.cash_basis_journal
|
||||
|
||||
# Purchase
|
||||
purchase_move = self.env['account.move'].create({
|
||||
'journal_id': self.purchase_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'name': 'expenseTaxed',
|
||||
'account_id': self.expense_account.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'tax_ids': [(4, self.tax_cash_basis.id)],
|
||||
'tax_exigible': False,
|
||||
'debit': 106841.65, 'credit': 0.0, 'amount_currency': 5301.00,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'TaxLine',
|
||||
'account_id': self.tax_waiting_account.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_exigible': False,
|
||||
'tax_base_amount': 106841.65,
|
||||
'debit': 17094.66, 'credit': 0.0, 'amount_currency': 848.16,
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'Payable',
|
||||
'account_id': self.account_rsa.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 0.0, 'credit': 123936.31, 'amount_currency': -6149.16,
|
||||
}),
|
||||
],
|
||||
})
|
||||
purchase_move.post()
|
||||
|
||||
# FX 01 Move
|
||||
fx_move_01 = self.env['account.move'].create({
|
||||
'journal_id': self.fx_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': self.account_rsa.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 0.0, 'credit': 1572.96, 'amount_currency': 0.00,
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': self.diff_expense_account.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 1572.96, 'credit': 0.0, 'amount_currency': 0.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
fx_move_01.post()
|
||||
|
||||
# FX 02 Move
|
||||
fx_move_02 = self.env['account.move'].create({
|
||||
'journal_id': self.fx_journal.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': self.account_rsa.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 1740.82, 'credit': 0.0, 'amount_currency': 0.00,
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': self.diff_income_account.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 0.0, 'credit': 1740.82, 'amount_currency': 0.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
fx_move_02.post()
|
||||
|
||||
# Payment Move
|
||||
payment_move = self.env['account.move'].create({
|
||||
'journal_id': self.bank_journal_usd.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'account_id': self.account_rsa.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 123768.45, 'credit': 0.0, 'amount_currency': 6149.16,
|
||||
}),
|
||||
(0, 0, {
|
||||
'account_id': self.account_usd.id,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'debit': 0.0, 'credit': 123768.45, 'amount_currency': -6149.16,
|
||||
}),
|
||||
],
|
||||
})
|
||||
payment_move.post()
|
||||
|
||||
to_reconcile = (
|
||||
(purchase_move + payment_move + fx_move_01 + fx_move_02)
|
||||
.mapped('line_ids')
|
||||
.filtered(lambda l: l.account_id.internal_type == 'payable'))
|
||||
to_reconcile.reconcile()
|
||||
|
||||
# check reconciliation in Payable account
|
||||
purchase_line_ids = purchase_move.line_ids.sorted()
|
||||
fx_move_01_line_ids = fx_move_01.line_ids.sorted()
|
||||
fx_move_02_line_ids = fx_move_02.line_ids.sorted()
|
||||
payment_line_ids = payment_move.line_ids.sorted()
|
||||
self.assertTrue(purchase_line_ids[2].full_reconcile_id.exists())
|
||||
self.assertEqual(
|
||||
purchase_line_ids[2].full_reconcile_id.reconciled_line_ids,
|
||||
purchase_line_ids[2] + fx_move_01_line_ids[0] + fx_move_02_line_ids[0] +
|
||||
payment_line_ids[0])
|
||||
|
||||
# check cash basis
|
||||
cash_basis_moves = self.env['account.move'].search(
|
||||
[('journal_id', '=', self.cash_basis_journal.id)])
|
||||
|
||||
self.assertEqual(len(cash_basis_moves), 1)
|
||||
|
||||
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
|
||||
self.assertEqual(len(cash_basis_aml_ids), 4)
|
||||
|
||||
# check amounts
|
||||
cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.currency_id.is_zero(sum(line.credit for line in m.line_ids) - 123936.31))
|
||||
|
||||
self.assertTrue(cash_basis_move1.exists())
|
||||
|
||||
# For first move
|
||||
move_lines = cash_basis_move1.line_ids
|
||||
base_amount_tax_lines = move_lines.filtered(
|
||||
lambda l: l.account_id == self.tax_base_amount_account)
|
||||
self.assertEqual(len(base_amount_tax_lines), 2)
|
||||
self.assertAlmostEqual(
|
||||
sum(base_amount_tax_lines.mapped('credit')), 106841.65)
|
||||
self.assertAlmostEqual(
|
||||
sum(base_amount_tax_lines.mapped('debit')), 106841.65)
|
||||
|
||||
self.assertAlmostEqual(
|
||||
(move_lines - base_amount_tax_lines)
|
||||
.filtered(lambda l: l.account_id == self.tax_waiting_account)
|
||||
.credit, 17094.66)
|
||||
self.assertAlmostEqual(
|
||||
(move_lines - base_amount_tax_lines)
|
||||
.filtered(lambda l: l.account_id == self.tax_final_account)
|
||||
.debit, 17094.66)
|
||||
|
||||
def test_reconciliation_cash_basis_revert(self):
|
||||
company = self.company
|
||||
company.tax_cash_basis_journal_id = self.cash_basis_journal
|
||||
tax_cash_basis10percent = self.tax_cash_basis.copy({'amount': 10})
|
||||
self.tax_waiting_account.reconcile = True
|
||||
tax_waiting_account10 = self.tax_waiting_account.copy({
|
||||
'name': 'TAX WAIT 10',
|
||||
'code': 'TWAIT1',
|
||||
})
|
||||
|
||||
AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False)
|
||||
|
||||
# Purchase
|
||||
purchase_move = self.env['account.move'].create({
|
||||
'name': 'invoice',
|
||||
'journal_id': self.purchase_journal.id,
|
||||
})
|
||||
|
||||
purchase_payable_line0 = AccountMoveLine.create({
|
||||
'account_id': self.account_rsa.id,
|
||||
'credit': 175,
|
||||
'move_id': purchase_move.id,
|
||||
})
|
||||
|
||||
AccountMoveLine.create({
|
||||
'name': 'expenseTaxed 10%',
|
||||
'account_id': self.expense_account.id,
|
||||
'debit': 50,
|
||||
'move_id': purchase_move.id,
|
||||
'tax_ids': [(4, tax_cash_basis10percent.id, False)],
|
||||
})
|
||||
tax_line0 = AccountMoveLine.create({
|
||||
'name': 'TaxLine0',
|
||||
'account_id': tax_waiting_account10.id,
|
||||
'debit': 5,
|
||||
'move_id': purchase_move.id,
|
||||
'tax_repartition_line_id': tax_cash_basis10percent.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_base_amount': 50,
|
||||
})
|
||||
AccountMoveLine.create({
|
||||
'name': 'expenseTaxed 20%',
|
||||
'account_id': self.expense_account.id,
|
||||
'debit': 100,
|
||||
'move_id': purchase_move.id,
|
||||
'tax_ids': [(4, self.tax_cash_basis.id, False)],
|
||||
})
|
||||
tax_line1 = AccountMoveLine.create({
|
||||
'name': 'TaxLine1',
|
||||
'account_id': self.tax_waiting_account.id,
|
||||
'debit': 20,
|
||||
'move_id': purchase_move.id,
|
||||
'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_base_amount': 100,
|
||||
})
|
||||
purchase_move.post()
|
||||
|
||||
reverted = purchase_move._reverse_moves(cancel=True)
|
||||
self.assertTrue(reverted.exists())
|
||||
|
||||
for inv_line in [purchase_payable_line0, tax_line0, tax_line1]:
|
||||
self.assertTrue(inv_line.full_reconcile_id.exists())
|
||||
reverted_expected = reverted.line_ids.filtered(lambda l: l.account_id == inv_line.account_id)
|
||||
self.assertEqual(len(reverted_expected), 1)
|
||||
self.assertEqual(reverted_expected.full_reconcile_id, inv_line.full_reconcile_id)
|
||||
|
||||
def test_reconciliation_revert_future(self):
|
||||
company = self.company
|
||||
company.tax_cash_basis_journal_id = self.cash_basis_journal
|
||||
@@ -1404,69 +757,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
self.assertEqual(reverse_move.auto_post, True)
|
||||
self.assertEqual(reverse_move.date, reverse_date)
|
||||
|
||||
def test_reconciliation_cash_basis_foreign_currency_low_values(self):
|
||||
self.company.tax_cash_basis_journal_id = self.cash_basis_journal
|
||||
journal = self.env['account.journal'].create({
|
||||
'name': 'Bank', 'type': 'bank', 'code': 'THE',
|
||||
'currency_id': self.currency_usd_id,
|
||||
})
|
||||
usd = self.env['res.currency'].browse(self.currency_usd_id)
|
||||
usd.rate_ids.unlink()
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': time.strftime('%Y-01-01'),
|
||||
'rate': 1/17.0,
|
||||
'currency_id': self.currency_usd_id,
|
||||
'company_id': self.company.id,
|
||||
})
|
||||
|
||||
move_form = Form(self.env['account.move'].with_context(default_move_type='out_invoice'))
|
||||
move_form.partner_id = self.partner_agrolait
|
||||
move_form.currency_id = self.env.ref('base.USD')
|
||||
move_form.invoice_date = time.strftime('%Y') + '-07-01'
|
||||
with move_form.invoice_line_ids.new() as line_form:
|
||||
line_form.name = 'test line'
|
||||
line_form.price_unit = 50
|
||||
line_form.tax_ids.clear()
|
||||
invoice = move_form.save()
|
||||
move_form = Form(invoice)
|
||||
with move_form.invoice_line_ids.edit(0) as line_form:
|
||||
line_form.tax_ids.add(self.tax_cash_basis)
|
||||
invoice = move_form.save()
|
||||
invoice.post()
|
||||
|
||||
self.assertTrue(invoice.currency_id != self.env.user.company_id.currency_id)
|
||||
|
||||
# First Payment
|
||||
line = invoice.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))
|
||||
payment0 = self.make_payment(invoice, journal, amount=59.99, reconcile_param=[{'id': line.id, 'balance': -59.99}])
|
||||
self.assertEqual(invoice.amount_residual, 0.01)
|
||||
|
||||
tax_waiting_line = invoice.line_ids.filtered(lambda l: l.account_id == self.tax_waiting_account)
|
||||
self.assertTrue(tax_waiting_line.exists())
|
||||
self.assertFalse(tax_waiting_line.reconciled)
|
||||
|
||||
move_caba0 = tax_waiting_line.matched_debit_ids.debit_move_id.move_id
|
||||
self.assertTrue(move_caba0.exists())
|
||||
self.assertEqual(move_caba0.journal_id, self.env.user.company_id.tax_cash_basis_journal_id)
|
||||
|
||||
pay_receivable_line0 = payment0.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv)
|
||||
self.assertTrue(pay_receivable_line0.reconciled)
|
||||
self.assertEqual(pay_receivable_line0.matched_debit_ids, move_caba0.tax_cash_basis_rec_id)
|
||||
|
||||
# Second Payment
|
||||
payment1 = self.make_payment(invoice, journal, 0.01, reconcile_param=[{'id': line.id, 'balance': -0.01}])
|
||||
self.assertEqual(invoice.amount_residual, 0)
|
||||
self.assertEqual(invoice.payment_state, 'paid')
|
||||
|
||||
self.assertTrue(tax_waiting_line.reconciled)
|
||||
move_caba1 = tax_waiting_line.matched_debit_ids.mapped('debit_move_id').mapped('move_id').filtered(lambda m: m != move_caba0)
|
||||
self.assertEqual(len(move_caba1.exists()), 1)
|
||||
self.assertEqual(move_caba1.journal_id, self.env.user.company_id.tax_cash_basis_journal_id)
|
||||
|
||||
pay_receivable_line1 = payment1.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv)
|
||||
self.assertTrue(pay_receivable_line1.reconciled)
|
||||
self.assertEqual(pay_receivable_line1.matched_debit_ids, move_caba1.tax_cash_basis_rec_id)
|
||||
|
||||
def test_reconciliation_with_currency(self):
|
||||
#reconciliation on an account having a foreign currency being
|
||||
#the same as the company one
|
||||
@@ -1927,7 +1217,13 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
|
||||
self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
|
||||
|
||||
self.assertFalse(inv1_receivable.full_reconcile_id.exchange_move_id)
|
||||
# Before saas-13.4, there was no exchange difference entry generated because the amount was
|
||||
# wrongly converted in the _amount_residual method at the invoice date like this:
|
||||
# 315.15 * (600.0 / 540.25) = 515.15 * 1.110596946 = 350.004627487 ~= 350.0
|
||||
# Now, the conversion is made using the payment rate using the _convert method and the
|
||||
# encoded currency rate:
|
||||
# 315.15 * 1.1106 = 350.00559 ~= 350.01
|
||||
self.assertTrue(inv1_receivable.full_reconcile_id.exchange_move_id)
|
||||
|
||||
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
|
||||
self.assertEqual(inv2.payment_state, 'paid')
|
||||
|
||||
@@ -176,7 +176,6 @@
|
||||
<group name="tax_configuration">
|
||||
<field name="active" groups="base.group_no_one"/>
|
||||
<field name="tax_exigibility" widget="radio" attrs="{'invisible':[('amount_type','=', 'group')]}"/>
|
||||
<field name="cash_basis_base_account_id" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}"/>
|
||||
<field name="cash_basis_transition_account_id" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -155,7 +155,6 @@
|
||||
<field name="include_base_amount" attrs="{'invisible':[('amount_type','=', 'group')]}" />
|
||||
<field name="hide_tax_exigibility" invisible="1"/>
|
||||
<field name="tax_exigibility" widget="radio" attrs="{'invisible':['|', ('amount_type','=', 'group'), ('hide_tax_exigibility', '=', False)]}" groups="account.group_account_readonly"/>
|
||||
<field name="cash_basis_base_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')]}" groups="account.group_account_readonly"/>
|
||||
<field name="cash_basis_transition_account_id" options="{'no_create': True}" attrs="{'invisible': [('tax_exigibility', '=', 'on_invoice')], 'required': [('tax_exigibility', '=', 'on_payment')]}" groups="account.group_account_readonly"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="o.move_id._get_reconciled_invoices_partials()" t-as="rec">
|
||||
<t t-set="amount" t-value="rec[0]"/>
|
||||
<t t-set="amount" t-value="rec[1]"/>
|
||||
<t t-set="inv" t-value="rec[2].move_id"/>
|
||||
<td><span t-field="inv.invoice_date"/></td>
|
||||
<td><span t-field="inv.name"/></td>
|
||||
|
||||
@@ -122,6 +122,10 @@
|
||||
<label for="tax_cash_basis_journal_id" class="col-lg-3 o_light_label"/>
|
||||
<field name="tax_cash_basis_journal_id"/>
|
||||
</div>
|
||||
<div class="row mt16">
|
||||
<label for="account_cash_basis_base_account_id" class="col-lg-3 o_light_label"/>
|
||||
<field name="account_cash_basis_base_account_id"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,23 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def action_invoice_paid(self):
|
||||
# OVERRIDE to mark as paid the expense sheets.
|
||||
res = super().action_invoice_paid()
|
||||
|
||||
if self:
|
||||
expense_sheets = self.env['hr.expense.sheet'].search([
|
||||
('account_move_id', 'in', self.ids),
|
||||
('state', '!=', 'done'),
|
||||
])
|
||||
expense_sheets.set_to_paid()
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
@@ -15,13 +31,3 @@ class AccountMoveLine(models.Model):
|
||||
if self.expense_id:
|
||||
attachment_domains.append([('res_model', '=', 'hr.expense'), ('res_id', '=', self.expense_id.id)])
|
||||
return attachment_domains
|
||||
|
||||
def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
|
||||
res = super(AccountMoveLine, self).reconcile(writeoff_acc_id=writeoff_acc_id, writeoff_journal_id=writeoff_journal_id)
|
||||
account_move_ids = [l.move_id.id for l in self if float_compare(l.move_id._get_cash_basis_matched_percentage(), 1, precision_digits=5) == 0]
|
||||
if account_move_ids:
|
||||
expense_sheets = self.env['hr.expense.sheet'].search([
|
||||
('account_move_id', 'in', account_move_ids), ('state', '!=', 'done')
|
||||
])
|
||||
expense_sheets.set_to_paid()
|
||||
return res
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_0"/>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
|
||||
<field name="cash_basis_transition_account_id" ref="cuenta209_01"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -110,7 +109,6 @@
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_16"/>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
|
||||
<field name="cash_basis_transition_account_id" ref="cuenta209_01"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -150,7 +148,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_ret_4"/>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
|
||||
<field name="cash_basis_transition_account_id" ref="cuenta216_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -190,7 +187,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_ret_10"/>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
|
||||
<field name="cash_basis_transition_account_id" ref="cuenta216_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -300,7 +296,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_ret_1067"/>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
|
||||
<field name="cash_basis_transition_account_id" ref="cuenta216_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -340,7 +335,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_ret_1067"/>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
|
||||
<field name="cash_basis_transition_account_id" ref="cuenta216_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -380,7 +374,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_0"/>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
|
||||
<field name="cash_basis_transition_account_id" ref="cuenta119_01"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -418,7 +411,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_16"/>
|
||||
<field name="tax_exigibility">on_payment</field>
|
||||
<field name="cash_basis_base_account_id" ref="cuenta801_01_99"/>
|
||||
<field name="cash_basis_transition_account_id" ref="cuenta119_01"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<field name="property_stock_account_input_categ_id" ref="cuenta205_06_01"/>
|
||||
<field name="property_stock_account_output_categ_id" ref="cuenta107_05_01"/>
|
||||
<field name="property_stock_valuation_account_id" ref="cuenta115_01"/>
|
||||
<field name="property_cash_basis_base_account_id" ref="cuenta801_01_99" />
|
||||
<field name="income_currency_exchange_account_id" ref="cuenta702_01"/>
|
||||
<field name="expense_currency_exchange_account_id" ref="cuenta701_01"/>
|
||||
<field name="default_pos_receivable_account_id" ref="cuenta105_02" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.tests.common import SavepointCase
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
Reference in New Issue
Block a user