Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

134
sell_only_by_packaging/README.rst Executable file
View File

@@ -0,0 +1,134 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
======================
Sell Only By Packaging
======================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c8c6fdcd867077bd039d90af283a0e4d2d6ab67374685deecdd4e44cb2cf37b6
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/sale-workflow/tree/18.0/sell_only_by_packaging
:alt: OCA/sale-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sell_only_by_packaging
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module provides different configuration option to manage packagings
on sale orders.
The creation/update of sale order line will be blocked (by constraints)
if the data on the sale.order.line does not fit with the configuration
of the product's packagings.
It's also possible to force the quantity to sell during
creation/modification of the sale order line if the "Force sale
quantity" is ticked on the packaging and the "Sell only by packaging" is
ticked on product.
For example, if your packaging is set to sell by 5 units and the
employee fill the quantity with 3, the quantity will be automatically
replaced by 5 (it always rounds up).
.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_
**Table of contents**
.. contents::
:local:
Configuration
=============
Following options are available to define which packaging level can be
sold and which product can only be sold by packaging.
- Sell only by packaging: On product template model, this checkbox
restricts sales of these products if no packaging is selected on the
sale order line. If no packaging is selected, it will either be
auto-assigned if the quantity on the sale order line matches a
packaging quantity or an error will be raised.
- Force sale quantity (on the packaging): force rounds up the quantity
during creation/modification of the sale order line with the factor
set on the packaging.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-workflow/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/sale-workflow/issues/new?body=module:%20sell_only_by_packaging%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Camptocamp
* BCIM
Contributors
------------
- Akim Juillerat <akim.juillerat@camptocamp.com>
- Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
- Thomas Nowicki <thomas.nowicki@camptocamp.com>
- François Honoré <francois.honore@acsone.eu>
- Hiep (Nguyen Hoang) <hiepnh@trobz.com>
- Phuc (Tran Thanh) <phuc@trobz.com>
- Duong (Tran Quoc) <duongtq@trobz.com>
- Tris Doan <tridm@trobz.com>
Other credits
-------------
The development and migration of this module has been financially
supported by:
- Camptocamp
This module feature was extracted from the original
`sale-workflow/sale_by_packaging <https://github.com/oca/sale-workflow/tree/14.0/sale_by_packaging>`__
module.
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/18.0/sell_only_by_packaging>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,20 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
{
"name": "Sell Only By Packaging",
"summary": "Manage sale of packaging",
"version": "18.0.1.0.2",
"development_status": "Alpha",
"category": "Sales",
"website": "https://github.com/OCA/sale-workflow",
"author": "Camptocamp, BCIM, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": ["product_packaging_level_salable"],
"data": [
"views/product_packaging.xml",
"views/product_template.xml",
"views/product_product.xml",
],
}

140
sell_only_by_packaging/i18n/es.po Executable file
View File

@@ -0,0 +1,140 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sell_only_by_packaging
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-27 09:42+0000\n"
"PO-Revision-Date: 2023-11-18 17:34+0000\n"
"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_packaging__force_sale_qty
msgid ""
"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)."
msgstr ""
"Determine si durante la creación de una línea de orden de venta, la cantidad "
"debe forzarse con un múltiplo del empaque.\n"
"Ejemplo:\n"
"Vendes un producto por empaque de 5 productos.\n"
"Cuando el usuario pone 3 como cantidad, el sistema puede forzar la cantidad "
"a la unidad superior (5 para este ejemplo)."
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_packaging__force_sale_qty
msgid "Force sale quantity"
msgstr "Cantidad de venta forzada"
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_packaging_level
msgid "Level management for product.packaging"
msgstr "Tipo de administración para empaquetado de producto"
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_product__min_sellable_qty
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_template__min_sellable_qty
msgid "Min Sellable Qty"
msgstr "Cantidad Mínima Vendible"
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_product__min_sellable_qty
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_template__min_sellable_qty
msgid ""
"Minimum sellable quantity, according to the available packagings, if Only "
"Sell by Packaging is set."
msgstr ""
"Cantidad mínima vendible, en función de los embalajes disponibles, si se "
"configura Sólo vender por embalaje."
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_product__sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_template__sell_only_by_packaging
msgid "Only sell by packaging"
msgstr "Solo vendemos por embalaje"
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_packaging_level.py:0
#, python-format
msgid ""
"Packaging level \"{}\" must stay with \"Can be sold\", at least one product "
"configured as \"sell only by packaging\" is using it."
msgstr ""
"El nivel de embalaje \"{}\" debe permanecer con \"Se puede vender\", al "
"menos un producto configurado como \"vender sólo por embalaje\" lo está "
"utilizando."
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_template
msgid "Product"
msgstr "Producto"
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/sale_order_line.py:0
#, python-format
msgid "Product %s can only be sold with a packaging and a packaging quantity."
msgstr ""
"El producto %s solo se puede vender con un embalaje y una cantidad de "
"embalaje."
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_template.py:0
#, python-format
msgid ""
"Product %s cannot be defined to be sold only by packaging if it cannot be "
"sold."
msgstr ""
"No se puede definir que el producto %s se venda solo por embalaje si no se "
"puede vender."
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_template.py:0
#, python-format
msgid ""
"Product %s cannot be defined to be sold only by packaging if it does not "
"have any packaging that can be sold defined."
msgstr ""
"No se puede definir que el producto %s se venda solo por embalaje si no "
"tiene ningún embalaje definido."
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_packaging
msgid "Product Packaging"
msgstr "Empaquetado del producto"
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_product
msgid "Product Variant"
msgstr "Variante de Producto"
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_product__sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_template__sell_only_by_packaging
msgid ""
"Restrict the usage of this product on sale order lines without packaging "
"defined"
msgstr ""
"Restrinja el uso de este producto en las líneas de orden de venta sin "
"embalaje definido"
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea de Órdenes de Venta"

137
sell_only_by_packaging/i18n/it.po Executable file
View File

@@ -0,0 +1,137 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sell_only_by_packaging
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-12-04 09:34+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_packaging__force_sale_qty
msgid ""
"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)."
msgstr ""
"Determina se durante la creazione di una riga ordine di vendita la quantità "
"debba essere forzata con un multiplo dell'imballaggio.\n"
"Esempio:\n"
"Vendi un prodotto con imballaggio di 5 prodotti.\n"
"Quando l'utente inserisce 3 come quantità, il sistema può forzare la "
"quantità all'unità superiore (5 per questo esempio)."
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_packaging__force_sale_qty
msgid "Force sale quantity"
msgstr "Forza quantità di vendita"
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_packaging_level
msgid "Level management for product.packaging"
msgstr "Gestione livello per product.packaging"
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_product__min_sellable_qty
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_template__min_sellable_qty
msgid "Min Sellable Qty"
msgstr "Q.tà minima vendibile"
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_product__min_sellable_qty
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_template__min_sellable_qty
msgid ""
"Minimum sellable quantity, according to the available packagings, if Only "
"Sell by Packaging is set."
msgstr ""
"Quantità minima vendibile, secondo gli imballaggi disponibili, se \"Vendi "
"solo per imballaggio\" è impostato."
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_product__sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_template__sell_only_by_packaging
msgid "Only sell by packaging"
msgstr "Vendi solo per imballaggio"
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_packaging_level.py:0
#, python-format
msgid ""
"Packaging level \"{}\" must stay with \"Can be sold\", at least one product "
"configured as \"sell only by packaging\" is using it."
msgstr ""
"Il livello imballaggio \"{}\" deve essere impostato come \"Può essere "
"venduto\", dato che almeno un prodotto impostato con \"Vendi solo per "
"imballaggio\" lo sta utilizzando."
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_template
msgid "Product"
msgstr "Prodotto"
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/sale_order_line.py:0
#, python-format
msgid "Product %s can only be sold with a packaging and a packaging quantity."
msgstr ""
"Il prodotto %s può essere venduto solo con un imballaggio e una quantità "
"imballaggio."
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_template.py:0
#, python-format
msgid ""
"Product %s cannot be defined to be sold only by packaging if it cannot be "
"sold."
msgstr ""
"Il prodotto %s non può essere definito per essere venduto solo con "
"imballaggio se \"Può essere venduto\" non è impostato."
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_template.py:0
#, python-format
msgid ""
"Product %s cannot be defined to be sold only by packaging if it does not "
"have any packaging that can be sold defined."
msgstr ""
"Il prodotto %s non può essere definito per essere venduto solo con "
"imballaggio se non è impostato alcun imballaggio che può essere venduto."
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_packaging
msgid "Product Packaging"
msgstr "Imballaggio prodotto"
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_product
msgid "Product Variant"
msgstr "Variante prodotto"
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_product__sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_template__sell_only_by_packaging
msgid ""
"Restrict the usage of this product on sale order lines without packaging "
"defined"
msgstr ""
"Non permettere l'uso di questo prodotto nelle righe ordine di vendita senza "
"imballaggio definito"
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_sale_order_line
msgid "Sales Order Line"
msgstr "Riga ordine di vendita"

View File

@@ -0,0 +1,111 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sell_only_by_packaging
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_packaging__force_sale_qty
msgid ""
"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)."
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_packaging__force_sale_qty
msgid "Force sale quantity"
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_packaging_level
msgid "Level management for product.packaging"
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_product__min_sellable_qty
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_template__min_sellable_qty
msgid "Min Sellable Qty"
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_product__min_sellable_qty
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_template__min_sellable_qty
msgid ""
"Minimum sellable quantity, according to the available packagings, if Only "
"Sell by Packaging is set."
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_product__sell_only_by_packaging
#: model:ir.model.fields,field_description:sell_only_by_packaging.field_product_template__sell_only_by_packaging
msgid "Only sell by packaging"
msgstr ""
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_packaging_level.py:0
msgid ""
"Packaging level %s must stay with \"Can be sold\", at least one product "
"configured as \"sell only by packaging\" is using it."
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_template
msgid "Product"
msgstr ""
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/sale_order_line.py:0
msgid "Product %s can only be sold with a packaging and a packaging quantity."
msgstr ""
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_template.py:0
msgid ""
"Product %s cannot be defined to be sold only by packaging if it cannot be "
"sold."
msgstr ""
#. module: sell_only_by_packaging
#. odoo-python
#: code:addons/sell_only_by_packaging/models/product_template.py:0
msgid ""
"Product %s cannot be defined to be sold only by packaging if it does not "
"have any packaging that can be sold defined."
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_packaging
msgid "Product Packaging"
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_product_product
msgid "Product Variant"
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_product__sell_only_by_packaging
#: model:ir.model.fields,help:sell_only_by_packaging.field_product_template__sell_only_by_packaging
msgid ""
"Restrict the usage of this product on sale order lines without packaging "
"defined"
msgstr ""
#. module: sell_only_by_packaging
#: model:ir.model,name:sell_only_by_packaging.model_sale_order_line
msgid "Sales Order Line"
msgstr ""

View 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

View 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).",
)

View 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

View 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

View 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()

View 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()

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,11 @@
Following options are available to define which packaging level can be
sold and which product can only be sold by packaging.
- Sell only by packaging: On product template model, this checkbox
restricts sales of these products if no packaging is selected on the
sale order line. If no packaging is selected, it will either be
auto-assigned if the quantity on the sale order line matches a
packaging quantity or an error will be raised.
- Force sale quantity (on the packaging): force rounds up the quantity
during creation/modification of the sale order line with the factor
set on the packaging.

View File

@@ -0,0 +1,8 @@
- Akim Juillerat \<<akim.juillerat@camptocamp.com>\>
- Jacques-Etienne Baudoux (BCIM) \<<je@bcim.be>\>
- Thomas Nowicki \<<thomas.nowicki@camptocamp.com>\>
- François Honoré \<<francois.honore@acsone.eu>\>
- Hiep (Nguyen Hoang) \<<hiepnh@trobz.com>\>
- Phuc (Tran Thanh) \<<phuc@trobz.com>\>
- Duong (Tran Quoc) \<<duongtq@trobz.com>\>
- Tris Doan \<<tridm@trobz.com>\>

View File

@@ -0,0 +1,7 @@
The development and migration of this module has been financially supported by:
- Camptocamp
This module feature was extracted from the original
[sale-workflow/sale_by_packaging](https://github.com/oca/sale-workflow/tree/14.0/sale_by_packaging)
module.

View File

@@ -0,0 +1,15 @@
This module provides different configuration option to manage packagings
on sale orders.
The creation/update of sale order line will be blocked (by constraints)
if the data on the sale.order.line does not fit with the configuration
of the product's packagings.
It's also possible to force the quantity to sell during
creation/modification of the sale order line if the "Force sale
quantity" is ticked on the packaging and the "Sell only by packaging" is
ticked on product.
For example, if your packaging is set to sell by 5 units and the
employee fill the quantity with 3, the quantity will be automatically
replaced by 5 (it always rounds up).

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,482 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="sell-only-by-packaging">
<h1>Sell Only By Packaging</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c8c6fdcd867077bd039d90af283a0e4d2d6ab67374685deecdd4e44cb2cf37b6
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/sale-workflow/tree/18.0/sell_only_by_packaging"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sell_only_by_packaging"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module provides different configuration option to manage packagings
on sale orders.</p>
<p>The creation/update of sale order line will be blocked (by constraints)
if the data on the sale.order.line does not fit with the configuration
of the products packagings.</p>
<p>Its also possible to force the quantity to sell during
creation/modification of the sale order line if the “Force sale
quantity” is ticked on the packaging and the “Sell only by packaging” is
ticked on product.</p>
<p>For example, if your packaging is set to sell by 5 units and the
employee fill the quantity with 3, the quantity will be automatically
replaced by 5 (it always rounds up).</p>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-6">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
<p>Following options are available to define which packaging level can be
sold and which product can only be sold by packaging.</p>
<ul class="simple">
<li>Sell only by packaging: On product template model, this checkbox
restricts sales of these products if no packaging is selected on the
sale order line. If no packaging is selected, it will either be
auto-assigned if the quantity on the sale order line matches a
packaging quantity or an error will be raised.</li>
<li>Force sale quantity (on the packaging): force rounds up the quantity
during creation/modification of the sale order line with the factor
set on the packaging.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/sale-workflow/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/sale-workflow/issues/new?body=module:%20sell_only_by_packaging%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-3">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-4">Authors</a></h3>
<ul class="simple">
<li>Camptocamp</li>
<li>BCIM</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<ul class="simple">
<li>Akim Juillerat &lt;<a class="reference external" href="mailto:akim.juillerat&#64;camptocamp.com">akim.juillerat&#64;camptocamp.com</a>&gt;</li>
<li>Jacques-Etienne Baudoux (BCIM) &lt;<a class="reference external" href="mailto:je&#64;bcim.be">je&#64;bcim.be</a>&gt;</li>
<li>Thomas Nowicki &lt;<a class="reference external" href="mailto:thomas.nowicki&#64;camptocamp.com">thomas.nowicki&#64;camptocamp.com</a>&gt;</li>
<li>François Honoré &lt;<a class="reference external" href="mailto:francois.honore&#64;acsone.eu">francois.honore&#64;acsone.eu</a>&gt;</li>
<li>Hiep (Nguyen Hoang) &lt;<a class="reference external" href="mailto:hiepnh&#64;trobz.com">hiepnh&#64;trobz.com</a>&gt;</li>
<li>Phuc (Tran Thanh) &lt;<a class="reference external" href="mailto:phuc&#64;trobz.com">phuc&#64;trobz.com</a>&gt;</li>
<li>Duong (Tran Quoc) &lt;<a class="reference external" href="mailto:duongtq&#64;trobz.com">duongtq&#64;trobz.com</a>&gt;</li>
<li>Tris Doan &lt;<a class="reference external" href="mailto:tridm&#64;trobz.com">tridm&#64;trobz.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h3><a class="toc-backref" href="#toc-entry-6">Other credits</a></h3>
<p>The development and migration of this module has been financially
supported by:</p>
<ul class="simple">
<li>Camptocamp</li>
</ul>
<p>This module feature was extracted from the original
<a class="reference external" href="https://github.com/oca/sale-workflow/tree/14.0/sale_by_packaging">sale-workflow/sale_by_packaging</a>
module.</p>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/18.0/sell_only_by_packaging">OCA/sale-workflow</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
from . import test_minimum_sellable_qty
from . import test_sale_only_by_packaging
from . import test_sale_line_onchanges

View File

@@ -0,0 +1,14 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo.addons.product_packaging_level_salable.tests.common import Common
TU_PRODUCT_QTY = 20
PL_PRODUCT_QTY = TU_PRODUCT_QTY * 30
class SellOnlyByPackagingCommon(Common):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.user.groups_id += cls.env.ref("product.group_stock_packaging")

View File

@@ -0,0 +1,58 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo.tests import TransactionCase
class TestMinimumSellableQty(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.product = cls.env.ref("product.product_product_9")
cls.packaging_level_1 = cls.env["product.packaging.level"].create(
{"name": "Packaging level 1", "code": "PL1", "sequence": 1}
)
cls.packaging_level_2 = cls.env["product.packaging.level"].create(
{"name": "Packaging level 1", "code": "PL2", "sequence": 2}
)
cls.packaging_level_3 = cls.env["product.packaging.level"].create(
{"name": "Packaging level 3", "code": "PL3", "sequence": 3}
)
cls.packaging1 = cls.env["product.packaging"].create(
{
"name": "Packaging of five",
"product_id": cls.product.id,
"qty": 5.0,
"packaging_level_id": cls.packaging_level_1.id,
}
)
cls.packaging2 = cls.env["product.packaging"].create(
{
"name": "Packing of 3",
"product_id": cls.product.id,
"qty": 3.0,
"packaging_level_id": cls.packaging_level_2.id,
}
)
def test_min_sellable_qty(self):
"""Check the computation of the minimum sellable quantity."""
self.product.product_tmpl_id.sell_only_by_packaging = False
self.assertEqual(self.product.min_sellable_qty, 0)
self.product.product_tmpl_id.sell_only_by_packaging = True
self.assertEqual(self.product.min_sellable_qty, 3)
self.assertEqual(self.product.product_tmpl_id.min_sellable_qty, 3)
self.packaging2.sales = False
self.assertEqual(self.product.min_sellable_qty, 5)
self.assertEqual(self.product.product_tmpl_id.min_sellable_qty, 5)
self.packaging3 = self.env["product.packaging"].create(
{
"name": "Packing of 2",
"product_id": self.product.id,
"qty": 2.0,
"packaging_level_id": self.packaging_level_3.id,
}
)
self.assertEqual(self.product.min_sellable_qty, 2)
self.assertEqual(self.product.product_tmpl_id.min_sellable_qty, 2)

View File

@@ -0,0 +1,32 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import logging
from odoo.tests import Form
from odoo.addons.product_packaging_level_salable.tests.common import (
PL_PRODUCT_QTY,
TU_PRODUCT_QTY,
)
from .common import SellOnlyByPackagingCommon
class TestPackaging(SellOnlyByPackagingCommon):
def test_compute_qties(self):
with Form(self.order) as so, self.assertLogs(level=logging.WARNING) as logs:
with so.order_line.edit(0) as line:
line.product_packaging_id = self.packaging_tu
line.product_packaging_qty = 31
# catch WARNING by _onchange_product_packaging_id
self.assertEqual(len(logs.records), 1)
self.assertEqual(logs.records[0].levelno, logging.WARNING)
# (20*30)+20 = 31*20 = 620
expected_qty = TU_PRODUCT_QTY + PL_PRODUCT_QTY
self.assertEqual(self.order_line.product_uom_qty, expected_qty)
with Form(self.order) as so:
with so.order_line.edit(0) as line:
line.product_packaging_qty = 30
# 20*30 = 600
expected_qty = PL_PRODUCT_QTY
self.assertEqual(self.order_line.product_uom_qty, expected_qty)

View File

@@ -0,0 +1,113 @@
# Copyright 2020 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
import logging
from odoo.exceptions import ValidationError
from odoo.tests import Form
from odoo.tools import mute_logger
from .common import SellOnlyByPackagingCommon
class TestSaleProductByPackagingOnly(SellOnlyByPackagingCommon):
def test_write_auto_fill_packaging(self):
order_line = self.order.order_line
self.assertFalse(order_line.product_packaging_id)
self.assertFalse(order_line.product_packaging_qty)
order_line.write({"product_uom_qty": 3.0})
self.assertFalse(order_line.product_packaging_id)
self.assertFalse(order_line.product_packaging_qty)
self.product.write({"sell_only_by_packaging": True})
self.assertFalse(order_line.product_packaging_id)
self.assertFalse(order_line.product_packaging_qty)
order_line.write({"product_uom_qty": self.packaging_tu.qty * 2})
self.assertTrue(order_line.product_packaging_id)
self.assertTrue(order_line.product_packaging_qty)
self.assertEqual(order_line.product_packaging_id, self.packaging_tu)
self.assertEqual(order_line.product_packaging_qty, 2)
with self.assertRaises(ValidationError):
order_line.write({"product_packaging_id": False})
def test_error_sale_packaging(self):
# If qty does not match a packaging qty, an exception should be raised
self.product.sell_only_by_packaging = True
with self.assertRaises(ValidationError):
with Form(self.order) as so:
with so.order_line.new() as so_line:
so_line.product_id = self.product
so_line.product_uom_qty = 2
@mute_logger("odoo.tests.common.onchange")
def test_convert_packaging_qty(self):
"""
Test if the function _convert_packaging_qty is correctly applied
during SO line create/edit and if qties are corrects.
:return:
"""
self.product.sell_only_by_packaging = True
packaging = self.packaging_tu
# For this step, the qty is not forced on the packaging
# But the warning will be raise because the value of packaging qty is
# not integer package
with self.assertRaises(ValidationError):
self.order_line.write(
{
"product_packaging_id": packaging,
"product_packaging_qty": 0.6,
}
)
# Now force the qty on the packaging
packaging.force_sale_qty = True
with Form(self.order) as sale_order:
with (
sale_order.order_line.edit(0) as so_line,
self.assertLogs(level=logging.WARNING) as logs,
):
so_line.product_packaging_id = packaging
so_line.product_uom_qty = 52
self.assertAlmostEqual(
so_line.product_uom_qty, 60, places=self.precision
)
so_line.product_uom_qty = 40
self.assertAlmostEqual(
so_line.product_uom_qty, 40, places=self.precision
)
so_line.product_uom_qty = 38
self.assertAlmostEqual(
so_line.product_uom_qty, 40, places=self.precision
)
so_line.product_uom_qty = 22
self.assertAlmostEqual(
so_line.product_uom_qty, 40, places=self.precision
)
so_line.product_uom_qty = 72
self.assertAlmostEqual(
so_line.product_uom_qty, 80, places=self.precision
)
so_line.product_uom_qty = 209.98
self.assertAlmostEqual(
so_line.product_uom_qty, 220, places=self.precision
)
# catch WARNING by _onchange_product_packaging_id
self.assertEqual(len(logs.records), 1)
self.assertEqual(logs.records[0].levelno, logging.WARNING)
def test_onchange_qty_is_not_pack_multiple(self):
"""Check package when qantity is not a multiple of package quantity.
When the uom quantity is changed for a value not a multpile of a
possible package an error is raised.
"""
self.product.write({"sell_only_by_packaging": True})
self.order_line.product_uom_qty = 40 # 2 packs
with self.assertRaises(ValidationError):
self.order_line.product_packaging_qty = 0.9
self.assertAlmostEqual(
self.order_line.product_uom_qty, 18, places=self.precision
)

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="product_packaging_tree_view_inherit" model="ir.ui.view">
<field name="name">product.packaging.list.view.inherit</field>
<field name="model">product.packaging</field>
<field name="inherit_id" ref="product.product_packaging_tree_view" />
<field name="arch" type="xml">
<field name="qty" position="after">
<field name="force_sale_qty" />
</field>
</field>
</record>
<record id="product_packaging_form_view_inherit" model="ir.ui.view">
<field name="name">product.packaging.form.view.inherit</field>
<field name="model">product.packaging</field>
<field name="inherit_id" ref="product.product_packaging_form_view" />
<field name="arch" type="xml">
<field name="barcode" position="before">
<field name="force_sale_qty" />
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="product_product_tree_view" model="ir.ui.view">
<field name="name">product.product.list.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_product_tree_view" />
<field name="arch" type="xml">
<field name="lst_price" position="after">
<field name="sell_only_by_packaging" invisible="1" />
<field
name="min_sellable_qty"
optional="show"
invisible="type != 'product' or not sell_only_by_packaging"
/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 Camptocamp License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="product_template_form_view" model="ir.ui.view">
<field name="name">product.template.form.view</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view" />
<field name="arch" type="xml">
<div name="options" position="inside">
<div invisible="not sale_ok">
<field name="sell_only_by_packaging" />
<label for="sell_only_by_packaging" />
</div>
</div>
<group name="group_general" position="inside">
<field name="min_sellable_qty" />
</group>
</field>
</record>
<record id="product_template_tree_view" model="ir.ui.view">
<field name="name">product.template.list.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_tree_view" />
<field name="arch" type="xml">
<field name="standard_price" position="after">
<field name="sell_only_by_packaging" invisible="1" />
<field name="min_sellable_qty" optional="show" />
</field>
</field>
</record>
</odoo>