[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:
Laurent Smet
2020-06-15 08:36:04 +00:00
parent f074772d25
commit beccf82e09
24 changed files with 3250 additions and 1556 deletions
+2
View File
@@ -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
-6
View File
@@ -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.")
+8 -8
View File
@@ -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
+11 -1
View File
@@ -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') {
+2
View File
@@ -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
+41 -1
View File
@@ -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
+8 -712
View File
@@ -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>
+17 -11
View File
@@ -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
-8
View File
@@ -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