Skip to content

Commit 2edebe1

Browse files
zamberjodocker-odoo
authored and
docker-odoo
committed
14.0 purchase global discount (#2)
[ADD] purchase_global_discount
1 parent bddea26 commit 2edebe1

17 files changed

+565
-0
lines changed

purchase_global_discount/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Copyright 2021 Comunitea - Omar Castiñeira
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
3+
{
4+
"name": "Purchase Global Discount",
5+
"version": "14.0.1.0.0",
6+
"category": "Purchases Management",
7+
"author": "Comunitea, Odoo Community Association (OCA)",
8+
"website": "https://github.com/OCA/purchase-workflow",
9+
"license": "AGPL-3",
10+
"depends": ["account_global_discount", "purchase"],
11+
"data": ["views/purchase_order_views.xml", "views/report_purchase_order.xml"],
12+
"application": False,
13+
"installable": True,
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import purchase_order
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright 2021 Comunitea - Omar Castiñeira
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
from odoo import _, api, exceptions, fields, models
4+
5+
6+
class PurchaseOrder(models.Model):
7+
_inherit = "purchase.order"
8+
9+
global_discount_ids = fields.Many2many(
10+
comodel_name="global.discount",
11+
string="Purchase Global Discounts",
12+
domain="[('discount_scope', '=', 'purchase'), "
13+
"('account_id', '!=', False), '|', "
14+
"('company_id', '=', company_id), ('company_id', '=', False)]",
15+
)
16+
# HACK: Looks like UI doesn't behave well with Many2many fields and
17+
# negative groups when the same field is shown. In this case, we want to
18+
# show the readonly version to any not in the global discount group.
19+
# TODO: Check if it's fixed in future versions
20+
global_discount_ids_readonly = fields.Many2many(
21+
related="global_discount_ids",
22+
string="Purchase Global Discounts (readonly)",
23+
readonly=True,
24+
)
25+
amount_global_discount = fields.Monetary(
26+
string="Total Global Discounts",
27+
compute="_amount_all", # pylint: disable=C8108
28+
currency_field="currency_id",
29+
compute_sudo=True, # Odoo core fields are storable so compute_sudo is True
30+
readonly=True,
31+
store=True,
32+
)
33+
amount_untaxed_before_global_discounts = fields.Monetary(
34+
string="Amount Untaxed Before Discounts",
35+
compute="_amount_all", # pylint: disable=C8108
36+
currency_field="currency_id",
37+
compute_sudo=True, # Odoo core fields are storable so compute_sudo is True
38+
readonly=True,
39+
store=True,
40+
)
41+
amount_total_before_global_discounts = fields.Monetary(
42+
string="Amount Total Before Discounts",
43+
compute="_amount_all", # pylint: disable=C8108
44+
currency_field="currency_id",
45+
compute_sudo=True, # Odoo core fields are storable so compute_sudo is True
46+
readonly=True,
47+
store=True,
48+
)
49+
50+
@api.model
51+
def get_discounted_global(self, price=0, discounts=None):
52+
"""Compute discounts successively"""
53+
discounts = discounts or []
54+
if not discounts:
55+
return price
56+
discount = discounts.pop(0)
57+
price *= 1 - (discount / 100)
58+
return self.get_discounted_global(price, discounts)
59+
60+
def _check_global_discounts_sanity(self):
61+
"""Perform a sanity check for discarding cases that will lead to
62+
incorrect data in discounts.
63+
"""
64+
self.ensure_one()
65+
if not self.global_discount_ids:
66+
return True
67+
taxes_keys = {}
68+
for line in self.order_line.filtered(lambda l: not l.display_type):
69+
if not line.taxes_id:
70+
raise exceptions.UserError(
71+
_("With global discounts, taxes in lines are required.")
72+
)
73+
for key in taxes_keys:
74+
if key == line.taxes_id:
75+
break
76+
elif key & line.taxes_id:
77+
raise exceptions.UserError(
78+
_("Incompatible taxes found for global discounts.")
79+
)
80+
else:
81+
taxes_keys[line.taxes_id] = True
82+
83+
@api.depends("order_line.price_total", "global_discount_ids")
84+
def _amount_all(self):
85+
res = super()._amount_all()
86+
for order in self:
87+
order._check_global_discounts_sanity()
88+
amount_untaxed_before_global_discounts = order.amount_untaxed
89+
amount_total_before_global_discounts = order.amount_total
90+
discounts = order.global_discount_ids.mapped("discount")
91+
amount_discounted_untaxed = amount_discounted_tax = 0
92+
for line in order.order_line:
93+
discounted_subtotal = self.get_discounted_global(
94+
line.price_subtotal, discounts.copy()
95+
)
96+
amount_discounted_untaxed += discounted_subtotal
97+
discounted_tax = line.taxes_id.compute_all(
98+
discounted_subtotal,
99+
line.order_id.currency_id,
100+
1.0,
101+
product=line.product_id,
102+
partner=line.order_id.partner_id,
103+
)
104+
amount_discounted_tax += sum(
105+
t.get("amount", 0.0) for t in discounted_tax.get("taxes", [])
106+
)
107+
order.update(
108+
{
109+
"amount_untaxed_before_global_discounts": (
110+
amount_untaxed_before_global_discounts
111+
),
112+
"amount_total_before_global_discounts": (
113+
amount_total_before_global_discounts
114+
),
115+
"amount_global_discount": (
116+
amount_untaxed_before_global_discounts
117+
- amount_discounted_untaxed
118+
),
119+
"amount_untaxed": amount_discounted_untaxed,
120+
"amount_tax": amount_discounted_tax,
121+
"amount_total": (amount_discounted_untaxed + amount_discounted_tax),
122+
}
123+
)
124+
return res
125+
126+
@api.onchange("partner_id")
127+
def onchange_partner_id(self):
128+
res = super().onchange_partner_id()
129+
self.global_discount_ids = self.partner_id.supplier_global_discount_ids.filtered(
130+
lambda d: d.company_id == self.company_id
131+
) or self.partner_id.commercial_partner_id.supplier_global_discount_ids.filtered(
132+
lambda d: d.company_id == self.company_id
133+
)
134+
return res
135+
136+
def _prepare_invoice(self):
137+
invoice_vals = super()._prepare_invoice()
138+
if self.global_discount_ids:
139+
invoice_vals.update(
140+
{"global_discount_ids": [(6, 0, self.global_discount_ids.ids)]}
141+
)
142+
return invoice_vals
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
To configure this module, you need to:
2+
3+
#. Go to *Settings > Technical > Parameters > Global Discounts*
4+
#. Add a new discount.
5+
#. Choose the discount scope (sales or purchases).
6+
#. You can also restrict it to a certain company if needed.
7+
8+
You can assign global discounts to partners as well:
9+
10+
#. Go to a partner that is a company.
11+
#. Go to the *Sales & Purchases* tab.
12+
#. In section sale (if the partner is a customer), you can set sale discounts.
13+
#. In section purchase (if the partner is a supplier), you can set purchase
14+
discounts.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Omar Castiñeira <omar@comunitea.com>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Apply global financial discounts to purchases that will be transmited to
2+
invoices and accounting.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
* Not all the taxes combination can be compatible with global discounts. An
2+
error is raised in that cases.
3+
* Currently, taxes in invoice lines are mandatory with global discounts.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
To use this module, you need to:
2+
3+
#. Create a new purchase order and choose a partner.
4+
#. If the partner has supplier global discounts set, those will be applied to
5+
the order by default.
6+
#. Otherwise, you can set them manually from the header of the purchase order.
7+
#. In the order footer, you can see the computed discounts.
8+
#. When you create an invoice from the order, the proper global discounts will
9+
be applied on it.
Loading

purchase_global_discount/static/description/icon.svg

+79
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_purchase_global_discount

0 commit comments

Comments
 (0)