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