odoo-addon-sale-commission-product-criteria 16.0.1.0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. odoo/addons/sale_commission_product_criteria/README.rst +116 -0
  2. odoo/addons/sale_commission_product_criteria/__init__.py +1 -0
  3. odoo/addons/sale_commission_product_criteria/__manifest__.py +22 -0
  4. odoo/addons/sale_commission_product_criteria/demo/sale_agent_demo.xml +69 -0
  5. odoo/addons/sale_commission_product_criteria/i18n/it.po +427 -0
  6. odoo/addons/sale_commission_product_criteria/i18n/sale_commission_product_criteria.pot +396 -0
  7. odoo/addons/sale_commission_product_criteria/models/__init__.py +5 -0
  8. odoo/addons/sale_commission_product_criteria/models/account_move.py +30 -0
  9. odoo/addons/sale_commission_product_criteria/models/commission.py +266 -0
  10. odoo/addons/sale_commission_product_criteria/models/sale.py +55 -0
  11. odoo/addons/sale_commission_product_criteria/models/sale_commission_line_mixin.py +78 -0
  12. odoo/addons/sale_commission_product_criteria/models/settlement.py +7 -0
  13. odoo/addons/sale_commission_product_criteria/readme/CONTRIBUTORS.rst +6 -0
  14. odoo/addons/sale_commission_product_criteria/readme/DESCRIPTION.rst +16 -0
  15. odoo/addons/sale_commission_product_criteria/readme/USAGE.rst +7 -0
  16. odoo/addons/sale_commission_product_criteria/security/ir.model.access.csv +3 -0
  17. odoo/addons/sale_commission_product_criteria/static/description/icon.png +0 -0
  18. odoo/addons/sale_commission_product_criteria/static/description/index.html +451 -0
  19. odoo/addons/sale_commission_product_criteria/tests/__init__.py +1 -0
  20. odoo/addons/sale_commission_product_criteria/tests/test_sale_commission_product_criteria.py +204 -0
  21. odoo/addons/sale_commission_product_criteria/views/views.xml +239 -0
  22. odoo_addon_sale_commission_product_criteria-16.0.1.0.0.2.dist-info/METADATA +135 -0
  23. odoo_addon_sale_commission_product_criteria-16.0.1.0.0.2.dist-info/RECORD +25 -0
  24. odoo_addon_sale_commission_product_criteria-16.0.1.0.0.2.dist-info/WHEEL +5 -0
  25. odoo_addon_sale_commission_product_criteria-16.0.1.0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,266 @@
1
+ # © 2023 ooops404
2
+ # Copyright 2023 Simone Rubino - Aion Tech
3
+ # License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html
4
+ from odoo import _, api, fields, models
5
+ from odoo.exceptions import ValidationError
6
+ from odoo.tools import float_repr
7
+
8
+
9
+ class SaleCommission(models.Model):
10
+ _inherit = "commission"
11
+
12
+ commission_type = fields.Selection(
13
+ selection_add=[("product", "Product criteria")],
14
+ ondelete={"product": "set default"},
15
+ )
16
+ item_ids = fields.One2many("commission.item", "commission_id", copy=True)
17
+
18
+ def action_unarchive(self):
19
+ res = super().action_unarchive()
20
+ items = (
21
+ self.env["commission.item"]
22
+ .with_context(active_test=False)
23
+ .search([("commission_id", "=", self.id)])
24
+ )
25
+ if items:
26
+ items.write({"active": True})
27
+ return res
28
+
29
+ @api.onchange("commission_type")
30
+ def onchange_commission_type(self):
31
+ # Prevent commission_type change in certain cases
32
+ self.check_type_change_allowed_sale()
33
+ self.check_type_change_allowed_moves()
34
+
35
+ def check_type_change_allowed_sale(self):
36
+ sola_ids = self.env["sale.order.line.agent"].search(
37
+ [("commission_id", "=", self._origin.id)]
38
+ )
39
+ done_so_ids = sola_ids.filtered(lambda x: x.object_id.state in ["done", "sale"])
40
+ if done_so_ids:
41
+ raise ValidationError(
42
+ _(
43
+ "There is done Sale Orders with this commission. "
44
+ "Commission type change is not allowed."
45
+ )
46
+ )
47
+
48
+ def check_type_change_allowed_moves(self):
49
+ aila_ids = self.env["account.invoice.line.agent"].search(
50
+ [("commission_id", "=", self._origin.id)]
51
+ )
52
+ done_move_ids = aila_ids.filtered(
53
+ lambda x: x.object_id.parent_state == "posted"
54
+ )
55
+ if done_move_ids:
56
+ raise ValidationError(
57
+ _(
58
+ "There is posted Account Move Lines with this commission. "
59
+ "Commission type change is not allowed."
60
+ )
61
+ )
62
+
63
+
64
+ class CommissionItem(models.Model):
65
+ _name = "commission.item"
66
+ _description = "Commission Item"
67
+ _order = "applied_on, based_on, categ_id desc, id desc"
68
+
69
+ sequence = fields.Integer(default=10)
70
+ active = fields.Boolean(default=True)
71
+ commission_id = fields.Many2one(
72
+ "commission",
73
+ string="Commission",
74
+ domain=[("commission_type", "=", "product")],
75
+ required=True,
76
+ )
77
+ product_tmpl_id = fields.Many2one(
78
+ "product.template",
79
+ "Product",
80
+ ondelete="cascade",
81
+ check_company=True,
82
+ help="Specify a template if this rule only applies to one "
83
+ "product template. Keep empty otherwise.",
84
+ )
85
+ product_id = fields.Many2one(
86
+ "product.product",
87
+ "Product Variant",
88
+ ondelete="cascade",
89
+ check_company=True,
90
+ help="Specify a product if this rule only applies "
91
+ "to one product. Keep empty otherwise.",
92
+ )
93
+ categ_id = fields.Many2one(
94
+ "product.category",
95
+ "Product Category",
96
+ ondelete="cascade",
97
+ help="Specify a product category if this rule only applies to "
98
+ "products belonging to this category or its children categories. "
99
+ "Keep empty otherwise.",
100
+ )
101
+ based_on = fields.Selection(
102
+ [("sol", "Any Sale Order Line")],
103
+ required=True,
104
+ default="sol",
105
+ )
106
+ applied_on = fields.Selection(
107
+ [
108
+ ("3_global", "All Products"),
109
+ ("2_product_category", "Product Category"),
110
+ ("1_product", "Product"),
111
+ ("0_product_variant", "Product Variant"),
112
+ ],
113
+ "Apply On",
114
+ default="3_global",
115
+ required=True,
116
+ help="Commission Item applicable on selected option",
117
+ )
118
+ commission_type = fields.Selection(
119
+ [("fixed", "Fixed"), ("percentage", "Percentage")],
120
+ index=True,
121
+ default="fixed",
122
+ required=True,
123
+ )
124
+ fixed_amount = fields.Float(digits="Product Price")
125
+ percent_amount = fields.Float("Percentage Amount")
126
+ company_id = fields.Many2one(
127
+ "res.company",
128
+ "Company",
129
+ default=lambda self: self.env.company,
130
+ readonly=True,
131
+ )
132
+ currency_id = fields.Many2one(
133
+ "res.currency",
134
+ related="company_id.currency_id",
135
+ readonly=True,
136
+ )
137
+ name = fields.Char(
138
+ compute="_compute_commission_item_name_value",
139
+ help="Explicit rule name for this commission line.",
140
+ )
141
+ commission_value = fields.Char(
142
+ "Value",
143
+ compute="_compute_commission_item_name_value",
144
+ )
145
+
146
+ @api.depends(
147
+ "applied_on",
148
+ "categ_id",
149
+ "product_tmpl_id",
150
+ "product_id",
151
+ "commission_type",
152
+ "fixed_amount",
153
+ "percent_amount",
154
+ )
155
+ def _compute_commission_item_name_value(self):
156
+ for item in self:
157
+ if item.categ_id and item.applied_on == "2_product_category":
158
+ item.name = _("Category: %s") % (item.categ_id.display_name)
159
+ elif item.product_tmpl_id and item.applied_on == "1_product":
160
+ item.name = _("Product: %s") % (item.product_tmpl_id.display_name)
161
+ elif item.product_id and item.applied_on == "0_product_variant":
162
+ item.name = _("Variant: %s") % (
163
+ item.product_id.with_context(
164
+ display_default_code=False
165
+ ).display_name
166
+ )
167
+ else:
168
+ item.name = _("All Products")
169
+
170
+ if item.commission_type == "fixed":
171
+ decimal_places = self.env["decimal.precision"].precision_get(
172
+ "Product Price"
173
+ )
174
+ if item.currency_id.position == "after":
175
+ item.commission_value = "%s %s" % (
176
+ float_repr(
177
+ item.fixed_amount,
178
+ decimal_places,
179
+ ),
180
+ item.currency_id.symbol or "",
181
+ )
182
+ else:
183
+ item.commission_value = "%s %s" % (
184
+ item.currency_id.symbol or "",
185
+ float_repr(
186
+ item.fixed_amount,
187
+ decimal_places,
188
+ ),
189
+ )
190
+ elif item.commission_type == "percentage":
191
+ item.commission_value = str(item.percent_amount) + " %"
192
+
193
+ @api.constrains("product_id", "product_tmpl_id", "categ_id")
194
+ def _check_product_consistency(self):
195
+ for item in self:
196
+ if item.applied_on == "2_product_category" and not item.categ_id:
197
+ raise ValidationError(
198
+ _(
199
+ "Please specify the category for which this rule should "
200
+ "be applied"
201
+ )
202
+ )
203
+ elif item.applied_on == "1_product" and not item.product_tmpl_id:
204
+ raise ValidationError(
205
+ _(
206
+ "Please specify the product for which this rule should "
207
+ "be applied"
208
+ )
209
+ )
210
+ elif item.applied_on == "0_product_variant" and not item.product_id:
211
+ raise ValidationError(
212
+ _(
213
+ "Please specify the product variant for "
214
+ "which this rule should be applied"
215
+ )
216
+ )
217
+
218
+ @api.onchange("product_id")
219
+ def _onchange_product_id(self):
220
+ has_product_id = self.filtered("product_id")
221
+ for item in has_product_id:
222
+ item.product_tmpl_id = item.product_id.product_tmpl_id
223
+ if self.env.context.get("default_applied_on", False) == "1_product":
224
+ # If a product variant is specified, apply on variants instead
225
+ # Reset if product variant is removed
226
+ has_product_id.update({"applied_on": "0_product_variant"})
227
+ (self - has_product_id).update({"applied_on": "1_product"})
228
+
229
+ @api.onchange("product_tmpl_id")
230
+ def _onchange_product_tmpl_id(self):
231
+ has_tmpl_id = self.filtered("product_tmpl_id")
232
+ for item in has_tmpl_id:
233
+ if (
234
+ item.product_id
235
+ and item.product_id.product_tmpl_id != item.product_tmpl_id
236
+ ):
237
+ item.product_id = None
238
+
239
+ @api.model_create_multi
240
+ def create(self, values_list):
241
+ new_values_list = []
242
+ for values in values_list:
243
+ values = self.validate_values(values)
244
+ new_values_list.append(values)
245
+ return super(CommissionItem, self).create(new_values_list)
246
+
247
+ def write(self, values):
248
+ values = self.validate_values(values)
249
+ res = super(CommissionItem, self).write(values)
250
+ return res
251
+
252
+ def validate_values(self, values):
253
+ if values.get("applied_on", False):
254
+ # Ensure item consistency for later searches.
255
+ applied_on = values["applied_on"]
256
+ if applied_on == "3_global":
257
+ values.update(
258
+ dict(product_id=None, product_tmpl_id=None, categ_id=None)
259
+ )
260
+ elif applied_on == "2_product_category":
261
+ values.update(dict(product_id=None, product_tmpl_id=None))
262
+ elif applied_on == "1_product":
263
+ values.update(dict(product_id=None, categ_id=None))
264
+ elif applied_on == "0_product_variant":
265
+ values.update(dict(categ_id=None))
266
+ return values
@@ -0,0 +1,55 @@
1
+ # © 2023 ooops404
2
+ # Copyright 2023 Simone Rubino - Aion Tech
3
+ # License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html
4
+ from odoo import api, fields, models
5
+
6
+
7
+ class SaleOrderLineAgent(models.Model):
8
+ _inherit = "sale.order.line.agent"
9
+
10
+ discount = fields.Float(related="object_id.discount")
11
+ applied_commission_item_id = fields.Many2one("commission.item")
12
+ based_on = fields.Selection(related="applied_commission_item_id.based_on")
13
+ applied_on_name = fields.Char(related="applied_commission_item_id.name")
14
+ commission_type = fields.Selection(
15
+ related="applied_commission_item_id.commission_type"
16
+ )
17
+ fixed_amount = fields.Float(related="applied_commission_item_id.fixed_amount")
18
+ percent_amount = fields.Float(related="applied_commission_item_id.percent_amount")
19
+
20
+ @api.depends(
21
+ "object_id.price_subtotal", "object_id.product_id", "object_id.product_uom_qty"
22
+ )
23
+ def _compute_amount(self):
24
+ res = None
25
+ for line in self:
26
+ if line.commission_id and line.commission_id.commission_type == "product":
27
+ order_line = line.object_id
28
+ line.amount = line._get_single_commission_amount(
29
+ line.commission_id,
30
+ order_line.price_subtotal,
31
+ order_line.product_id,
32
+ order_line.product_uom_qty,
33
+ )
34
+ else:
35
+ res = super(SaleOrderLineAgent, line)._compute_amount()
36
+ return res
37
+
38
+
39
+ class SaleOrderLine(models.Model):
40
+ _inherit = "sale.order.line"
41
+
42
+ def _prepare_invoice_line(self, **optional_values):
43
+ vals = super()._prepare_invoice_line(**optional_values)
44
+ vals["agent_ids"] = [
45
+ (
46
+ 0,
47
+ 0,
48
+ {
49
+ "agent_id": x.agent_id.id,
50
+ "commission_id": x.commission_id.id,
51
+ },
52
+ )
53
+ for x in self.agent_ids
54
+ ]
55
+ return vals
@@ -0,0 +1,78 @@
1
+ # © 2023 ooops404
2
+ # Copyright 2023 Simone Rubino - Aion Tech
3
+ # License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html
4
+ from odoo import fields, models
5
+
6
+
7
+ class SaleCommissionLineMixin(models.AbstractModel):
8
+ _inherit = "commission.line.mixin"
9
+
10
+ applied_commission_id = fields.Many2one("commission", readonly=True)
11
+ commission_id = fields.Many2one(
12
+ comodel_name="commission",
13
+ ondelete="restrict",
14
+ required=False,
15
+ compute="_compute_commission_id",
16
+ store=True,
17
+ readonly=False,
18
+ copy=True,
19
+ )
20
+
21
+ def _get_commission_items(self, commission, product):
22
+ # Method replaced
23
+ categ_ids = {}
24
+ categ = product.categ_id
25
+ while categ:
26
+ categ_ids[categ.id] = True
27
+ categ = categ.parent_id
28
+ categ_ids = list(categ_ids)
29
+ # Select all suitable items. Order by best match
30
+ # (priority is: all/cat/subcat/product/variant).
31
+ self.env.cr.execute(
32
+ """
33
+ SELECT
34
+ item.id
35
+ FROM
36
+ commission_item AS item
37
+ LEFT JOIN product_category AS categ ON item.categ_id = categ.id
38
+ WHERE
39
+ (item.product_tmpl_id IS NULL OR item.product_tmpl_id = any(%s))
40
+ AND (item.product_id IS NULL OR item.product_id = any(%s))
41
+ AND (item.categ_id IS NULL OR item.categ_id = any(%s))
42
+ AND (item.commission_id = %s)
43
+ AND (item.active = TRUE)
44
+ ORDER BY
45
+ item.applied_on, item.based_on, categ.complete_name desc
46
+ """,
47
+ (
48
+ product.product_tmpl_id.ids,
49
+ product.ids,
50
+ categ_ids,
51
+ commission._origin.id, # Added this
52
+ ),
53
+ )
54
+ item_ids = [x[0] for x in self.env.cr.fetchall()]
55
+ return item_ids
56
+
57
+ def _get_single_commission_amount(self, commission, subtotal, product, quantity):
58
+ self.ensure_one()
59
+ item_ids = self._get_commission_items(commission, product)
60
+ if not item_ids:
61
+ return 0.0
62
+ commission_item = self.env["commission.item"].browse(item_ids[0])
63
+ if commission.amount_base_type == "net_amount":
64
+ # If subtotal (sale_price * quantity) is less than
65
+ # standard_price * quantity, it means that we are selling at
66
+ # lower price than we bought, so set amount_base to 0
67
+ subtotal = max([0, subtotal - product.standard_price * quantity])
68
+ self.applied_commission_item_id = commission_item
69
+ # if self.agent_id.use_multi_type_commissions:
70
+ self.applied_commission_id = commission_item.commission_id
71
+ if commission_item.commission_type == "fixed":
72
+ return commission_item.fixed_amount
73
+ elif commission_item.commission_type == "percentage":
74
+ return subtotal * (commission_item.percent_amount / 100.0)
75
+
76
+ def _get_discount_value(self, commission_item):
77
+ # Will be overridden
78
+ return self.object_id.discount
@@ -0,0 +1,7 @@
1
+ # © 2023 ooops404
2
+ # License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html
3
+ from odoo import models
4
+
5
+
6
+ class SettlementLine(models.Model):
7
+ _inherit = "commission.settlement.line"
@@ -0,0 +1,6 @@
1
+ * `Ooops404 <https://www.ooops404.com>`__:
2
+
3
+ * Ilyas <irazor147@gmail.com>
4
+ * `Aion Tech <https://aiontech.company/>`_:
5
+
6
+ * Simone Rubino <simone.rubino@aion-tech.it>
@@ -0,0 +1,16 @@
1
+ This module allows to set in the same Commission Type different commission rates according to the product on SO/invoice line.
2
+
3
+ This is made possible since this module adds a new "Product criteria" type to Commission Type and applies commission rates with the same logic of sale pricelist items.
4
+
5
+ For example, such a Commission Type can grant:
6
+
7
+ 10% on a specific Product A,
8
+ 10$ on Product B,
9
+ 4% on products in Category 1 and
10
+ 5$ on all other products.
11
+
12
+ In SO/invoice, system will apply different commissions based on variant/product/category or global, applied hierarchically. This means that for the example above, if product A is assigned to Category 1, commission assigned is 10%, as per variant/product/category/global rule application order.
13
+
14
+ Furthermore, these commission type items can be accessed and created by a specific menu, to facilitate their management in environments with lots of records.
15
+
16
+ The form for commission type item can be extended by future modules with further conditions to decide when to apply a specific item.
@@ -0,0 +1,7 @@
1
+ To use features of this module, you need to:
2
+
3
+ #. Go to Commissions > Configuration > Commission Types.
4
+ #. Create a Commission Type with type = "Product criteria".
5
+ #. Create multiple rules based on variant/product/category or global
6
+ #. These rules will be sorted according to the same logic of sale pricelist.
7
+ #. Rest flow is according to OCA sale_commission module.
@@ -0,0 +1,3 @@
1
+ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2
+ access_commission_item_manager,access_commission_item_manager,model_commission_item,sales_team.group_sale_manager,1,1,1,1
3
+ access_commission_item_salesman,access_commission_item_salesman,model_commission_item,sales_team.group_sale_salesman,1,0,0,0