diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index e2baf6ee0b4..5d6f70d3277 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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). diff --git a/addons/account/models/account_payment_term.py b/addons/account/models/account_payment_term.py index c7d526c5b06..48adc657fc0 100644 --- a/addons/account/models/account_payment_term.py +++ b/addons/account/models/account_payment_term.py @@ -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" diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 0d2d67f8e39..9c7dff3f4ae 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -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. diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index 879cfc6d72b..29fc18354a0 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -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']: diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index a95f2b395c0..bed7c82425a 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -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 = {} diff --git a/addons/account/models/company.py b/addons/account/models/company.py index c1579671df3..7bf66d19c62 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -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") diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 54cd92d4978..686a143783b 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -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) diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index 0f599db0c0b..65a5440755a 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -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 diff --git a/addons/account/tests/test_early_payment_discount.py b/addons/account/tests/test_early_payment_discount.py new file mode 100644 index 00000000000..1d8878e1381 --- /dev/null +++ b/addons/account/tests/test_early_payment_discount.py @@ -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) diff --git a/addons/account/tests/test_reconciliation_matching_rules.py b/addons/account/tests/test_reconciliation_matching_rules.py index e29fc59043e..125420fdede 100644 --- a/addons/account/tests/test_reconciliation_matching_rules.py +++ b/addons/account/tests/test_reconciliation_matching_rules.py @@ -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}, ) diff --git a/addons/account/views/account_payment_term_views.xml b/addons/account/views/account_payment_term_views.xml index e88df582e41..e865a5d400d 100644 --- a/addons/account/views/account_payment_term_views.xml +++ b/addons/account/views/account_payment_term_views.xml @@ -102,6 +102,48 @@ The last line's computation type should be "Balance" to ensure that the whole amount will be allocated.

+ + + +
+ +
diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 343c88d6b65..ab28c665970 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -197,7 +197,29 @@ -

+ + +

+

+ + Early Payment (before ): + + ( % discount) + +

+
+
+
+ + + + +
+
+
+ + +

Please use the following communication for your payment :

diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 6e37c5f2042..356fbb5749f 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -527,6 +527,14 @@
+
+
+
+
diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py index d515c8a533c..ed6c74e3da4 100644 --- a/addons/account/wizard/account_payment_register.py +++ b/addons/account/wizard/account_payment_register.py @@ -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. diff --git a/addons/account/wizard/account_payment_register_views.xml b/addons/account/wizard/account_payment_register_views.xml index c59bc8bed74..5584305c5b4 100644 --- a/addons/account/wizard/account_payment_register_views.xml +++ b/addons/account/wizard/account_payment_register_views.xml @@ -26,6 +26,8 @@ + + @@ -48,7 +50,10 @@ required="1" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/> + +