From 0afbf44f1d917595888bb9ba3296ca99959ecbba Mon Sep 17 00:00:00 2001 From: Telmo Santos Date: Thu, 21 Sep 2023 15:19:13 +0200 Subject: [PATCH] [ADD] product_packaging_container_deposit --- .../README.rst | 78 ++++ .../__init__.py | 1 + .../__manifest__.py | 22 + .../models/__init__.py | 4 + .../container_deposit_order_line_mixin.py | 82 ++++ .../models/container_deposit_order_mixin.py | 78 ++++ .../models/product_product.py | 43 ++ .../models/stock_package_type.py | 12 + .../readme/CONTRIBUTORS.rst | 2 + .../readme/DESCRIPTION.rst | 4 + .../static/description/index.html | 423 ++++++++++++++++++ .../tests/__init__.py | 2 + .../tests/common.py | 86 ++++ ...est_product_packaging_container_deposit.py | 30 ++ .../views/stock_package_type_views.xml | 14 + .../product_packaging_container_deposit | 1 + .../setup.py | 6 + 17 files changed, 888 insertions(+) create mode 100644 product_packaging_container_deposit/README.rst create mode 100644 product_packaging_container_deposit/__init__.py create mode 100644 product_packaging_container_deposit/__manifest__.py create mode 100644 product_packaging_container_deposit/models/__init__.py create mode 100644 product_packaging_container_deposit/models/container_deposit_order_line_mixin.py create mode 100644 product_packaging_container_deposit/models/container_deposit_order_mixin.py create mode 100644 product_packaging_container_deposit/models/product_product.py create mode 100644 product_packaging_container_deposit/models/stock_package_type.py create mode 100644 product_packaging_container_deposit/readme/CONTRIBUTORS.rst create mode 100644 product_packaging_container_deposit/readme/DESCRIPTION.rst create mode 100644 product_packaging_container_deposit/static/description/index.html create mode 100644 product_packaging_container_deposit/tests/__init__.py create mode 100644 product_packaging_container_deposit/tests/common.py create mode 100644 product_packaging_container_deposit/tests/test_product_packaging_container_deposit.py create mode 100644 product_packaging_container_deposit/views/stock_package_type_views.xml create mode 120000 setup/product_packaging_container_deposit/odoo/addons/product_packaging_container_deposit create mode 100644 setup/product_packaging_container_deposit/setup.py diff --git a/product_packaging_container_deposit/README.rst b/product_packaging_container_deposit/README.rst new file mode 100644 index 00000000000..3a3336d29a6 --- /dev/null +++ b/product_packaging_container_deposit/README.rst @@ -0,0 +1,78 @@ +=================================== +Product Packaging Container Deposit +=================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-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%2Fproduct--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/16.0/product_packaging_container_deposit + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_packaging_container_deposit + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/product-attribute&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allow to indicate on a product packaging package type that it comes with a container deposit. The deposit product should be a service product. +For instance, you can create a package type "plastic box" and indicate as container deposit "plastic box deposit". + +Use the corresponding sales and purchase modules to add the deposit fees in the order. Each packaging level can have a deposit. The biggest package type of each packaging level will be used in the computation unless a specific package type is given on the order line. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp +* BCIM + +Contributors +~~~~~~~~~~~~ + +* Telmo Santos +* Jacques-Etienne Baudoux (BCIM) + +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/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_packaging_container_deposit/__init__.py b/product_packaging_container_deposit/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/product_packaging_container_deposit/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_packaging_container_deposit/__manifest__.py b/product_packaging_container_deposit/__manifest__.py new file mode 100644 index 00000000000..d7b031467fb --- /dev/null +++ b/product_packaging_container_deposit/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2023 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Product Packaging Container Deposit", + "version": "16.0.1.0.0", + "development_status": "Beta", + "category": "Product", + "summary": "Add container deposit fees in a order", + "author": "Camptocamp, BCIM, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-attribute", + "license": "AGPL-3", + "depends": [ + "stock", + "product_packaging_level", + ], + "data": [ + "views/stock_package_type_views.xml", + ], + "installable": True, + "auto_install": False, +} diff --git a/product_packaging_container_deposit/models/__init__.py b/product_packaging_container_deposit/models/__init__.py new file mode 100644 index 00000000000..438e2d36234 --- /dev/null +++ b/product_packaging_container_deposit/models/__init__.py @@ -0,0 +1,4 @@ +from . import stock_package_type +from . import product_product +from . import container_deposit_order_line_mixin +from . import container_deposit_order_mixin diff --git a/product_packaging_container_deposit/models/container_deposit_order_line_mixin.py b/product_packaging_container_deposit/models/container_deposit_order_line_mixin.py new file mode 100644 index 00000000000..77a5c11f2cb --- /dev/null +++ b/product_packaging_container_deposit/models/container_deposit_order_line_mixin.py @@ -0,0 +1,82 @@ +# Copyright 2023 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class OrderLineMixin(models.AbstractModel): + """ + Provide common features to order lines. + Supported order lines: purchase.order.line, sale.order.line. + """ + + _name = "container.deposit.order.line.mixin" + _description = "Container Deposit Order Line Mixin" + + is_container_deposit = fields.Boolean() + + def _get_product_qty_field(self): + raise NotImplementedError() + + def _get_product_qty_delivered_received_field(self): + raise NotImplementedError() + + def _get_order_lines_container_deposit_quantities(self): + """Get container deposit quantities by product. + + :return: a dict with quantity of container deposit + + { + container_deposit_product: [quantity, quantity_delivered_received] + } + """ + deposit_product_qties = {} + for line in self: + line_deposit_qties = ( + line.product_id.get_product_container_deposit_quantities( + line[self._get_product_qty_field()], + forced_packaging=line.product_packaging_id, + ) + ) + line_deposit_dlvd_rcvd_qties = ( + line.product_id.get_product_container_deposit_quantities( + line[self._get_product_qty_delivered_received_field()], + forced_packaging=line.product_packaging_id, + ) + ) + + for plevel in line_deposit_qties: + product = line_deposit_qties[plevel][0] + qty = line_deposit_qties[plevel][1] + if qty == 0: + continue + if plevel in line_deposit_dlvd_rcvd_qties: + dlvd_rcvd = line_deposit_dlvd_rcvd_qties[plevel][1] + else: + dlvd_rcvd = 0 + if product in deposit_product_qties: + deposit_product_qties[product][0] += qty + deposit_product_qties[product][1] += dlvd_rcvd + else: + deposit_product_qties[product] = [qty, dlvd_rcvd] + return deposit_product_qties + + @api.model_create_multi + def create(self, vals_list): + lines = super().create(vals_list) + if not self.env.context.get( + "skip_update_container_deposit" + ) and not self.env.context.get("from_copy"): + orders = lines.mapped("order_id") + orders.update_order_container_deposit_quantity() + return lines + + def write(self, vals): + res = super().write(vals) + # Context var to avoid recursive calls when updating container deposit + if not self.env.context.get("skip_update_container_deposit") and ( + self._get_product_qty_field() in vals + or self._get_product_qty_delivered_received_field() in vals + ): + self.order_id.update_order_container_deposit_quantity() + return res diff --git a/product_packaging_container_deposit/models/container_deposit_order_mixin.py b/product_packaging_container_deposit/models/container_deposit_order_mixin.py new file mode 100644 index 00000000000..2a6e05c7f83 --- /dev/null +++ b/product_packaging_container_deposit/models/container_deposit_order_mixin.py @@ -0,0 +1,78 @@ +# Copyright 2023 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class OrderMixin(models.AbstractModel): + """This mixin should only be inherited by purchase.order and sale.order models.""" + + _name = "container.deposit.order.mixin" + _description = "Container Deposit Order Mixin" + + def prepare_deposit_container_line(self, product, qty): + self.ensure_one() + values = { + "name": product.name, + "product_id": product.id, + self.order_line._get_product_qty_field(): qty, + "is_container_deposit": True, + "order_id": self.id, + # Bottom of the order + "sequence": 999, + } + return values + + def _get_order_line_field(self): + raise NotImplementedError() + + def update_order_container_deposit_quantity(self): + if self.env.context.get("skip_update_container_deposit"): + return + self = self.with_context(skip_update_container_deposit=True) + for order in self: + # Lines to compute container deposit + lines_to_comp_deposit = order[self._get_order_line_field()].filtered( + lambda ln: ( + ln.product_packaging_id.package_type_id.container_deposit_product_id + ) + or line.product_id.packaging_ids + ) + deposit_container_qties = ( + lines_to_comp_deposit._get_order_lines_container_deposit_quantities() + ) + for line in self[self._get_order_line_field()]: + if not line.is_container_deposit: + continue + qty, qty_dlvd_rcvd = deposit_container_qties.pop( + line["product_id"], [False, False] + ) + if not qty: + if order.state == "draft": + line.unlink() + else: + line.write( + { + line._get_product_qty_field(): 0, + } + ) + else: + line.write( + { + line._get_product_qty_field(): qty, + line._get_product_qty_delivered_received_field(): qty_dlvd_rcvd, + } + ) + values_lst = [] + for product in deposit_container_qties: + if deposit_container_qties[product][0] > 0: + values = order.prepare_deposit_container_line( + product, deposit_container_qties[product][0] + ) + values_lst.append(values) + order[self._get_order_line_field()].create(values_lst) + + def copy(self, default=None): + return super( + OrderMixin, self.with_context(skip_update_container_deposit=True) + ).copy(default=default) diff --git a/product_packaging_container_deposit/models/product_product.py b/product_packaging_container_deposit/models/product_product.py new file mode 100644 index 00000000000..147df3efefb --- /dev/null +++ b/product_packaging_container_deposit/models/product_product.py @@ -0,0 +1,43 @@ +# Copyright 2023 Camptocamp (). +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import models +from odoo.tools import groupby + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def get_product_container_deposit_quantities(self, qty, forced_packaging=False): + """Get the quantity of deposit per packaging level for a given product quantity + + :return a dict with quantity of deposit per packaging level for a given product quantity + { + "PL1": (CP1, QTY), + "PLn": (CPn, QTYn) + } + """ + + def get_sort_key(packaging): + return ( + packaging.packaging_level_id, + packaging != forced_packaging, + packaging.qty < qty, + -packaging.qty, + ) + + pack_qties = {} + if qty > 0: + # Sort by forced_packaging, fitting packagings, biggest packaging + packagings = self.packaging_ids.sorted(key=get_sort_key) + for plevel, packs in groupby(packagings, lambda p: p.packaging_level_id): + container_deposit = packs[ + 0 + ].package_type_id.container_deposit_product_id + if not container_deposit: + continue + pack_qties[plevel] = ( + container_deposit, + qty // packs[0].qty, + ) + return pack_qties diff --git a/product_packaging_container_deposit/models/stock_package_type.py b/product_packaging_container_deposit/models/stock_package_type.py new file mode 100644 index 00000000000..567666f7ab3 --- /dev/null +++ b/product_packaging_container_deposit/models/stock_package_type.py @@ -0,0 +1,12 @@ +# Copyright 2023 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class PackageType(models.Model): + _inherit = "stock.package.type" + + container_deposit_product_id = fields.Many2one( + "product.product", domain=[("type", "=", "service")] + ) diff --git a/product_packaging_container_deposit/readme/CONTRIBUTORS.rst b/product_packaging_container_deposit/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..9f18b1b4106 --- /dev/null +++ b/product_packaging_container_deposit/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Telmo Santos +* Jacques-Etienne Baudoux (BCIM) diff --git a/product_packaging_container_deposit/readme/DESCRIPTION.rst b/product_packaging_container_deposit/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..9c569b8bb3d --- /dev/null +++ b/product_packaging_container_deposit/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +Allow to indicate on a product packaging package type that it comes with a container deposit. The deposit product should be a service product. +For instance, you can create a package type "plastic box" and indicate as container deposit "plastic box deposit". + +Use the corresponding sales and purchase modules to add the deposit fees in the order. Each packaging level can have a deposit. The biggest package type of each packaging level will be used in the computation unless a specific package type is given on the order line. diff --git a/product_packaging_container_deposit/static/description/index.html b/product_packaging_container_deposit/static/description/index.html new file mode 100644 index 00000000000..8f4aad04825 --- /dev/null +++ b/product_packaging_container_deposit/static/description/index.html @@ -0,0 +1,423 @@ + + + + + + +Product Packaging Container Deposit + + + +
+

Product Packaging Container Deposit

+ + +

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

Allow to indicate on a product packaging package type that it comes with a container deposit. The deposit product should be a service product. +For instance, you can create a package type “plastic box” and indicate as container deposit “plastic box deposit”.

+

Use the corresponding sales and purchase modules to add the deposit fees in the order. Each packaging level can have a deposit. The biggest package type of each packaging level will be used in the computation unless a specific package type is given on the order line.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
  • BCIM
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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/product-attribute project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_packaging_container_deposit/tests/__init__.py b/product_packaging_container_deposit/tests/__init__.py new file mode 100644 index 00000000000..41fe0df9b19 --- /dev/null +++ b/product_packaging_container_deposit/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_product_packaging_container_deposit diff --git a/product_packaging_container_deposit/tests/common.py b/product_packaging_container_deposit/tests/common.py new file mode 100644 index 00000000000..aeccfea34a8 --- /dev/null +++ b/product_packaging_container_deposit/tests/common.py @@ -0,0 +1,86 @@ +# Copyright 2023 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.tests import common + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + + +class Common(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env["base"].with_context(**DISABLED_MAIL_CONTEXT).env + cls.env.ref("stock.group_tracking_lot").users += cls.env.user + cls.package_type_pallet = cls.env.ref("stock.package_type_01") + cls.package_type_box = cls.env.ref("stock.package_type_02") + cls.package_type_pallet.container_deposit_product_id = cls.env[ + "product.product" + ].create( + { + "name": "EUROPAL", + "detailed_type": "service", + } + ) + cls.package_type_box.container_deposit_product_id = cls.env[ + "product.product" + ].create( + { + "name": "Box", + "detailed_type": "service", + } + ) + cls.product_packaging_level_pallet = cls.env["product.packaging.level"].create( + { + "name": "PALLET", + "code": "PAL", + "sequence": 1, + "name_policy": "by_package_type", + } + ) + cls.product_packaging_level_box = cls.env["product.packaging.level"].create( + { + "name": "BOX", + "code": "BOX", + "sequence": 1, + "name_policy": "by_package_type", + } + ) + cls.packaging = cls.env["product.packaging"].create( + [ + { + "name": "Box of 12", + "qty": 12, + "package_type_id": cls.package_type_box.id, + "packaging_level_id": cls.product_packaging_level_box.id, + }, + { + "name": "Box of 24", + "qty": 24, + "package_type_id": cls.package_type_box.id, + "packaging_level_id": cls.product_packaging_level_box.id, + }, + { + "name": "EU pallet", + "qty": 240, + "package_type_id": cls.package_type_pallet.id, + "packaging_level_id": cls.product_packaging_level_pallet.id, + }, + ] + ) + + cls.product_a = cls.env["product.product"].create( + {"name": "Product A", "packaging_ids": [(6, 0, cls.packaging.ids)]} + ) + + # Copy packaging of product A to product B + cls.product_b = cls.env["product.product"].create( + { + "name": "Product B", + "packaging_ids": [(6, 0, [pack.copy().id for pack in cls.packaging])], + } + ) + cls.product_c = cls.env["product.product"].create( + {"name": "Product Test C (No packaging)"} + ) + cls.pallet = cls.package_type_pallet.container_deposit_product_id + cls.box = cls.package_type_box.container_deposit_product_id diff --git a/product_packaging_container_deposit/tests/test_product_packaging_container_deposit.py b/product_packaging_container_deposit/tests/test_product_packaging_container_deposit.py new file mode 100644 index 00000000000..4767f816ec5 --- /dev/null +++ b/product_packaging_container_deposit/tests/test_product_packaging_container_deposit.py @@ -0,0 +1,30 @@ +# Copyright 2023 Camptocamp (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from .common import Common + + +class TestProductPackagingContainerDeposit(Common): + # TODO: test the mixins using fake model + odoo-test-helper to not depend on so and po tests + def test_product_container_deposit_qties_with_not_set_container_deposit_on_packaging_type( + self, + ): + self.package_type_pallet.container_deposit_product_id = False + self.package_type_box.container_deposit_product_id = False + packaging_qties = self.product_a.get_product_container_deposit_quantities(280) + self.assertEqual(packaging_qties, {}) + + def test_product_container_deposit_quantities_per_packaging_level(self): + packaging_qties = self.product_a.get_product_container_deposit_quantities(280) + self.assertEqual( + packaging_qties, + { + self.product_packaging_level_pallet: ( + self.package_type_pallet.container_deposit_product_id, + 1, + ), + self.product_packaging_level_box: ( + self.package_type_box.container_deposit_product_id, + 11, + ), + }, + ) diff --git a/product_packaging_container_deposit/views/stock_package_type_views.xml b/product_packaging_container_deposit/views/stock_package_type_views.xml new file mode 100644 index 00000000000..1d71663cb42 --- /dev/null +++ b/product_packaging_container_deposit/views/stock_package_type_views.xml @@ -0,0 +1,14 @@ + + + + + stock.package.type.form.container.deposit + stock.package.type + + + + + + + + diff --git a/setup/product_packaging_container_deposit/odoo/addons/product_packaging_container_deposit b/setup/product_packaging_container_deposit/odoo/addons/product_packaging_container_deposit new file mode 120000 index 00000000000..116726bcfbd --- /dev/null +++ b/setup/product_packaging_container_deposit/odoo/addons/product_packaging_container_deposit @@ -0,0 +1 @@ +../../../../product_packaging_container_deposit \ No newline at end of file diff --git a/setup/product_packaging_container_deposit/setup.py b/setup/product_packaging_container_deposit/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/product_packaging_container_deposit/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)