Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
5
sell_only_by_packaging/models/__init__.py
Executable file
5
sell_only_by_packaging/models/__init__.py
Executable file
@@ -0,0 +1,5 @@
|
||||
from . import product_packaging
|
||||
from . import product_packaging_level
|
||||
from . import product_product
|
||||
from . import product_template
|
||||
from . import sale_order_line
|
||||
18
sell_only_by_packaging/models/product_packaging.py
Executable file
18
sell_only_by_packaging/models/product_packaging.py
Executable file
@@ -0,0 +1,18 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductPackaging(models.Model):
|
||||
_inherit = "product.packaging"
|
||||
|
||||
force_sale_qty = fields.Boolean(
|
||||
string="Force sale quantity",
|
||||
help="Determine if during the creation of a sale order line, the "
|
||||
"quantity should be forced with a multiple of the packaging.\n"
|
||||
"Example:\n"
|
||||
"You sell a product by packaging of 5 products.\n"
|
||||
"When the user will put 3 as quantity, the system can force the "
|
||||
"quantity to the superior unit (5 for this example).",
|
||||
)
|
||||
27
sell_only_by_packaging/models/product_packaging_level.py
Executable file
27
sell_only_by_packaging/models/product_packaging_level.py
Executable file
@@ -0,0 +1,27 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, exceptions, models
|
||||
|
||||
|
||||
class ProductPackagingLevel(models.Model):
|
||||
_inherit = "product.packaging.level"
|
||||
|
||||
@api.constrains("can_be_sold")
|
||||
def _check_sell_only_by_packaging_can_be_sold_packaging_ids(self):
|
||||
for record in self:
|
||||
if record.can_be_sold:
|
||||
continue
|
||||
products = record.packaging_ids.product_id
|
||||
templates = products.product_tmpl_id
|
||||
try:
|
||||
templates._check_sell_only_by_packaging_can_be_sold_packaging_ids()
|
||||
except exceptions.ValidationError as e:
|
||||
raise exceptions.ValidationError(
|
||||
self.env._(
|
||||
'Packaging level %s must stay with "Can be sold",'
|
||||
' at least one product configured as "sell only'
|
||||
' by packaging" is using it.',
|
||||
record.display_name,
|
||||
),
|
||||
) from e
|
||||
60
sell_only_by_packaging/models/product_product.py
Executable file
60
sell_only_by_packaging/models/product_product.py
Executable file
@@ -0,0 +1,60 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_compare, float_round
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = "product.product"
|
||||
|
||||
min_sellable_qty = fields.Float(
|
||||
compute="_compute_variant_min_sellable_qty",
|
||||
help=(
|
||||
"Minimum sellable quantity, according to the available packagings, "
|
||||
"if Only Sell by Packaging is set."
|
||||
),
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"sell_only_by_packaging",
|
||||
"packaging_ids.qty",
|
||||
"packaging_ids.sales",
|
||||
)
|
||||
def _compute_variant_min_sellable_qty(self):
|
||||
for record in self:
|
||||
record.min_sellable_qty = 0.0
|
||||
if record.sell_only_by_packaging and record.packaging_ids:
|
||||
sellable_pkgs = record.packaging_ids.filtered(lambda p: p.sales)
|
||||
record.min_sellable_qty = fields.first(
|
||||
sellable_pkgs.sorted(lambda p: p.qty)
|
||||
).qty
|
||||
|
||||
def _convert_packaging_qty(self, qty, uom, packaging):
|
||||
"""
|
||||
Convert the given qty with given UoM to the packaging uom.
|
||||
To do that, first transform the qty to the reference UoM and then
|
||||
transform using the packaging UoM.
|
||||
The given qty is not updated if the product has sell_only_by_packaging
|
||||
set to False or if the packaging is not set.
|
||||
Inspired from sale_stock/models.sale_order.py _check_package(...)
|
||||
:param qty: float
|
||||
:return: float
|
||||
"""
|
||||
if not self or not packaging:
|
||||
return qty
|
||||
self.ensure_one()
|
||||
if self.sell_only_by_packaging and packaging.force_sale_qty:
|
||||
q = self.uom_id._compute_quantity(packaging.qty, uom)
|
||||
if (
|
||||
qty
|
||||
and q
|
||||
and float_compare(
|
||||
qty / q,
|
||||
float_round(qty / q, precision_rounding=1.0),
|
||||
precision_rounding=0.001,
|
||||
)
|
||||
!= 0
|
||||
):
|
||||
qty = qty - (qty % q) + q
|
||||
return qty
|
||||
81
sell_only_by_packaging/models/product_template.py
Executable file
81
sell_only_by_packaging/models/product_template.py
Executable file
@@ -0,0 +1,81 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
sell_only_by_packaging = fields.Boolean(
|
||||
string="Only sell by packaging",
|
||||
company_dependent=True,
|
||||
default=False,
|
||||
help="Restrict the usage of this product on sale order lines without "
|
||||
"packaging defined",
|
||||
copy=False,
|
||||
)
|
||||
|
||||
min_sellable_qty = fields.Float(
|
||||
compute="_compute_template_min_sellable_qty",
|
||||
help=(
|
||||
"Minimum sellable quantity, according to the available packagings, "
|
||||
"if Only Sell by Packaging is set."
|
||||
),
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"sell_only_by_packaging",
|
||||
"uom_id.factor",
|
||||
"product_variant_ids.min_sellable_qty",
|
||||
)
|
||||
def _compute_template_min_sellable_qty(self):
|
||||
for record in self:
|
||||
record.min_sellable_qty = 0.0
|
||||
if len(record.product_variant_ids) == 1:
|
||||
# Pick the value from the variant if there's only 1
|
||||
record.min_sellable_qty = record.product_variant_ids.min_sellable_qty
|
||||
|
||||
@api.constrains("sell_only_by_packaging", "sale_ok")
|
||||
def _check_sell_only_by_packaging_sale_ok(self):
|
||||
for product in self:
|
||||
if product.sell_only_by_packaging and not product.sale_ok:
|
||||
raise ValidationError(
|
||||
self.env._(
|
||||
"Product %s cannot be defined to be sold only by "
|
||||
"packaging if it cannot be sold.",
|
||||
product.name,
|
||||
),
|
||||
)
|
||||
|
||||
@api.constrains("sell_only_by_packaging", "packaging_ids")
|
||||
def _check_sell_only_by_packaging_can_be_sold_packaging_ids(self):
|
||||
for product in self:
|
||||
if product.sell_only_by_packaging:
|
||||
if (
|
||||
# Product template only condition
|
||||
len(product.product_variant_ids) == 1
|
||||
and not any(pack.sales for pack in product.packaging_ids)
|
||||
# Product variants condition
|
||||
or len(product.product_variant_ids) > 1
|
||||
and not any(
|
||||
pack.sales
|
||||
for pack in product.product_variant_ids.mapped("packaging_ids")
|
||||
)
|
||||
):
|
||||
raise ValidationError(
|
||||
self.env._(
|
||||
"Product %s cannot be defined to be sold only by "
|
||||
"packaging if it does not have any packaging that "
|
||||
"can be sold defined.",
|
||||
product.name,
|
||||
),
|
||||
)
|
||||
|
||||
@api.depends("sale_ok")
|
||||
def _compute_expense_policy(self):
|
||||
self.filtered(
|
||||
lambda t: not t.sale_ok and t.sell_only_by_packaging
|
||||
).sell_only_by_packaging = False
|
||||
return super()._compute_expense_policy()
|
||||
47
sell_only_by_packaging/models/sale_order_line.py
Executable file
47
sell_only_by_packaging/models/sale_order_line.py
Executable file
@@ -0,0 +1,47 @@
|
||||
# Copyright 2020 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
@api.constrains(
|
||||
"product_id", "product_packaging_id", "product_packaging_qty", "product_uom_qty"
|
||||
)
|
||||
def _check_product_packaging_sell_only_by_packaging(self):
|
||||
for line in self:
|
||||
if not line.product_id.sell_only_by_packaging or not line.product_uom_qty:
|
||||
continue
|
||||
|
||||
if (
|
||||
not line.product_packaging_id
|
||||
or float_compare(
|
||||
line.product_packaging_qty,
|
||||
int(line.product_packaging_qty),
|
||||
precision_digits=2,
|
||||
)
|
||||
!= 0
|
||||
):
|
||||
raise ValidationError(
|
||||
self.env._(
|
||||
"Product %s can only be sold with a packaging and a "
|
||||
"packaging quantity.",
|
||||
line.product_id.name,
|
||||
),
|
||||
)
|
||||
|
||||
def _force_qty_with_package(self):
|
||||
self.ensure_one()
|
||||
qty = self.product_id._convert_packaging_qty(
|
||||
self.product_uom_qty, self.product_uom, packaging=self.product_packaging_id
|
||||
)
|
||||
self.product_uom_qty = qty
|
||||
return True
|
||||
|
||||
@api.onchange("product_uom_qty")
|
||||
def _onchange_product_uom_qty(self):
|
||||
self._force_qty_with_package()
|
||||
Reference in New Issue
Block a user