[IMP] account: cash discount improvements

Purpose :
Improve multiple use cases while handling the Accounting part of Cash Discounts in Odoo.

When creating a Payment Term, the user can now specifically design an early payment discount through an additional part in the wizard.
This will, amongst other things, allow the user to choose in which way the tax should be computed on the discounted invoice (tax included/excluded).

The discounted amount and the conditions to fulfill are printed below the invoice PDF, as well as the details.

Registering a payment while filling the condition for an early payment discount will prefill the fields accordingly.

Reconciliation --> In the reconciliation widget, the logic of Early Payments Discounts are also applied.

Related : odoo/enterprise#28607
Task:2729038

Part-of: odoo/odoo#94050
This commit is contained in:
Camille Spiritus
2022-08-17 19:48:08 +02:00
parent 5060204e49
commit 7482aff6d7
15 changed files with 791 additions and 70 deletions
+51
View File
@@ -444,6 +444,12 @@ 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',
@@ -1155,6 +1161,21 @@ 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:
@@ -1273,6 +1294,36 @@ 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,6 +4,7 @@ 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):
@@ -21,6 +22,17 @@ 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:
@@ -95,6 +107,32 @@ 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,74 +809,126 @@ 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']
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
amls_values_list = []
same_currency_mode = amls.currency_id == st_line_currency
for aml in amls:
extra_aml_values = {}
kepts_amls = self.env['account.move.line']
sum_aml_residual = 0.0
for aml in amls:
amount_residual = aml.amount_residual
amount_residual_currency = aml.amount_residual_currency
if st_line_currency.compare_amounts(st_line_amount, -aml.amount_residual_currency) == 0:
# 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:
# Special case: the amounts are the same, submit the line directly.
kepts_amls = aml
kepts_amls_values_list = [aml_values]
break
elif st_line_currency.compare_amounts(sign * (st_line_amount + sum_aml_residual), 0.0) > 0:
if st_line_currency.compare_amounts(sign * (st_line_amount + sum_amount_residual_currency), 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 |= aml
sum_aml_residual += aml.amount_residual_currency
else:
kepts_amls = amls
kepts_amls_values_list.append(aml_values)
sum_amount_residual_currency += aml_values['amount_residual_currency']
# 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 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
rslt = {'amls': kepts_amls}
# 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
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):
def _check_rule_propositions(self, st_line, amls_values_list):
""" 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: The candidates account.move.line.
: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.
: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'}
currency = st_line.foreign_currency_id or st_line.currency_id
st_line_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.currency_id, aml.amount_residual, aml.amount_residual_currency)['amount_currency']
for aml in amls
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
)
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 currency.is_zero(amount_curr_after_rec):
if st_line_currency.is_zero(amount_curr_after_rec):
return {'allow_auto_reconcile'}
# The payment amount is higher than the sum of invoices.
+34 -9
View File
@@ -807,8 +807,7 @@ 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):
def _compute_taxes(self, base_lines, tax_lines=None, handle_price_include=True, include_caba_tags=False, early_payment_term=None):
""" 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.
@@ -816,6 +815,7 @@ 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,10 +852,15 @@ class AccountTax(models.Model):
base_line_map = {}
for line_vals in base_lines:
price_unit_after_discount = line_vals['price_unit'] * (1 - (line_vals['discount'] / 100.0))
orig_price_unit_after_discount = line_vals['price_unit'] * (1 - (line_vals['discount'] / 100.0))
price_unit_after_discount = orig_price_unit_after_discount
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:
@@ -880,6 +885,24 @@ 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
@@ -1006,13 +1029,14 @@ class AccountTax(models.Model):
})
return tax_group_vals_list
def _prepare_tax_totals(self, base_lines, currency, tax_lines=None):
def _prepare_tax_totals(self, base_lines, currency, tax_lines=None, early_payment_term=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 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.
:return: A dictionary in the following form:
{
'amount_total': The total amount to be displayed on the document, including every total
@@ -1062,6 +1086,7 @@ 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,6 +136,9 @@ 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')
@@ -197,6 +200,24 @@ 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.
@@ -311,6 +332,13 @@ 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"),
@@ -617,6 +645,8 @@ 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 = {}
+6 -2
View File
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
from datetime import timedelta, datetime, date
import calendar
from dateutil.relativedelta import relativedelta
from datetime import timedelta, datetime, date
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,6 +66,10 @@ 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")
@@ -161,6 +161,21 @@ 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
@@ -35,3 +35,4 @@ 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
@@ -0,0 +1,316 @@
# -*- 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,6 +234,7 @@ 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):
@@ -258,20 +259,26 @@ 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(
rule._apply_rules(st_line, st_line._retrieve_partner()),
rslt,
{'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(
rule._apply_rules(st_line, st_line._retrieve_partner()),
rslt,
{'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(
rule._apply_rules(st_line, st_line._retrieve_partner()),
rslt,
{'amls': inv3, 'model': rule},
)
@@ -102,6 +102,48 @@
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>
+23 -1
View File
@@ -197,7 +197,29 @@
</div>
</div>
</div>
<p t-if="o.move_type in ('out_invoice', 'in_refund') and o.payment_reference" name="payment_communication">
<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">
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,6 +527,14 @@
<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>
+123 -18
View File
@@ -3,7 +3,7 @@ from collections import defaultdict
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from odoo.tools import float_compare, frozendict
from odoo.tools import frozendict
class AccountPaymentRegister(models.TransientModel):
@@ -81,6 +81,12 @@ 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',
@@ -98,15 +104,29 @@ class AccountPaymentRegister(models.TransientModel):
# == Payment difference fields ==
payment_difference = fields.Monetary(
compute='_compute_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')
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',
)
# == 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
@@ -114,10 +134,18 @@ 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.
@@ -205,6 +233,7 @@ 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,
@@ -212,6 +241,7 @@ 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):
@@ -290,13 +320,15 @@ 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'],
@@ -305,13 +337,14 @@ 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')
@api.depends('line_ids', 'payment_date')
def _compute_from_lines(self):
''' Load initial values from the account.moves passed through the context. '''
for wizard in self:
@@ -335,6 +368,7 @@ class AccountPaymentRegister(models.TransientModel):
'source_currency_id': False,
'source_amount': False,
'source_amount_currency': False,
'discounted_amount': False
})
wizard.can_edit_wizard = False
@@ -360,6 +394,51 @@ 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:
@@ -485,16 +564,25 @@ class AccountPaymentRegister(models.TransientModel):
self.payment_date,
)
@api.depends('can_edit_wizard', 'source_amount', 'source_amount_currency', 'source_currency_id', 'company_id', 'currency_id', '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')
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:
@@ -505,6 +593,14 @@ 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
# -------------------------------------------------------------------------
@@ -589,7 +685,6 @@ 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:
@@ -600,9 +695,12 @@ 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]
return {
early_pay_applicable = not self.currency_id.is_zero(
batch_values['discounted_amount']) and self.early_pay_discount_toggle_button
payment_vals = {
'date': self.payment_date,
'amount': batch_values['source_amount_currency'],
'amount': batch_values['source_amount_currency'] if not early_pay_applicable else batch_values[
'discounted_amount'],
'payment_type': batch_values['payment_type'],
'partner_type': batch_values['partner_type'],
'ref': self._get_batch_communication(batch_result),
@@ -613,6 +711,13 @@ 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,6 +26,8 @@
<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">
@@ -48,7 +50,10 @@
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)]}"/>