From c3e741ce16664b00212e04cb54e19f3e1543c195 Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Tue, 16 Apr 2024 14:35:47 +0700 Subject: [PATCH] [IMP] stock_orderpoint_mto_as_mts --- stock_orderpoint_mto_as_mts/README.rst | 3 +- stock_orderpoint_mto_as_mts/__manifest__.py | 4 +- .../data/stock_data.xml | 8 - .../models/__init__.py | 4 +- stock_orderpoint_mto_as_mts/models/product.py | 45 ------ .../models/product_product.py | 141 ++++++++++++++++++ .../models/sale_order.py | 86 ----------- .../models/stock_move.py | 18 --- .../models/stock_warehouse.py | 4 +- .../readme/CONTRIBUTORS.rst | 1 + .../static/description/index.html | 16 +- stock_orderpoint_mto_as_mts/tests/__init__.py | 2 +- .../test_sale_stock_mto_as_mts_orderpoint.py | 103 ------------- .../tests/test_stock_orderpoint_mto_as_mts.py | 43 ++++++ .../views/stock_warehouse_views.xml | 13 ++ 15 files changed, 214 insertions(+), 277 deletions(-) delete mode 100644 stock_orderpoint_mto_as_mts/data/stock_data.xml delete mode 100644 stock_orderpoint_mto_as_mts/models/product.py create mode 100644 stock_orderpoint_mto_as_mts/models/product_product.py delete mode 100644 stock_orderpoint_mto_as_mts/models/sale_order.py delete mode 100644 stock_orderpoint_mto_as_mts/models/stock_move.py delete mode 100644 stock_orderpoint_mto_as_mts/tests/test_sale_stock_mto_as_mts_orderpoint.py create mode 100644 stock_orderpoint_mto_as_mts/tests/test_stock_orderpoint_mto_as_mts.py create mode 100644 stock_orderpoint_mto_as_mts/views/stock_warehouse_views.xml diff --git a/stock_orderpoint_mto_as_mts/README.rst b/stock_orderpoint_mto_as_mts/README.rst index 5495717a..e4f8c93a 100644 --- a/stock_orderpoint_mto_as_mts/README.rst +++ b/stock_orderpoint_mto_as_mts/README.rst @@ -7,7 +7,7 @@ Sale Stock Mto As Mts Orderpoint !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:9d20edbec95507caa98c58ff6804d63697bb6169bf0b50f59bffba1a8b383b0b + !! source digest: sha256:c8b1ce07ee2789445838ad44716c9e14a30bc2f72ed5b4cf970336217b7fdf4e !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png @@ -81,6 +81,7 @@ Contributors * Akim Juillerat * Jacques-Etienne Baudoux (BCIM) +* ACSONE SA/NV * Dung Tran Other credits diff --git a/stock_orderpoint_mto_as_mts/__manifest__.py b/stock_orderpoint_mto_as_mts/__manifest__.py index 74b1ff0c..7fe10bfa 100644 --- a/stock_orderpoint_mto_as_mts/__manifest__.py +++ b/stock_orderpoint_mto_as_mts/__manifest__.py @@ -11,8 +11,8 @@ "license": "AGPL-3", "application": False, "installable": True, - "depends": ["sale_stock", "stock_orderpoint_manual_procurement"], + "depends": ["base_partition", "product_route_mto", "stock"], "data": [ - "data/stock_data.xml", + "views/stock_warehouse_views.xml", ], } diff --git a/stock_orderpoint_mto_as_mts/data/stock_data.xml b/stock_orderpoint_mto_as_mts/data/stock_data.xml deleted file mode 100644 index b0c97c58..00000000 --- a/stock_orderpoint_mto_as_mts/data/stock_data.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/stock_orderpoint_mto_as_mts/models/__init__.py b/stock_orderpoint_mto_as_mts/models/__init__.py index e5bab062..30f05258 100644 --- a/stock_orderpoint_mto_as_mts/models/__init__.py +++ b/stock_orderpoint_mto_as_mts/models/__init__.py @@ -1,4 +1,2 @@ -from . import product -from . import sale_order -from . import stock_move +from . import product_product from . import stock_warehouse diff --git a/stock_orderpoint_mto_as_mts/models/product.py b/stock_orderpoint_mto_as_mts/models/product.py deleted file mode 100644 index 95f998d2..00000000 --- a/stock_orderpoint_mto_as_mts/models/product.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2020 Camptocamp SA -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import models - - -class ProductTemplate(models.Model): - - _inherit = "product.template" - - def write(self, vals): - # Archive orderpoints when MTO route is removed - if "route_ids" not in vals: - return super().write(vals) - mto_products = self._filter_mto_products() - res = super().write(vals) - not_mto_products = self._filter_mto_products(mto=False) - # products to update are the intersection of both recordsets - products_to_update = mto_products & not_mto_products - if products_to_update: - products_to_update._archive_orderpoints_on_mto_removal() - return res - - def _filter_mto_products(self, mto=True): - mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) - if mto: - func = lambda p: mto_route in p.route_ids # noqa - else: - func = lambda p: mto_route not in p.route_ids # noqa - return self.filtered(func) - - def _get_orderpoints_to_archive_domain(self): - warehouses = self.env["stock.warehouse"].search([]) - locations = warehouses._get_locations_for_mto_orderpoints() - return [ - ("product_id", "in", self.mapped("product_variant_ids").ids), - ("product_min_qty", "=", 0.0), - ("product_max_qty", "=", 0.0), - ("location_id", "in", locations.ids), - ] - - def _archive_orderpoints_on_mto_removal(self): - domain = self._get_orderpoints_to_archive_domain() - ops = self.env["stock.warehouse.orderpoint"].search(domain) - if ops: - ops.write({"active": False}) diff --git a/stock_orderpoint_mto_as_mts/models/product_product.py b/stock_orderpoint_mto_as_mts/models/product_product.py new file mode 100644 index 00000000..7a026d1d --- /dev/null +++ b/stock_orderpoint_mto_as_mts/models/product_product.py @@ -0,0 +1,141 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + is_missing_default_orderpoint_for_mto = fields.Boolean( + compute="_compute_is_missing_default_orderpoint_for_mto", + ) + + @api.depends("is_mto", "orderpoint_ids", "type") + def _compute_is_missing_default_orderpoint_for_mto(self): + default_company = self.env["res.company"]._get_main_company() + for product in self: + company = product.company_id or default_company + len_wh = len( + self.env["stock.warehouse"].search([("company_id", "=", company.id)]) + ) + len_wh_orderpoint = len( + product.orderpoint_ids.partition("warehouse_id").keys() + ) + product.is_missing_default_orderpoint_for_mto = ( + product.is_mto + and product.type == "product" + and len_wh == len_wh_orderpoint + ) + + def _create_default_orderpoint_for_mto(self): + default_company = self.env["res.company"]._get_main_company() + for company, products in self.partition("company_id").items(): + company = company or default_company + warehouses = self.env["stock.warehouse"].search( + [("company_id", "=", company.id)] + ) + for product in products: + if not product.is_missing_default_orderpoint_for_mto: + continue + for warehouse in warehouses: + product._get_mto_orderpoint(warehouse) + + def _get_mto_orderpoint(self, warehouse): + self.ensure_one() + orderpoint = ( + self.env["stock.warehouse.orderpoint"] + .with_context(active_test=False) + .search( + [ + ("product_id", "=", self.id), + ( + "location_id", + "=", + warehouse._get_locations_for_mto_orderpoints().id, + ), + ], + limit=1, + ) + ) + if orderpoint and not orderpoint.active: + orderpoint.write( + {"active": True, "product_min_qty": 0.0, "product_max_qty": 0.0} + ) + elif not orderpoint: + vals = self._prepare_missing_orderpoint_vals(warehouse) + self.env["stock.warehouse.orderpoint"].create(vals) + return orderpoint + + def _prepare_missing_orderpoint_vals(self, warehouse): + self.ensure_one() + return { + "warehouse_id": warehouse.id, + "product_id": self.id, + "company_id": warehouse.company_id.id, + "product_min_qty": 0, + "product_max_qty": 0, + "location_id": warehouse._get_locations_for_mto_orderpoints().id, + "product_uom": self.uom_id.id, + } + + def _ensure_default_orderpoint_for_mto(self): + """Ensure that a default orderpoint is created for the MTO products. + + that have no orderpoint yet. + """ + self.filtered( + "is_missing_default_orderpoint_for_mto" + )._create_default_orderpoint_for_mto() + + @api.model_create_multi + def create(self, vals_list): + products = super().create(vals_list) + products.sudo()._ensure_default_orderpoint_for_mto() + return products + + def write(self, vals): + # Archive orderpoints when MTO route is removed + if "route_ids" not in vals: + res = super().write(vals) + self.sudo()._ensure_default_orderpoint_for_mto() + return res + mto_products = self._filter_mto_products() + res = super().write(vals) + not_mto_products = self._filter_mto_products(mto=False) + # products to update are the intersection of both recordsets + products_to_update = mto_products & not_mto_products + if products_to_update: + products_to_update._archive_orderpoints_on_mto_removal() + return res + + def _filter_mto_products(self, mto=True): + if mto: + func = lambda p: p.is_mto # noqa + else: + func = lambda p: not p.is_mto # noqa + return self.filtered(func) + + def _get_orderpoints_to_archive_domain(self): + domain = [] + warehouses = self.env["stock.warehouse"].search( + [("archive_orderpoints_mto_removal", "=", True)] + ) + if warehouses: + locations = warehouses._get_locations_for_mto_orderpoints() + domain.extend( + [ + ("product_id", "in", self.ids), + ("product_min_qty", "=", 0.0), + ("product_max_qty", "=", 0.0), + ("location_id", "in", locations.ids), + ] + ) + return domain + + def _archive_orderpoints_on_mto_removal(self): + domain = self._get_orderpoints_to_archive_domain() + if domain: + ops = self.env["stock.warehouse.orderpoint"].search(domain) + if ops: + ops.write({"active": False}) diff --git a/stock_orderpoint_mto_as_mts/models/sale_order.py b/stock_orderpoint_mto_as_mts/models/sale_order.py deleted file mode 100644 index 6846c143..00000000 --- a/stock_orderpoint_mto_as_mts/models/sale_order.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright 2020 Camptocamp SA -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import models - - -class SaleOrderLine(models.Model): - - _inherit = "sale.order.line" - - def _action_launch_stock_rule(self, previous_product_uom_qty=False): - res = super()._action_launch_stock_rule( - previous_product_uom_qty=previous_product_uom_qty - ) - self._run_orderpoints_for_mto_products() - return res - - def _run_orderpoints_for_mto_products(self): - orderpoints_to_procure_ids = [] - mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) - if not mto_route: - return - for line in self: - delivery_moves = line.move_ids.filtered( - lambda m: m.picking_id.picking_type_code == "outgoing" - and m.state not in ("done", "cancel") - ) - for delivery_move in delivery_moves: - if ( - not delivery_move.is_from_mto_route - or mto_route not in line.product_id.route_ids - ): - continue - orderpoint = line._get_mto_orderpoint(delivery_move.product_id) - if orderpoint.procure_recommended_qty: - orderpoints_to_procure_ids.append(orderpoint.id) - wiz = ( - self.env["make.procurement.orderpoint"] - .with_context( - **{ - "active_model": "stock.warehouse.orderpoint", - "active_ids": orderpoints_to_procure_ids, - } - ) - .create({}) - ) - wiz.make_procurement() - - def _get_mto_orderpoint(self, product_id): - self.ensure_one() - warehouse = self.warehouse_id or self.order_id.warehouse_id - orderpoint = ( - self.env["stock.warehouse.orderpoint"] - .with_context(active_test=False) - .search( - [ - ("product_id", "=", product_id.id), - ( - "location_id", - "=", - warehouse._get_locations_for_mto_orderpoints().id, - ), - ], - limit=1, - ) - ) - if orderpoint and not orderpoint.active: - orderpoint.write( - {"active": True, "product_min_qty": 0.0, "product_max_qty": 0.0} - ) - elif not orderpoint: - orderpoint = ( - self.env["stock.warehouse.orderpoint"] - .sudo() - .create( - { - "product_id": self.product_id.id, - "warehouse_id": warehouse.id, - "location_id": ( - warehouse._get_locations_for_mto_orderpoints().id - ), - "product_min_qty": 0.0, - "product_max_qty": 0.0, - } - ) - ) - return orderpoint diff --git a/stock_orderpoint_mto_as_mts/models/stock_move.py b/stock_orderpoint_mto_as_mts/models/stock_move.py deleted file mode 100644 index a9039460..00000000 --- a/stock_orderpoint_mto_as_mts/models/stock_move.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2020 Camptocamp SA -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import fields, models - - -class StockMove(models.Model): - - _inherit = "stock.move" - - is_from_mto_route = fields.Boolean(compute="_compute_is_from_mto_route") - - def _compute_is_from_mto_route(self): - mto_route = self.env.ref("stock.route_warehouse0_mto", raise_if_not_found=False) - if not mto_route: - self.update({"is_from_mto_route": False}) - else: - for move in self: - move.is_from_mto_route = move.rule_id.route_id == mto_route diff --git a/stock_orderpoint_mto_as_mts/models/stock_warehouse.py b/stock_orderpoint_mto_as_mts/models/stock_warehouse.py index 2d742d1b..7360fbf5 100644 --- a/stock_orderpoint_mto_as_mts/models/stock_warehouse.py +++ b/stock_orderpoint_mto_as_mts/models/stock_warehouse.py @@ -1,11 +1,13 @@ # Copyright 2020 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import models +from odoo import fields, models class StockWarehouse(models.Model): _inherit = "stock.warehouse" + archive_orderpoints_mto_removal = fields.Boolean(default=False) + def _get_locations_for_mto_orderpoints(self): return self.mapped("lot_stock_id") diff --git a/stock_orderpoint_mto_as_mts/readme/CONTRIBUTORS.rst b/stock_orderpoint_mto_as_mts/readme/CONTRIBUTORS.rst index 12845dd9..91796eae 100644 --- a/stock_orderpoint_mto_as_mts/readme/CONTRIBUTORS.rst +++ b/stock_orderpoint_mto_as_mts/readme/CONTRIBUTORS.rst @@ -1,3 +1,4 @@ * Akim Juillerat * Jacques-Etienne Baudoux (BCIM) +* ACSONE SA/NV * Dung Tran diff --git a/stock_orderpoint_mto_as_mts/static/description/index.html b/stock_orderpoint_mto_as_mts/static/description/index.html index 5669b646..1045deab 100644 --- a/stock_orderpoint_mto_as_mts/static/description/index.html +++ b/stock_orderpoint_mto_as_mts/static/description/index.html @@ -2,17 +2,16 @@ - + Sale Stock Mto As Mts Orderpoint