Skip to content

Commit b5bc2f1

Browse files
committed
[14.0][ADD] sale_pricelist_packaging
1 parent 1256fff commit b5bc2f1

File tree

15 files changed

+815
-0
lines changed

15 files changed

+815
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
========================
2+
Sale Pricelist Packaging
3+
========================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:14e7fa40de1ec46148b3ccae7be7a38dd3d6d213766586d618e604da92091087
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github
20+
:target: https://github.com/OCA/purchase-workflow/tree/18.0/sale_pricelist_packaging
21+
:alt: OCA/purchase-workflow
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/purchase-workflow-18-0/purchase-workflow-18-0-sale_pricelist_packaging
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&target_branch=18.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module adds packaging to the product.pricelist.item and allows you
32+
to sell the same product with different packaging and at different
33+
prices.
34+
35+
**Table of contents**
36+
37+
.. contents::
38+
:local:
39+
40+
Bug Tracker
41+
===========
42+
43+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/purchase-workflow/issues>`_.
44+
In case of trouble, please check there if your issue has already been reported.
45+
If you spotted it first, help us to smash it by providing a detailed and welcomed
46+
`feedback <https://github.com/OCA/purchase-workflow/issues/new?body=module:%20sale_pricelist_packaging%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
47+
48+
Do not contact contributors directly about support or help with technical issues.
49+
50+
Credits
51+
=======
52+
53+
Authors
54+
-------
55+
56+
* Akretion
57+
58+
Contributors
59+
------------
60+
61+
- Mathieu Delva <mathieu.delva@akretion.com>
62+
63+
Maintainers
64+
-----------
65+
66+
This module is maintained by the OCA.
67+
68+
.. image:: https://odoo-community.org/logo.png
69+
:alt: Odoo Community Association
70+
:target: https://odoo-community.org
71+
72+
OCA, or the Odoo Community Association, is a nonprofit organization whose
73+
mission is to support the collaborative development of Odoo features and
74+
promote its widespread use.
75+
76+
This module is part of the `OCA/purchase-workflow <https://github.com/OCA/purchase-workflow/tree/18.0/sale_pricelist_packaging>`_ project on GitHub.
77+
78+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2025 Akretion (https://www.akretion.com).
2+
# @author Mathieu DELVA <mathieu.delva@akretion.com>
3+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
4+
5+
{
6+
"name": "Sale Pricelist Packaging",
7+
"summary": "Sale Pricelist Packaging",
8+
"version": "14.0.1.0.0",
9+
"category": "sale",
10+
"website": "https://github.com/OCA/sale-workflow",
11+
"author": " Akretion, Odoo Community Association (OCA)",
12+
"license": "AGPL-3",
13+
"depends": [
14+
"sale_stock",
15+
],
16+
"data": ["views/product_pricelist.xml"],
17+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import product_pricelist
2+
from . import sale_order
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright 2025 Akretion (https://www.akretion.com).
2+
# @author Mathieu DELVA <mathieu.delva@akretion.com>
3+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
4+
from odoo import fields, models
5+
from itertools import chain
6+
from odoo import fields, models, _
7+
from odoo.exceptions import UserError
8+
9+
class ProductPricelistItem(models.Model):
10+
_inherit = "product.pricelist.item"
11+
12+
packaging_id = fields.Many2one("product.packaging")
13+
14+
def _is_applicable_for(self, product, qty_in_product_uom):
15+
ctx = self.env.context
16+
print("### _is_applicable_for TRIGGERED", ctx)
17+
if "packaging" in ctx:
18+
if ctx["packaging"] == self.packaging_id:
19+
return super()._is_applicable_for(product, qty_in_product_uom)
20+
return False
21+
22+
elif "packaging" not in ctx and self.packaging_id:
23+
return False
24+
25+
return super()._is_applicable_for(product, qty_in_product_uom)
26+
27+
class ProductPricelist(models.Model):
28+
_inherit = "product.pricelist"
29+
30+
def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False):
31+
""" Low-level method - Mono pricelist, multi products
32+
Returns: dict{product_id: (price, suitable_rule) for the given pricelist}
33+
34+
Date in context can be a date, datetime, ...
35+
36+
:param products_qty_partner: list of typles products, quantity, partner
37+
:param datetime date: validity date
38+
:param ID uom_id: intermediate unit of measure
39+
"""
40+
self.ensure_one()
41+
if not date:
42+
date = self._context.get('date') or fields.Datetime.now()
43+
if not uom_id and self._context.get('uom'):
44+
uom_id = self._context['uom']
45+
if uom_id:
46+
# rebrowse with uom if given
47+
products = [item[0].with_context(uom=uom_id) for item in products_qty_partner]
48+
products_qty_partner = [(products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner)]
49+
else:
50+
products = [item[0] for item in products_qty_partner]
51+
52+
if not products:
53+
return {}
54+
55+
categ_ids = {}
56+
for p in products:
57+
categ = p.categ_id
58+
while categ:
59+
categ_ids[categ.id] = True
60+
categ = categ.parent_id
61+
categ_ids = list(categ_ids)
62+
63+
is_product_template = products[0]._name == "product.template"
64+
if is_product_template:
65+
prod_tmpl_ids = [tmpl.id for tmpl in products]
66+
# all variants of all products
67+
prod_ids = [p.id for p in
68+
list(chain.from_iterable([t.product_variant_ids for t in products]))]
69+
else:
70+
prod_ids = [product.id for product in products]
71+
prod_tmpl_ids = [product.product_tmpl_id.id for product in products]
72+
73+
items = self._compute_price_rule_get_items(products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids)
74+
print("### items TRIGGERED", items)
75+
results = {}
76+
for product, qty, partner in products_qty_partner:
77+
results[product.id] = 0.0
78+
suitable_rule = False
79+
80+
# Final unit price is computed according to `qty` in the `qty_uom_id` UoM.
81+
# An intermediary unit price may be computed according to a different UoM, in
82+
# which case the price_uom_id contains that UoM.
83+
# The final price will be converted to match `qty_uom_id`.
84+
qty_uom_id = self._context.get('uom') or product.uom_id.id
85+
qty_in_product_uom = qty
86+
if qty_uom_id != product.uom_id.id:
87+
try:
88+
qty_in_product_uom = self.env['uom.uom'].browse([self._context['uom']])._compute_quantity(qty, product.uom_id)
89+
except UserError:
90+
# Ignored - incompatible UoM in context, use default product UoM
91+
pass
92+
93+
# if Public user try to access standard price from website sale, need to call price_compute.
94+
# TDE SURPRISE: product can actually be a template
95+
price = product.price_compute('list_price')[product.id]
96+
97+
price_uom = self.env['uom.uom'].browse([qty_uom_id])
98+
for rule in items:
99+
print("### rule TRIGGERED", rule)
100+
if not rule._is_applicable_for(product, qty_in_product_uom):
101+
continue
102+
if rule.base == 'pricelist' and rule.base_pricelist_id:
103+
price = rule.base_pricelist_id._compute_price_rule([(product, qty, partner)], date, uom_id)[product.id][0] # TDE: 0 = price, 1 = rule
104+
src_currency = rule.base_pricelist_id.currency_id
105+
else:
106+
# if base option is public price take sale price else cost price of product
107+
# price_compute returns the price in the context UoM, i.e. qty_uom_id
108+
price = product.price_compute(rule.base)[product.id]
109+
if rule.base == 'standard_price':
110+
src_currency = product.cost_currency_id
111+
else:
112+
src_currency = product.currency_id
113+
114+
if src_currency != self.currency_id:
115+
price = src_currency._convert(
116+
price, self.currency_id, self.env.company, date, round=False)
117+
118+
if price is not False:
119+
price = rule._compute_price(price, price_uom, product, quantity=qty, partner=partner)
120+
suitable_rule = rule
121+
break
122+
123+
if not suitable_rule:
124+
cur = product.currency_id
125+
price = cur._convert(price, self.currency_id, self.env.company, date, round=False)
126+
127+
results[product.id] = (price, suitable_rule and suitable_rule.id or False)
128+
129+
return results
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2025 Akretion (https://www.akretion.com).
2+
# @author Mathieu DELVA <mathieu.delva@akretion.com>
3+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
4+
from odoo import api, models
5+
6+
7+
class SaleOrderLine(models.Model):
8+
_inherit = "sale.order.line"
9+
10+
@api.onchange("product_packaging")
11+
def _onchange_product_packaging(self):
12+
self.with_context(packaging=self.product_packaging).product_uom_change()
13+
return super()._onchange_product_packaging()
14+
15+
@api.onchange('product_uom', 'product_uom_qty')
16+
def product_uom_change(self):
17+
res = super().product_uom_change()
18+
if self.product_packaging and "packaging" not in self.env.context:
19+
self = self.with_context(packaging=self.product_packaging)
20+
print("### product_uom_change TRIGGERED", self.env.context)
21+
return res
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Mathieu Delva <mathieu.delva@akretion.com>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This module adds packaging to the product.pricelist.item and allows you to sell the same product with different packaging and at different prices.

0 commit comments

Comments
 (0)