Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12.0][IMP] account_global_discount: add fixed discount support #1662

Open
wants to merge 2 commits into
base: 12.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 125 additions & 32 deletions account_global_discount/models/account_invoice.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Copyright 2019 Tecnativa - David Vidal
# Copyright 2020 Tecnativa - Pedro M. Baeza
# Copyright 2022 Simone Rubino - TAKOBI
# Copyright 2024 Sergio Zanchetta - PNLUG APS
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, exceptions, fields, models
from odoo.addons import decimal_precision as dp
from odoo.fields import first


class AccountInvoice(models.Model):
Expand All @@ -21,6 +24,22 @@
"}.get(type, [])), ('account_id', '!=', False), '|', "
"('company_id', '=', company_id), ('company_id', '=', False)]",
)
global_discount_base = fields.Selection(
selection=[
('subtotal', 'Subtotal'),
('total', 'Total'),
],
string='Discount Base',
compute='_compute_global_discount_selection',
)
global_discount_type = fields.Selection(
selection=[
('percentage', 'Percentage'),
('fixed', 'Fixed'),
],
string='Discount Type',
compute='_compute_global_discount_selection',
)
# HACK: Looks like UI doesn't behave well with Many2many fields and
# negative groups when the same field is shown. In this case, we want to
# show the readonly version to any not in the global discount group.
Expand All @@ -42,25 +61,49 @@
currency_field='currency_id',
readonly=True,
)
amount_total_before_global_discounts = fields.Monetary(
string='Amount Total Before Discounts',
compute='_compute_amount',
currency_field='currency_id',
readonly=True,
)
invoice_global_discount_ids = fields.One2many(
comodel_name='account.invoice.global.discount',
inverse_name='invoice_id',
readonly=True,
)

@api.multi
@api.depends('global_discount_ids')
def _compute_global_discount_selection(self):
for invoice in self:
# Only check first because sanity checks
# assure all global discounts in same invoice have same base
first_global_discount = first(invoice.global_discount_ids)
invoice.global_discount_base = first_global_discount.discount_base
invoice.global_discount_type = first_global_discount.discount_type

def _set_global_discounts_by_tax(self):
"""Create invoice global discount lines by taxes combinations and
discounts.
"""
self.ensure_one()
invoice_global_discounts = self.env['account.invoice.global.discount']
self.invoice_global_discount_ids = invoice_global_discounts.browse()
if not self.global_discount_ids:
self.amount_global_discount = 0.0
self.amount_untaxed_before_global_discounts = 0.0
return
invoice_global_discounts = self.env['account.invoice.global.discount']
taxes_keys = {}
# Perform a sanity check for discarding cases that will lead to
# incorrect data in discounts
discount_base = set(self.global_discount_ids.mapped('discount_base'))
if len(discount_base) > 1:
raise exceptions.UserError(
_("All global discount must have the same base")
)
discount_base = discount_base.pop()

for inv_line in self.invoice_line_ids.filtered(
lambda l: not l.display_type):
if not inv_line.invoice_line_tax_ids:
Expand All @@ -76,30 +119,51 @@
))
else:
taxes_keys[inv_line.invoice_line_tax_ids] = True
for tax_line in self.tax_line_ids:
key = []
to_create = True
for key in taxes_keys:
if tax_line.tax_id in key:
to_create = taxes_keys[key]
taxes_keys[key] = False # mark for not duplicating
break # we leave in key variable the proper taxes value
if not to_create:
continue
base = tax_line.base_before_global_discounts or tax_line.base

if discount_base == 'subtotal':
for tax_line in self.tax_line_ids:
key = []
to_create = True
for key in taxes_keys:
if tax_line.tax_id in key:
to_create = taxes_keys[key]
taxes_keys[key] = False # mark for not duplicating
break # we leave in key variable the proper taxes value
if not to_create:
continue
base = tax_line.base_before_global_discounts or tax_line.base
for global_discount in self.global_discount_ids:
discount = global_discount._get_global_discount_vals(base)
invoice_global_discounts += invoice_global_discounts.new({
'name': global_discount.display_name,
'invoice_id': self.id,
'global_discount_id': global_discount.id,
'discount_type': global_discount.discount_type,
'discount': global_discount.discount,
'discount_fixed': global_discount.discount_fixed,
'base': base,
'base_discounted': discount['base_discounted'],
'account_id': global_discount.account_id.id,
'tax_ids': [(4, x.id) for x in key],
})
base = discount['base_discounted']
elif discount_base == 'total':
base = self.amount_total
for global_discount in self.global_discount_ids:
discount = global_discount._get_global_discount_vals(base)
invoice_global_discounts += invoice_global_discounts.new({
'name': global_discount.display_name,
'invoice_id': self.id,
'global_discount_id': global_discount.id,
'discount_type': global_discount.discount_type,
'discount': global_discount.discount,
'discount_fixed': global_discount.discount_fixed,
'base': base,
'base_discounted': discount['base_discounted'],
'account_id': global_discount.account_id.id,
'tax_ids': [(4, x.id) for x in key],
})
base = discount['base_discounted']

self.invoice_global_discount_ids = invoice_global_discounts

def _set_global_discounts(self):
Expand Down Expand Up @@ -142,9 +206,15 @@
round_curr(discount.discount_amount) * - 1
for discount in self.invoice_global_discount_ids)
self.amount_untaxed_before_global_discounts = self.amount_untaxed
self.amount_untaxed = (
self.amount_untaxed + self.amount_global_discount)
self.amount_total = self.amount_untaxed + self.amount_tax
self.amount_total_before_global_discounts = self.amount_total

if self.global_discount_base == 'subtotal':
self.amount_untaxed = (
self.amount_untaxed + self.amount_global_discount)
self.amount_total = self.amount_untaxed + self.amount_tax
elif self.global_discount_base == 'total':
self.amount_total += self.amount_global_discount

amount_total_company_signed = self.amount_total
amount_untaxed_signed = self.amount_untaxed
if (self.currency_id and self.company_id and
Expand Down Expand Up @@ -174,25 +244,29 @@
"""Override this computation for adding global discount to taxes."""
round_curr = self.currency_id.round
tax_grouped = super().get_taxes_values()
for key in tax_grouped.keys():
base = tax_grouped[key]['base']
tax_grouped[key]['base_before_global_discounts'] = base
amount = tax_grouped[key]['amount']
for discount in self.global_discount_ids:
base = discount._get_global_discount_vals(
base)['base_discounted']
amount = discount._get_global_discount_vals(
amount)['base_discounted']
tax_grouped[key]['base'] = round_curr(base)
tax_grouped[key]['amount'] = round_curr(amount)
if self.global_discount_base == 'subtotal':
for key in tax_grouped.keys():
base = tax_grouped[key]['base']
tax_grouped[key]['base_before_global_discounts'] = base
amount = tax_grouped[key]['amount']
for discount in self.global_discount_ids:
base = discount._get_global_discount_vals(
base)['base_discounted']
if discount.discount_type == 'percentage':
amount = discount._get_global_discount_vals(
amount)['base_discounted']
tax_grouped[key]['base'] = round_curr(base)
tax_grouped[key]['amount'] = round_curr(amount)

return tax_grouped

@api.model
def invoice_line_move_line_get(self):
"""Append global discounts move lines"""
res = super().invoice_line_move_line_get()

for discount in self.invoice_global_discount_ids:
if not discount.discount:
if not (discount.discount or discount.discount_fixed):
continue
# Traverse upstream result for taking existing dictionary vals
inv_lines = self.invoice_line_ids.filtered(
Expand Down Expand Up @@ -246,10 +320,23 @@
string='Global Discount',
readonly=True,
)
discount_type = fields.Selection(
selection=[
('percentage', 'Percentage'),
('fixed', 'Fixed'),
],
string='Discount Type',
readonly=True,
)
discount = fields.Float(
string='Discount (number)',
readonly=True,
)
discount_fixed = fields.Float(
string='Discount (number)',
currency_field='currency_id',
readonly=True,
)
discount_display = fields.Char(
compute='_compute_discount_display',
readonly=True,
Expand All @@ -270,7 +357,7 @@
readonly=True,
)
discount_amount = fields.Monetary(
string='Discounted Amount',
string='Discount Amount',
compute='_compute_discount_amount',
currency_field='currency_id',
readonly=True,
Expand All @@ -295,10 +382,16 @@

def _compute_discount_display(self):
"""Given a discount type, we need to render a different symbol"""

for one in self:
precision = self.env['decimal.precision'].precision_get('Discount')
one.discount_display = '{0:.{1}f}%'.format(
one.discount * -1, precision)
if one.discount_type == 'percentage':
precision = self.env['decimal.precision'].precision_get('Discount')
one.discount_display = '{0:.{1}f}%'.format(
one.discount * -1, precision)
elif one.discount_type == 'fixed':
precision = self.env['decimal.precision'].precision_get('Product Price')
one.discount_display = '{0:.{1}f}'.format(

Check warning on line 393 in account_global_discount/models/account_invoice.py

View check run for this annotation

Codecov / codecov/patch

account_global_discount/models/account_invoice.py#L392-L393

Added lines #L392 - L393 were not covered by tests
one.discount_fixed * -1, precision)

@api.depends('base', 'base_discounted')
def _compute_discount_amount(self):
Expand Down
6 changes: 6 additions & 0 deletions account_global_discount/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
* David Vidal
* Carlos Dauden
* Rafael Blasco
* `TAKOBI <https://takobi.online>`_:

* Simone Rubino <[email protected]>
* `Pordenone Linux User Group APS <https://www.pnlug.it>`_:

* Sergio Zanchetta
86 changes: 86 additions & 0 deletions account_global_discount/tests/test_global_discount.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright 2019 Tecnativa - David Vidal
# Copyright 2020 Tecnativa - Pedro M. Baeza
# Copyright 2022 Simone Rubino - TAKOBI
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import exceptions
from odoo.tests import common
Expand Down Expand Up @@ -39,6 +40,21 @@ def setUpClass(cls):
'discount': 50,
'account_id': cls.account.id,
})
cls.global_discount_total = cls.global_discount_obj.create({
'name': 'Test Total Discount',
'discount_scope': 'sale',
'discount_base': 'total',
'discount': 50,
'account_id': cls.account.id,
})
cls.global_discount_fixed_total = cls.global_discount_obj.create({
'name': 'Test Total Fixed Discount',
'discount_scope': 'sale',
'discount_base': 'total',
'discount_type': 'fixed',
'discount_fixed': 20,
'account_id': cls.account.id,
})
cls.partner_1 = cls.env['res.partner'].create({
'name': 'Mr. Odoo',
})
Expand Down Expand Up @@ -242,3 +258,73 @@ def test_06_no_taxes(self):
self.invoice.global_discount_ids = self.global_discount_1
with self.assertRaises(exceptions.UserError):
self.invoice._onchange_global_discount_ids()

def test_mixed_discount_base(self):
"""
Check that an invoice can only have discount having the same base.
"""
self.invoice.global_discount_ids = \
self.global_discount_1 + self.global_discount_total
with self.assertRaises(exceptions.UserError) as ue:
self.invoice._onchange_global_discount_ids()
exception_message = ue.exception.name
self.assertIn('must have the same base', exception_message)

def test_discount_total(self):
"""
Add global discounts on total to the invoice.

Check that only the total amount is discounted,
taxes and subtotal remain the same.
"""
# Pre-condition: check starting amounts and the discount base
self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0)
self.assertAlmostEqual(self.invoice.amount_tax, 30.0)
self.assertAlmostEqual(self.invoice.amount_global_discount, 0)
self.assertAlmostEqual(self.invoice.amount_total, 230)
self.assertEqual(self.global_discount_total.discount_base, 'total')

# Act: set the global total discount
self.invoice.global_discount_ids = self.global_discount_total
self.invoice._onchange_global_discount_ids()

# Assert: global discounts are applied to the total
# and taxes remain the same:
# 230 - 50% (global disc. 1) = 115
self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0)
self.assertAlmostEqual(self.invoice.amount_tax, 30.0)
self.assertAlmostEqual(self.invoice.amount_global_discount, -115.0)
self.assertAlmostEqual(self.invoice.amount_total, 115.0)

def test_global_invoice_total_mixed_discount(self):
"""Add both global fixed and percentage discount on total to the invoice"""
# Pre-condition: check starting amounts and the discount base
self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0)
self.assertAlmostEqual(self.invoice.amount_tax, 30.0)
self.assertAlmostEqual(self.invoice.amount_global_discount, 0)
self.assertAlmostEqual(self.invoice.amount_total, 230)
self.assertEqual(self.global_discount_total.discount_base, 'total')

# Act: set the global total fixed discount
self.invoice.global_discount_ids = self.global_discount_fixed_total
self.invoice._onchange_global_discount_ids()

# Assert: fixed discount is applied to the total
# and taxes remain the same:
# 230 - 20 (fixed disc.) = 210
self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0)
self.assertAlmostEqual(self.invoice.amount_tax, 30.0)
self.assertAlmostEqual(self.invoice.amount_global_discount, -20.0)
self.assertAlmostEqual(self.invoice.amount_total, 210.0)

# Act: set the next global total discount
self.invoice.global_discount_ids += self.global_discount_total
self.invoice._onchange_global_discount_ids()

# Assert: percentage discount is applied to previous discounted total,
# and taxes remain the same:
# 210 - 50% (percentage disc.) = 105
self.assertAlmostEqual(self.invoice.amount_untaxed, 200.0)
self.assertAlmostEqual(self.invoice.amount_tax, 30.0)
self.assertAlmostEqual(self.invoice.amount_global_discount, -125.0)
self.assertAlmostEqual(self.invoice.amount_total, 105.0)
Loading
Loading