[REV] account: cash discount improvements

Current task is not covered the whole cash discount feature. For now, we decided to revert the current feature and make a new more complete one later.

This reverts commit 7482aff6d7.

closes odoo/odoo#98986

Related: odoo/enterprise#30783
Signed-off-by: Quentin De Paoli <qdp@odoo.com>
This commit is contained in:
Laurent Smet
2022-08-27 03:33:55 +02:00
parent 124ae0f340
commit 8b1ed8e510
15 changed files with 68 additions and 790 deletions
-51
View File
@@ -444,12 +444,6 @@ class AccountMove(models.Model):
tracking=True,
)
# ==== Early payment cash discount field ====
invoice_early_pay_amount_after_discount = fields.Monetary(
compute='_compute_early_pay_amount_after_discount',
help="Total amount left to pay after the discount was applied",
)
# === Reverse feature fields === #
reversed_entry_id = fields.Many2one(
comodel_name='account.move',
@@ -1162,21 +1156,6 @@ class AccountMove(models.Model):
# Non-invoice moves don't support that field (because of multicurrency: all lines of the invoice share the same currency)
move.tax_totals = None
def _get_report_early_payment_totals_values(self):
self.ensure_one()
if self.move_type not in ['out_invoice', 'out_receipt', 'in_invoice', 'in_receipt'] or \
not self.payment_state == 'not_paid' or \
not self.invoice_payment_term_id.has_early_payment:
return
base_lines = self.line_ids.filtered(lambda x: x.display_type == 'product')
return self.env['account.tax']._prepare_tax_totals(
[x._convert_to_tax_base_line_dict() for x in base_lines],
self.currency_id,
early_payment_term=self.invoice_payment_term_id,
)
@api.depends('partner_id', 'invoice_source_email', 'partner_id.name')
def _compute_invoice_partner_display_info(self):
for move in self:
@@ -1295,36 +1274,6 @@ class AccountMove(models.Model):
updated_credit = move.partner_id.credit + move.amount_total_signed
move.partner_credit_warning = self._build_credit_warning_message(move, updated_credit)
@api.depends('invoice_payment_term_id', 'amount_residual_signed', 'currency_id')
def _compute_early_pay_amount_after_discount(self):
for record in self:
if record.invoice_payment_term_id.has_early_payment:
percentage_to_discount = record.invoice_payment_term_id.percentage_to_discount
discount_computation = self.invoice_payment_term_id.discount_computation
discounted_amount_untaxed = (100 - percentage_to_discount) * record.amount_untaxed / 100
if discount_computation == 'included':
discounted_amount_tax = (100 - percentage_to_discount) * record.amount_tax / 100
else:
discounted_amount_tax = record.amount_tax
record.invoice_early_pay_amount_after_discount = discounted_amount_untaxed + discounted_amount_tax
if record.currency_id.compare_amounts(record.invoice_early_pay_amount_after_discount, 0.0) <= 0.0:
record.invoice_early_pay_amount_after_discount = 0
else:
record.invoice_early_pay_amount_after_discount = 0
def _is_eligible_for_early_discount(self, payment_date):
'''
An early payment discount is possible if the option has been activated,
no partial payment was registered,
and the payment date is before the last early_payment_date possible.
'''
self.ensure_one()
return self.move_type in ['out_invoice', 'out_receipt', 'in_invoice', 'in_receipt'] and \
self.invoice_payment_term_id.has_early_payment and \
self.payment_state == 'not_paid' and \
payment_date <= self.invoice_payment_term_id._get_last_date_for_discount(self.invoice_date)
def _build_credit_warning_message(self, record, updated_credit):
''' Build the warning message that will be displayed in a yellow banner on top of the current record
if the partner exceeds a credit limit (set on the company or the partner itself).
@@ -4,7 +4,6 @@ from odoo import api, exceptions, fields, models, _
from odoo.exceptions import UserError, ValidationError
from dateutil.relativedelta import relativedelta
from datetime import timedelta
class AccountPaymentTerm(models.Model):
@@ -22,17 +21,6 @@ class AccountPaymentTerm(models.Model):
company_id = fields.Many2one('res.company', string='Company')
sequence = fields.Integer(required=True, default=10)
# -------Early payment discount fields-------
early_payment_applicable = fields.Boolean(string="True if the conditions required to offer an EPD are fulfilled.", compute="_compute_early_payment_applicable")
has_early_payment = fields.Boolean(string="Apply Early Payment Discount", compute="_compute_has_early_payment", readonly=False, store=True)
percentage_to_discount = fields.Float("Discount", digits='Discount', default=2)
discount_computation = fields.Selection([
('included', 'Tax included'),
('excluded', 'Tax excluded'),
], string='Computation', default='included')
discount_days = fields.Integer(string='Availability', required=True, default=7)
discount_account_id = fields.Many2one(comodel_name='account.account', string='Counterpart Account')
@api.constrains('line_ids')
def _check_lines(self):
for terms in self:
@@ -107,32 +95,6 @@ class AccountPaymentTerm(models.Model):
).unlink()
return super(AccountPaymentTerm, self).unlink()
@api.depends('line_ids')
def _compute_early_payment_applicable(self):
for term in self:
term.early_payment_applicable = not any(line.value != 'balance' for line in term.line_ids)
@api.depends('early_payment_applicable')
def _compute_has_early_payment(self):
for term in self:
if not term.early_payment_applicable:
term.has_early_payment = False
@api.onchange('discount_days')
def _onchange_discount_days(self):
if self.discount_days <= 0:
raise ValidationError(_("The discount availability must be strictly positive."))
@api.onchange('percentage_to_discount')
def _onchange_percentage_to_discount(self):
if self.percentage_to_discount <= 0:
raise ValidationError(_("The discount percentage must be strictly positive."))
if self.percentage_to_discount > 100:
raise ValidationError(_("The discount percentage cannot exceed 100."))
def _get_last_date_for_discount(self, move_date):
return move_date + timedelta(days=self.discount_days)
class AccountPaymentTermLine(models.Model):
_name = "account.payment.term.line"
@@ -809,126 +809,74 @@ class AccountReconcileModel(models.Model):
return self.env['res.partner']
def _get_invoice_matching_amls_result(self, st_line, partner, candidate_vals):
def _create_result_dict(amls_values_list, status):
if 'rejected' in status:
return
result = {
'amls': self.env['account.move.line'],
'amls_values_list': amls_values_list,
}
for aml_values in amls_values_list:
result['amls'] |= aml_values['aml']
if 'allow_write_off' in status and self.line_ids:
result['status'] = 'write_off'
if 'allow_auto_reconcile' in status and candidate_vals['allow_auto_reconcile'] and self.auto_reconcile:
result['auto_reconcile'] = True
return result
st_line_currency = st_line.foreign_currency_id or st_line.currency_id
st_line_amount = st_line._prepare_move_line_default_vals()[1]['amount_currency']
sign = 1 if st_line_amount > 0.0 else -1
amls = candidate_vals['amls']
amls_values_list = []
same_currency_mode = amls.currency_id == st_line_currency
for aml in amls:
extra_aml_values = {}
if amls.currency_id == st_line_currency:
st_line_amount = st_line._prepare_move_line_default_vals()[1]['amount_currency']
sign = 1 if st_line_amount > 0.0 else -1
amount_residual = aml.amount_residual
amount_residual_currency = aml.amount_residual_currency
kepts_amls = self.env['account.move.line']
sum_aml_residual = 0.0
for aml in amls:
# Manage the early payment discount.
if same_currency_mode and aml.move_id._is_eligible_for_early_discount(st_line.date):
amount_residual = aml.move_id.amount_total_signed
amount_residual_currency = -aml.move_id.invoice_early_pay_amount_after_discount * sign
discounted_balance = aml.amount_residual_currency - amount_residual_currency
if aml.move_id.move_type == 'out_invoice':
account = aml.company_id.account_journal_cash_discount_expense_id
else:
account = aml.company_id.account_journal_cash_discount_income_id
extra_aml_values['early_payment_vals'] = {
'name': _("%s - Early Payment Discount", aml.display_name),
'account': account,
'balance': discounted_balance,
}
amls_values_list.append({
'aml': aml,
'amount_residual': amount_residual,
'amount_residual_currency': amount_residual_currency,
**extra_aml_values,
})
kepts_amls_values_list = []
sum_amount_residual_currency = 0.0
if same_currency_mode:
for aml_values in amls_values_list:
if st_line_currency.compare_amounts(st_line_amount, -aml_values['amount_residual_currency']) == 0:
if st_line_currency.compare_amounts(st_line_amount, -aml.amount_residual_currency) == 0:
# Special case: the amounts are the same, submit the line directly.
kepts_amls_values_list = [aml_values]
kepts_amls = aml
break
if st_line_currency.compare_amounts(sign * (st_line_amount + sum_amount_residual_currency), 0.0) > 0:
elif st_line_currency.compare_amounts(sign * (st_line_amount + sum_aml_residual), 0.0) > 0:
# Here, we still have room for other candidates ; so we add the current one to the list we keep.
# Then, we continue iterating, even if there is no room anymore, just in case one of the following candidates
# is an exact match, which would then be preferred on the current candidates.
kepts_amls_values_list.append(aml_values)
sum_amount_residual_currency += aml_values['amount_residual_currency']
kepts_amls |= aml
sum_aml_residual += aml.amount_residual_currency
else:
kepts_amls = amls
# Try to match the amls having the same currency as the statement line.
if kepts_amls_values_list:
status = self._check_rule_propositions(st_line, kepts_amls_values_list)
result = _create_result_dict(kepts_amls_values_list, status)
if result:
return result
# We check the amount criteria of the reconciliation model, and select the
# kept_candidates if they pass the verification.
status = self._check_rule_propositions(st_line, kepts_amls)
if 'rejected' in status:
return None
# Try to match the whole candidates.
if amls:
status = self._check_rule_propositions(st_line, amls_values_list)
result = _create_result_dict(amls_values_list, status)
if result:
return result
rslt = {'amls': kepts_amls}
def _check_rule_propositions(self, st_line, amls_values_list):
if 'allow_write_off' in status and self.line_ids:
rslt['status'] = 'write_off'
if 'allow_auto_reconcile' in status and candidate_vals['allow_auto_reconcile'] and self.auto_reconcile:
rslt['auto_reconcile'] = True
return rslt
def _check_rule_propositions(self, st_line, amls):
""" Check restrictions that can't be handled for each move.line separately.
Note: Only used by models having a type equals to 'invoice_matching'.
:param st_line: The statement line.
:param amls_values_list: The candidates account.move.line as a list of dict:
* aml: The record.
* amount_residual: The amount residual to consider.
* amount_residual_currency: The amount residual in foreign currency to consider.
:param st_line: The statement line.
:param amls: The candidates account.move.line.
:return: A string representing what to do with the candidates:
* rejected: Reject candidates.
* allow_write_off: Allow to generate the write-off from the reconcile model lines if specified.
* allow_auto_reconcile: Allow to automatically reconcile entries if 'auto_validate' is enabled.
"""
self.ensure_one()
if not self.allow_payment_tolerance:
return {'allow_write_off', 'allow_auto_reconcile'}
if not amls:
return {'rejected'}
st_line_currency = st_line.foreign_currency_id or st_line.currency_id
currency = st_line.foreign_currency_id or st_line.currency_id
st_line_amount_curr = st_line._prepare_move_line_default_vals()[1]['amount_currency']
amls_amount_curr = sum(
st_line._prepare_counterpart_amounts_using_st_line_rate(
aml_values['aml'].currency_id,
aml_values['amount_residual'],
aml_values['amount_residual_currency'],
)['amount_currency']
for aml_values in amls_values_list
st_line._prepare_counterpart_amounts_using_st_line_rate(aml.currency_id, aml.amount_residual, aml.amount_residual_currency)['amount_currency']
for aml in amls
)
sign = 1 if st_line_amount_curr > 0.0 else -1
amount_curr_after_rec = sign * (amls_amount_curr + st_line_amount_curr)
# The statement line will be fully reconciled.
if st_line_currency.is_zero(amount_curr_after_rec):
if currency.is_zero(amount_curr_after_rec):
return {'allow_auto_reconcile'}
# The payment amount is higher than the sum of invoices.
+9 -34
View File
@@ -807,7 +807,8 @@ class AccountTax(models.Model):
'analytic_account_id': line_vals['analytic_account'].id if tax.analytic else False,
}
def _compute_taxes(self, base_lines, tax_lines=None, handle_price_include=True, include_caba_tags=False, early_payment_term=None):
def _compute_taxes(self, base_lines, tax_lines=None,
handle_price_include=True, include_caba_tags=False):
""" Generic method to compute the taxes for different business models.
:param base_lines: A list of python dictionaries created using the '_convert_to_tax_base_line_dict' method.
@@ -815,7 +816,6 @@ class AccountTax(models.Model):
:param handle_price_include: Manage the price-included taxes. If None, use the 'handle_price_include' key
set on base lines.
:param include_caba_tags: Manage tags for taxes being exigible on_payment.
:param early_payment_term: The payment term to consider for the early payment discount.
:return: A python dictionary containing:
The complete diff on tax lines if 'tax_lines' is passed as parameter:
@@ -852,15 +852,10 @@ class AccountTax(models.Model):
base_line_map = {}
for line_vals in base_lines:
orig_price_unit_after_discount = line_vals['price_unit'] * (1 - (line_vals['discount'] / 100.0))
price_unit_after_discount = orig_price_unit_after_discount
price_unit_after_discount = line_vals['price_unit'] * (1 - (line_vals['discount'] / 100.0))
taxes = line_vals['taxes']._origin
currency = line_vals['currency'] or self.env.company.currency_id
if early_payment_term and early_payment_term.has_early_payment:
remaining_part_to_consider = (100 - early_payment_term.percentage_to_discount) / 100.0
price_unit_after_discount = remaining_part_to_consider * price_unit_after_discount
if taxes:
if handle_price_include is None:
@@ -885,24 +880,6 @@ class AccountTax(models.Model):
'price_total': taxes_res['total_included'],
}
if early_payment_term \
and early_payment_term.has_early_payment \
and early_payment_term.discount_computation == 'excluded':
new_taxes_res = taxes.with_context(**line_vals['extra_context']).compute_all(
orig_price_unit_after_discount,
currency=currency,
quantity=line_vals['quantity'],
product=line_vals['product'],
partner=line_vals['partner'],
is_refund=line_vals['is_refund'],
handle_price_include=manage_price_include,
include_caba_tags=include_caba_tags,
)
for tax_res, new_taxes_res in zip(taxes_res['taxes'], new_taxes_res['taxes']):
delta_tax = new_taxes_res['amount'] - tax_res['amount']
tax_res['amount'] += delta_tax
to_update_vals['price_total'] += delta_tax
for tax_res in taxes_res['taxes']:
grouping_dict = self._get_generation_dict_from_base_line(line_vals, tax_res)
tax_res['grouping_dict'] = grouping_dict
@@ -1029,14 +1006,13 @@ class AccountTax(models.Model):
})
return tax_group_vals_list
def _prepare_tax_totals(self, base_lines, currency, tax_lines=None, early_payment_term=None):
def _prepare_tax_totals(self, base_lines, currency, tax_lines=None):
""" Compute the tax totals details for the business documents.
:param base_lines: A list of python dictionaries created using the '_convert_to_tax_base_line_dict' method.
:param currency: The currency set on the business document.
:param tax_lines: Optional list of python dictionaries created using the '_convert_to_tax_line_dict' method.
If specified, the taxes will be recomputed using them instead of recomputing the taxes on
the provided base lines.
:param early_payment_term: The payment term to consider for the early payment discount.
:param base_lines: A list of python dictionaries created using the '_convert_to_tax_base_line_dict' method.
:param currency: The currency set on the business document.
:param tax_lines: Optional list of python dictionaries created using the '_convert_to_tax_line_dict' method.
If specified, the taxes will be recomputed using them instead of recomputing the taxes on
the provided base lines.
:return: A dictionary in the following form:
{
'amount_total': The total amount to be displayed on the document, including every total
@@ -1086,7 +1062,6 @@ class AccountTax(models.Model):
tax_results = self._compute_taxes(
base_lines,
tax_lines=tax_lines,
early_payment_term=early_payment_term,
)
for base_line_vals, to_update in tax_results['base_lines_to_update']:
-30
View File
@@ -136,9 +136,6 @@ class AccountChartTemplate(models.Model):
default_cash_difference_expense_account_id = fields.Many2one('account.account.template', string="Cash Difference Expense Account")
default_pos_receivable_account_id = fields.Many2one('account.account.template', string="PoS receivable account")
account_journal_cash_discount_expense_id = fields.Many2one(comodel_name='account.account.template', string='Cash Discount Write-Off Expense Account',)
account_journal_cash_discount_income_id = fields.Many2one(comodel_name='account.account.template', string='Cash Discount Write-Off Income Account',)
property_account_receivable_id = fields.Many2one('account.account.template', string='Receivable Account')
property_account_payable_id = fields.Many2one('account.account.template', string='Payable Account')
property_account_expense_categ_id = fields.Many2one('account.account.template', string='Category of Expense Account')
@@ -200,24 +197,6 @@ class AccountChartTemplate(models.Model):
'company_id': company.id,
})
@api.model
def _create_cash_discount_expense_account(self, company, code_digits):
return self.env['account.account'].create({
'name': _("Cash Discount Expense Account"),
'code': 999997,
'account_type': 'expense',
'company_id': company.id,
})
@api.model
def _create_cash_discount_income_account(self, company, code_digits):
return self.env['account.account'].create({
'name': _("Cash Discount Income Account"),
'code': 999998,
'account_type': 'income_other',
'company_id': company.id,
})
def try_loading(self, company=False, install_demo=True):
""" Installs this chart of accounts for the current company if not chart
of accounts had been created for it yet.
@@ -332,13 +311,6 @@ class AccountChartTemplate(models.Model):
if not company.account_journal_suspense_account_id:
company.account_journal_suspense_account_id = self._create_liquidity_journal_suspense_account(company, self.code_digits)
# Set default cash discount write-off accounts
if not company.account_journal_cash_discount_expense_id:
company.account_journal_cash_discount_expense_id = self._create_cash_discount_expense_account(company, self.code_digits)
if not company.account_journal_cash_discount_income_id:
company.account_journal_cash_discount_income_id = self._create_cash_discount_income_account(company, self.code_digits)
if not company.account_journal_payment_debit_account_id:
company.account_journal_payment_debit_account_id = self.env['account.account'].create({
'name': _("Outstanding Receipts"),
@@ -645,8 +617,6 @@ class AccountChartTemplate(models.Model):
'account_default_pos_receivable_account_id': self.default_pos_receivable_account_id,
'income_currency_exchange_account_id': self.income_currency_exchange_account_id,
'expense_currency_exchange_account_id': self.expense_currency_exchange_account_id,
'account_journal_cash_discount_expense_id': self.account_journal_cash_discount_expense_id,
'account_journal_cash_discount_income_id': self.account_journal_cash_discount_income_id,
}
values = {}
+1 -6
View File
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
import calendar
from datetime import timedelta, datetime, date
import calendar
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError, UserError, RedirectWarning
from odoo.tools.float_utils import float_round, float_is_zero
from odoo.tools.mail import is_html_empty
from odoo.tools.misc import format_date
from odoo.tools.float_utils import float_round, float_is_zero
@@ -66,10 +65,6 @@ class ResCompany(models.Model):
account_journal_suspense_account_id = fields.Many2one('account.account', string='Journal Suspense Account')
account_journal_payment_debit_account_id = fields.Many2one('account.account', string='Journal Outstanding Receipts Account')
account_journal_payment_credit_account_id = fields.Many2one('account.account', string='Journal Outstanding Payments Account')
account_journal_cash_discount_income_id = fields.Many2one(comodel_name='account.account', string='Cash Discount Write-Off Income Account')
account_journal_cash_discount_expense_id = fields.Many2one(comodel_name='account.account', string='Cash Discount Write-Off Expense Account')
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")
@@ -159,21 +159,6 @@ class ResConfigSettings(models.TransientModel):
# Quick encoding (fiduciary mode)
quick_edit_mode = fields.Selection(string="Quick encoding", readonly=False, related='company_id.quick_edit_mode')
account_journal_cash_discount_income_id = fields.Many2one(
comodel_name='account.account',
string='Cash Discount Write-Off Income Account',
readonly=False,
related='company_id.account_journal_cash_discount_income_id',
domain="[('deprecated', '=', False), ('company_id', '=', company_id), \
('account_type', 'in', ('income', 'income_other'))]")
account_journal_cash_discount_expense_id = fields.Many2one(
comodel_name='account.account',
string='Cash Discount Write-Off Expense Account',
readonly=False,
related='company_id.account_journal_cash_discount_expense_id',
domain="[('deprecated', '=', False), ('company_id', '=', company_id), \
('account_type', '=', 'expense')]")
def set_values(self):
super().set_values()
# install a chart of accounts for the given company (if required)
-1
View File
@@ -36,4 +36,3 @@ from . import test_account_incoming_supplier_invoice
from . import test_payment_term
from . import test_account_payment_register
from . import test_tour
from . import test_early_payment_discount
@@ -1,316 +0,0 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo import fields, Command
@tagged('post_install', '-at_install')
class TestAccountEarlyPaymentDiscount(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# Payment Terms
cls.default_pay_term_cash_discount = cls.env['account.payment.term'].create({
'name': 'Default Early Payment Cash Discount Payment Term',
'has_early_payment': True,
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 0,
'option': 'day_after_invoice_date',
}),
],
})
cls.pay_term_cash_discount_10_percent_10_days_tax_inc = cls.env['account.payment.term'].create({
'name': '10% reduction if payment within 10 days, tax included',
'has_early_payment': True,
'discount_days': 10,
'percentage_to_discount': 10,
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 0,
'option': 'day_after_invoice_date',
}),
],
})
cls.pay_term_cash_discount_10_percent_10_days_tax_excl = cls.env['account.payment.term'].create({
'name': '10% reduction if payment within 10 days, tax excluded',
'has_early_payment': True,
'discount_days': 10,
'percentage_to_discount': 10,
'discount_computation': 'excluded',
'line_ids': [
(0, 0, {
'value': 'balance',
'days': 0,
'option': 'day_after_invoice_date',
}),
],
})
# Invoices (account move tests)
cls.inv_1200_10_percents_discount_no_tax = cls.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': cls.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [(0, 0, {
'name': 'line',
'price_unit': 1200.0,
'tax_ids': []
})],
'invoice_payment_term_id': cls.pay_term_cash_discount_10_percent_10_days_tax_inc.id,
})
cls.inv_1500_10_percents_discount_tax_incl_15_percents_tax = cls.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': cls.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [(0, 0, {
'name': 'line',
'price_unit': 1500.0,
'tax_ids': [Command.set(cls.product_a.taxes_id.ids)],# 15%
})],
'invoice_payment_term_id': cls.pay_term_cash_discount_10_percent_10_days_tax_inc.id,
})
cls.inv_1500_10_percents_discount_tax_excl_15_percents_tax = cls.env['account.move'].create({
'move_type': 'in_invoice',
'partner_id': cls.partner_a.id,
'invoice_date': '2019-01-01',
'date': '2019-01-01',
'invoice_line_ids': [(0, 0, {
'name': 'line',
'price_unit': 1500.0,
'tax_ids': [Command.set(cls.product_a.taxes_id.ids)],# 15%
})],
'invoice_payment_term_id': cls.pay_term_cash_discount_10_percent_10_days_tax_excl.id,
})
# Invoices (Payment Register & reconciliation tests)
# -- Customer invoices sharing the same batch
cls.out_invoice_1 = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 1000.0, 'tax_ids': []})],
'invoice_payment_term_id': cls.default_pay_term_cash_discount.id,
})
cls.out_invoice_2 = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_a.id,
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 2000.0, 'tax_ids': []})],
'invoice_payment_term_id': cls.default_pay_term_cash_discount.id,
})
#--Customer invoices from a different batch
cls.out_invoice_3 = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_b.id,
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 3000.0, 'tax_ids': []})],
'invoice_payment_term_id': cls.default_pay_term_cash_discount.id,
})
cls.out_invoice_4_no_discount = cls.env['account.move'].create({
'move_type': 'out_invoice',
'date': '2017-01-01',
'invoice_date': '2017-01-01',
'partner_id': cls.partner_b.id,
'currency_id': cls.currency_data['currency'].id,
'invoice_line_ids': [(0, 0, {'product_id': cls.product_a.id, 'price_unit': 5000.0, 'tax_ids': []})],
'invoice_payment_term_id': None,
})
(cls.out_invoice_1 + cls.out_invoice_2 + cls.out_invoice_3 + cls.out_invoice_4_no_discount).action_post()
# Taxes
cls.tax_10 = cls.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10.0,
})
cls.tax_20 = cls.env['account.tax'].create({
'name': "tax_20",
'amount_type': 'percent',
'amount': 20.0,
})
lines_data = [
(1100, cls.tax_10 + cls.tax_20),
(1100, cls.tax_10),
(1000, cls.tax_20),
]
invoice_lines_vals = [
(0, 0, {
'name': 'line',
'account_id': cls.company_data['default_account_revenue'].id,
'price_unit': amount,
'tax_ids': [(6, 0, taxes.ids)],
})
for amount, taxes in lines_data]
cls.taxed_invoice = cls.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': cls.partner_a.id,
'invoice_line_ids': invoice_lines_vals,
'invoice_payment_term_id': cls.pay_term_cash_discount_10_percent_10_days_tax_inc.id,
})
# ========================== Tests Payment Terms ==========================
def test_early_payment_assert_default_values(self):
# Check that the early discount payment term is correctly set by default
self.assertRecordValues(self.default_pay_term_cash_discount, [{
'discount_computation': 'included',
'discount_days': 7,
'percentage_to_discount': 2.0
}])
def test_early_payment_end_date(self):
# Check that the last date of the cash discount availability is correctly computed.
# The default time delay is 7 days after the invoice.
def assertEarlyDiscountPaymentTermDate(pay_term, move_date, expected_date):
self.assertEqual(
pay_term._get_last_date_for_discount(fields.Date.from_string(move_date)),
fields.Date.from_string(expected_date)
)
assertEarlyDiscountPaymentTermDate(self.default_pay_term_cash_discount, '2022-01-01', '2022-01-08')
assertEarlyDiscountPaymentTermDate(self.default_pay_term_cash_discount, '2022-01-31', '2022-02-07')
assertEarlyDiscountPaymentTermDate(self.default_pay_term_cash_discount, '2022-02-25', '2022-03-04')
# ========================== Tests Account Move ==========================
def test_early_pay_reduced_amounts(self):
# Check that the monetary amounts after the discount are correct.
# 1200 invoice, no tax, 10% discount
self.assertRecordValues(self.inv_1200_10_percents_discount_no_tax, [{
'invoice_early_pay_amount_after_discount': 1080.00,
}])
# 1500 invoice, 15% tax, 10% discount including taxes
self.assertRecordValues(self.inv_1500_10_percents_discount_tax_incl_15_percents_tax, [{
'invoice_early_pay_amount_after_discount': 1552.50,
}])
# 1500 invoice, 15% tax, 10% discount excluding taxes
self.assertRecordValues(self.inv_1500_10_percents_discount_tax_excl_15_percents_tax, [{
'invoice_early_pay_amount_after_discount': 1575.0,
}])
# ========================== Tests Payment Register ==========================
def test_register_discounted_payment_on_single_invoice(self):
active_ids = self.out_invoice_1.ids
payments = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=active_ids).create({
'payment_date': '2017-01-01',
'early_pay_discount_toggle_button': True,
})._create_payments()
self.assertTrue(payments.is_reconciled)
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -1000.0},
{
'account_id': self.env.company['account_journal_cash_discount_expense_id'].id,
'amount_currency': 20.0,
},
{'amount_currency': 980.0},
])
def test_register_discounted_payment_on_batched_invoice(self):
active_ids = (self.out_invoice_1 + self.out_invoice_2).ids
payment_register = self.env['account.payment.register']\
.with_context(active_model='account.move', active_ids=active_ids)\
.create({
'payment_date': '2017-01-01',
'early_pay_discount_toggle_button': True,
})
payments = payment_register._create_payments()
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -2000.0},
{'amount_currency': -1000.0},
{
'account_id': self.env.company['account_journal_cash_discount_expense_id'].id,
'amount_currency': 20.0,
},
{
'account_id': self.env.company['account_journal_cash_discount_expense_id'].id,
'amount_currency': 40.0,
},
{'amount_currency': 980.0},
{'amount_currency': 1960.0},
])
self.assertTrue(all(p.is_reconciled for p in payments))
def test_register_discounted_payment_on_non_batched_invoice(self):
active_ids = (self.out_invoice_1 + self.out_invoice_2 + self.out_invoice_3).ids
payment_register = self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=active_ids) \
.create({
'payment_date': '2017-01-01',
'early_pay_discount_toggle_button': True,
})
payments = payment_register._create_payments()
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -3000.0},
{'amount_currency': -2000.0},
{'amount_currency': -1000.0},
{
'account_id': self.env.company['account_journal_cash_discount_expense_id'].id,
'amount_currency': 20.0,
},
{
'account_id': self.env.company['account_journal_cash_discount_expense_id'].id,
'amount_currency': 40.0,
},
{
'account_id': self.env.company['account_journal_cash_discount_expense_id'].id,
'amount_currency': 60.0,
},
{'amount_currency': 980.0},
{'amount_currency': 1960.0},
{'amount_currency': 2940.0},
])
self.assertTrue(all(p.is_reconciled for p in payments))
def test_register_discounted_payment_on_non_batched_invoice_one_no_discount(self):
active_ids = (self.out_invoice_3 + self.out_invoice_4_no_discount).ids
payment_register = self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=active_ids) \
.create({
'payment_date': '2017-01-01',
'early_pay_discount_toggle_button': True,
})
payments = payment_register._create_payments()
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -5000.0},
{'amount_currency': -3000.0},
{
'account_id': self.env.company['account_journal_cash_discount_expense_id'].id,
'amount_currency': 60.0,
},
{'amount_currency': 2940.0},
{'amount_currency': 5000.0},
])
self.assertTrue(all(p.is_reconciled for p in payments))
def test_register_payment_deactivate_discount(self):
active_ids = self.out_invoice_3.ids
payment_register = self.env['account.payment.register'] \
.with_context(active_model='account.move', active_ids=active_ids) \
.create({
'payment_date': '2017-01-01',
'early_pay_discount_toggle_button': False,
})
payments = payment_register._create_payments()
self.assertRecordValues(payments.line_ids.sorted('balance'), [
{'amount_currency': -3000.0},
{'amount_currency': 3000.0},
])
self.assertTrue(payments.is_reconciled)
def test_early_payment_taxes_computation(self):
result = self.taxed_invoice._get_report_early_payment_totals_values()
self.assertEqual(result['amount_total'], 3456)
self.assertEqual(result['amount_untaxed'], 2880)
self.assertEqual(result['groups_by_subtotal']['Untaxed Amount'][0]['tax_group_amount'], 576)
@@ -234,7 +234,6 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
def _check_statement_matching(self, rules, expected_values_list):
for statement_line, expected_values in expected_values_list.items():
res = rules._apply_rules(statement_line, statement_line._retrieve_partner())
res.pop('amls_values_list', None)
self.assertDictEqual(res, expected_values)
def test_matching_fields(self):
@@ -259,26 +258,20 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
match_text_location_reference=False,
match_text_location_note=False,
)
rslt = rule._apply_rules(st_line, st_line._retrieve_partner())
rslt.pop('amls_values_list')
self.assertDictEqual(
rslt,
rule._apply_rules(st_line, st_line._retrieve_partner()),
{'amls': inv1, 'model': rule},
)
rule.match_text_location_reference = True
rslt = rule._apply_rules(st_line, st_line._retrieve_partner())
rslt.pop('amls_values_list')
self.assertDictEqual(
rslt,
rule._apply_rules(st_line, st_line._retrieve_partner()),
{'amls': inv2, 'model': rule},
)
rule.match_text_location_note = True
rslt = rule._apply_rules(st_line, st_line._retrieve_partner())
rslt.pop('amls_values_list')
self.assertDictEqual(
rslt,
rule._apply_rules(st_line, st_line._retrieve_partner()),
{'amls': inv3, 'model': rule},
)
@@ -102,48 +102,6 @@
The last line's computation type should be "Balance" to ensure that the whole amount will be allocated.
</p>
<field name="line_ids"/>
<!-- ============================== Early payment section ============================== -->
<field name="early_payment_applicable" invisible="1"/>
<div attrs="{'invisible':[('early_payment_applicable','=',False)]}">
<field name="has_early_payment"/>
<label for="has_early_payment"/>
<div attrs="{'invisible':[('has_early_payment','=',False)]}">
<div class="row mt-2">
<div class="col-2">
<strong><label for="percentage_to_discount"/></strong>
</div>
<div class="col-1">
<field name="percentage_to_discount"/>
</div>
<div class="col-1 oe_inline">
<span>%</span>
</div>
</div>
<div class="row">
<div class="col-2">
<strong><label for="discount_computation"/></strong>
</div>
<div class="col-4">
<field name="discount_computation"/>
</div>
</div>
<div class="row">
<div class="col-2">
<strong><label for="discount_days"/></strong>
</div>
<div class="col-1 pe-0">
Within
</div>
<div class="col-1 px-0">
<field name="discount_days"/>
</div>
<div class="col-3 px-0">
days of the invoice date
</div>
</div>
</div>
</div>
</sheet>
</form>
</field>
+1 -23
View File
@@ -197,29 +197,7 @@
</div>
</div>
</div>
<t t-set="early_pay_totals" t-value="o._get_report_early_payment_totals_values()"/>
<t t-if="early_pay_totals">
<div>
<p>
<strong>
Early Payment (before <span t-esc="o.invoice_payment_term_id._get_last_date_for_discount(o.invoice_date)"/>):
<span t-esc="early_pay_totals['formatted_amount_total']"/>
(<span t-esc="o.invoice_payment_term_id.percentage_to_discount"/> % discount)
</strong>
</p>
</div>
<div id="total-early" class="row">
<div t-attf-class="#{'col-6' if report_type != 'html' else 'col-sm-7 col-md-6'}">
<table class="table table-sm" style="page-break-inside: avoid;">
<t t-call="account.document_tax_totals">
<t t-set="tax_totals" t-value="early_pay_totals"/>
</t>
</table>
</div>
</div>
</t>
<p t-if="o.move_type in ('out_invoice', 'in_refund') and o.payment_reference" name="payment_communication" class="mt-4">
<p t-if="o.move_type in ('out_invoice', 'in_refund') and o.payment_reference" name="payment_communication">
Please use the following communication for your payment : <b><span t-field="o.payment_reference"/></b>
</p>
<div t-if="o.invoice_payment_term_id" name="payment_term">
@@ -527,14 +527,6 @@
<label for="transfer_account_id" class="col-lg-5 o_light_label"/>
<field name="transfer_account_id"/>
</div>
<div class="row mt8">
<label for="account_journal_cash_discount_income_id" class="col-lg-5 o_light_label"/>
<field name="account_journal_cash_discount_income_id"/>
</div>
<div class="row mt8">
<label for="account_journal_cash_discount_expense_id" class="col-lg-5 o_light_label"/>
<field name="account_journal_cash_discount_expense_id"/>
</div>
</div>
</div>
</div>
+17 -122
View File
@@ -81,12 +81,6 @@ class AccountPaymentRegister(models.TransientModel):
partner_id = fields.Many2one('res.partner',
string="Customer/Vendor", store=True, copy=False, ondelete='restrict',
compute='_compute_from_lines')
discounted_amount = fields.Monetary(
store=True,
copy=False,
currency_field='source_currency_id',
compute='_compute_from_lines',
)
# == Payment methods fields ==
payment_method_line_id = fields.Many2one('account.payment.method.line', string='Payment Method',
@@ -104,29 +98,15 @@ class AccountPaymentRegister(models.TransientModel):
# == Payment difference fields ==
payment_difference = fields.Monetary(
compute='_compute_payment_difference')
payment_difference_handling = fields.Selection(
string="Payment Difference Handling",
selection=[('open', 'Keep open'), ('reconcile', 'Mark as fully paid')],
compute='_compute_payment_difference_handling',
store=True,
readonly=False,
)
writeoff_account_id = fields.Many2one(
comodel_name='account.account',
string="Difference Account",
copy=False,
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]",
compute='_compute_writeoff_account_id',
store=True,
readonly=False,
)
writeoff_label = fields.Char(
string='Journal Item Label',
compute='_compute_writeoff_label',
store=True,
readonly=False,
help='Change label of the counterpart that will hold the payment difference',
)
payment_difference_handling = fields.Selection([
('open', 'Keep open'),
('reconcile', 'Mark as fully paid'),
], default='open', string="Payment Difference Handling")
writeoff_account_id = fields.Many2one('account.account', string="Difference Account", copy=False,
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]")
writeoff_label = fields.Char(string='Journal Item Label', default='Write-Off',
help='Change label of the counterpart that will hold the payment difference')
# == Display purpose fields ==
show_partner_bank_account = fields.Boolean(
compute='_compute_show_require_partner_bank') # Used to know whether the field `partner_bank_id` should be displayed
@@ -134,18 +114,10 @@ class AccountPaymentRegister(models.TransientModel):
compute='_compute_show_require_partner_bank') # used to know whether the field `partner_bank_id` should be required
country_code = fields.Char(related='company_id.account_fiscal_country_id.code', readonly=True)
# == Early Payment Discount ==
show_early_pay_discount_button = fields.Boolean(compute="_compute_show_early_pay_discount_button")
early_pay_discount_toggle_button = fields.Boolean(
string="Early Payment Discounts",
compute='_compute_early_pay_discount_toggle_button',
readonly=False,
store=True,
)
# -------------------------------------------------------------------------
# HELPERS
# -------------------------------------------------------------------------
@api.model
def _get_batch_communication(self, batch_result):
''' Helper to compute the communication based on the batch.
@@ -233,7 +205,6 @@ class AccountPaymentRegister(models.TransientModel):
partner_bank_account = self.env['res.partner.bank']
if move.is_invoice(include_receipts=True):
partner_bank_account = move.partner_bank_id._origin
early_discount_pay_term = move.invoice_payment_term_id if move._is_eligible_for_early_discount(self.payment_date) else False
return {
'partner_id': line.partner_id.id,
@@ -241,7 +212,6 @@ class AccountPaymentRegister(models.TransientModel):
'currency_id': line.currency_id.id,
'partner_bank_id': partner_bank_account.id,
'partner_type': 'customer' if line.account_type == 'asset_receivable' else 'supplier',
'early_discount_pay_term': early_discount_pay_term,
}
def _get_batches(self):
@@ -320,15 +290,13 @@ class AccountPaymentRegister(models.TransientModel):
payment_values = batch_result['payment_values']
lines = batch_result['lines']
company = lines[0].company_id
source_amount = abs(sum(lines.mapped('amount_residual')))
if payment_values['currency_id'] == company.currency_id.id:
source_amount_currency = source_amount
else:
source_amount_currency = abs(sum(lines.mapped('amount_residual_currency')))
if payment_values['early_discount_pay_term']:
early_pay_discounted_amount = abs(sum(lines.move_id.mapped('invoice_early_pay_amount_after_discount')))
else:
early_pay_discounted_amount = False
return {
'company_id': company.id,
'partner_id': payment_values['partner_id'],
@@ -337,14 +305,13 @@ class AccountPaymentRegister(models.TransientModel):
'source_currency_id': payment_values['currency_id'],
'source_amount': source_amount,
'source_amount_currency': source_amount_currency,
'discounted_amount': early_pay_discounted_amount
}
# -------------------------------------------------------------------------
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('line_ids', 'payment_date')
@api.depends('line_ids')
def _compute_from_lines(self):
''' Load initial values from the account.moves passed through the context. '''
for wizard in self:
@@ -368,7 +335,6 @@ class AccountPaymentRegister(models.TransientModel):
'source_currency_id': False,
'source_amount': False,
'source_amount_currency': False,
'discounted_amount': False
})
wizard.can_edit_wizard = False
@@ -394,51 +360,6 @@ class AccountPaymentRegister(models.TransientModel):
else:
wizard.group_payment = False
@api.depends('can_edit_wizard', 'payment_date', 'currency_id', 'amount')
def _compute_payment_difference_handling(self):
for wizard in self:
if not wizard.payment_difference_handling:
wizard.payment_difference_handling = 'open'
if wizard.can_edit_wizard:
batch_result = wizard._get_batches()[0]
if batch_result['payment_values']['early_discount_pay_term']:
wizard.payment_difference_handling = 'reconcile'
else:
wizard.payment_difference_handling = 'open'
@api.depends('payment_difference_handling', 'can_edit_wizard', 'amount', 'discounted_amount')
def _compute_writeoff_account_id(self):
for wizard in self:
wizard.writeoff_account_id = None
if wizard.can_edit_wizard:
batch_result = wizard._get_batches()[0]
cash_discount_accounts = wizard.company_id.account_journal_cash_discount_income_id +\
wizard.company_id.account_journal_cash_discount_expense_id
if wizard.writeoff_account_id and wizard.writeoff_account_id not in cash_discount_accounts:
continue
if batch_result['payment_values']['early_discount_pay_term'] and wizard.amount == wizard.discounted_amount:
wizard.writeoff_account_id = self._get_early_pay_cash_discount_account(batch_result['payment_values'])
def _get_early_pay_cash_discount_account(self, payment_values):
self.ensure_one()
if payment_values['payment_type'] == 'inbound':
writeoff_account_id = self.company_id.account_journal_cash_discount_income_id
else:
writeoff_account_id = self.company_id.account_journal_cash_discount_expense_id
return writeoff_account_id
@api.depends('payment_difference_handling', 'can_edit_wizard', 'amount', 'discounted_amount')
def _compute_writeoff_label(self):
for wizard in self:
if wizard.writeoff_label and wizard.writeoff_label not in [_('Write-Off'), _('Early Payment Discount')]:
continue
if wizard.can_edit_wizard:
batch_result = wizard._get_batches()[0]
if batch_result['payment_values']['early_discount_pay_term'] and wizard.amount == wizard.discounted_amount:
wizard.writeoff_label = _("Early Payment Discount")
else:
wizard.writeoff_label = _('Write-Off')
@api.depends('journal_id')
def _compute_currency_id(self):
for wizard in self:
@@ -564,25 +485,16 @@ class AccountPaymentRegister(models.TransientModel):
self.payment_date,
)
@api.depends('early_pay_discount_toggle_button', 'discounted_amount', 'can_edit_wizard', 'source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date')
@api.depends('can_edit_wizard', 'source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', 'payment_date')
def _compute_amount(self):
for wizard in self:
if wizard.source_currency_id and wizard.can_edit_wizard:
batch_result = wizard._get_batches()[0]
wizard.amount = wizard._get_total_amount_in_wizard_currency_to_full_reconcile(batch_result)
if not wizard.currency_id.is_zero(wizard.discounted_amount) and wizard.early_pay_discount_toggle_button:
wizard.amount = wizard.discounted_amount
else:
# The wizard is not editable so no partial payment allowed and then, 'amount' is not used.
wizard.amount = None
@api.depends('can_edit_wizard')
def _compute_early_pay_discount_toggle_button(self):
for wizard in self:
batches = wizard._get_batches()
for batch in batches:
wizard.early_pay_discount_toggle_button = batch['payment_values']['payment_type'] == 'outbound'
@api.depends('can_edit_wizard', 'amount')
def _compute_payment_difference(self):
for wizard in self:
@@ -593,14 +505,6 @@ class AccountPaymentRegister(models.TransientModel):
else:
wizard.payment_difference = 0.0
@api.depends('can_edit_wizard')
def _compute_show_early_pay_discount_button(self):
for wizard in self:
wizard.show_early_pay_discount_button = False
for wizard in self:
batches = wizard._get_batches()
wizard.show_early_pay_discount_button = any(batch['payment_values']['early_discount_pay_term'] for batch in batches)
# -------------------------------------------------------------------------
# LOW-LEVEL METHODS
# -------------------------------------------------------------------------
@@ -685,6 +589,7 @@ class AccountPaymentRegister(models.TransientModel):
def _create_payment_vals_from_batch(self, batch_result):
batch_values = self._get_wizard_values_from_batch(batch_result)
if batch_values['payment_type'] == 'inbound':
partner_bank_id = self.journal_id.bank_account_id.id
else:
@@ -695,12 +600,9 @@ class AccountPaymentRegister(models.TransientModel):
if batch_values['payment_type'] != payment_method_line.payment_type:
payment_method_line = self.journal_id._get_available_payment_method_lines(batch_values['payment_type'])[:1]
early_pay_applicable = not self.currency_id.is_zero(
batch_values['discounted_amount']) and self.early_pay_discount_toggle_button
payment_vals = {
return {
'date': self.payment_date,
'amount': batch_values['source_amount_currency'] if not early_pay_applicable else batch_values[
'discounted_amount'],
'amount': batch_values['source_amount_currency'],
'payment_type': batch_values['payment_type'],
'partner_type': batch_values['partner_type'],
'ref': self._get_batch_communication(batch_result),
@@ -711,13 +613,6 @@ class AccountPaymentRegister(models.TransientModel):
'payment_method_line_id': payment_method_line.id,
'destination_account_id': batch_result['lines'][0].account_id.id
}
if early_pay_applicable:
payment_vals['write_off_line_vals'] = {
'name': _("Early payment discount"),
'amount': batch_values['source_amount_currency'] - batch_values['discounted_amount'],
'account_id': self._get_early_pay_cash_discount_account(payment_vals).id,
}
return payment_vals
def _init_payments(self, to_process, edit_mode=False):
""" Create the payments.
@@ -26,8 +26,6 @@
<field name="available_payment_method_line_ids" invisible="1"/>
<field name="available_partner_bank_ids" invisible="1"/>
<field name="company_currency_id" invisible="1"/>
<field name="show_early_pay_discount_button" invisible="1"/>
<field name="discounted_amount" invisible="1"/>
<group>
<group name="group1">
@@ -50,10 +48,7 @@
required="1"
options="{'no_create': True, 'no_open': True}"
groups="base.group_multi_currency"/>
</div>
<field name="early_pay_discount_toggle_button"
attrs="{'invisible':[('show_early_pay_discount_button', '=', False)]}"/>
<field name="payment_date"/>
<field name="communication"
attrs="{'invisible': ['|', ('can_edit_wizard', '=', False), '&amp;', ('can_group_payments', '=', True), ('group_payment', '=', False)]}"/>