From d3d11b12feb736a1c32aef6fa01e8e4a174bf277 Mon Sep 17 00:00:00 2001 From: hveficent Date: Thu, 12 Mar 2020 09:13:57 +0100 Subject: [PATCH] [13.0][ADD] stock_request_mrp [UPD] Update stock_request_mrp.pot [UPD] README.rst --- stock_request_mrp/README.rst | 87 ++++ stock_request_mrp/__init__.py | 2 + stock_request_mrp/__manifest__.py | 22 + stock_request_mrp/hooks.py | 73 +++ stock_request_mrp/i18n/stock_request_mrp.pot | 70 +++ stock_request_mrp/models/__init__.py | 4 + stock_request_mrp/models/mrp_production.py | 69 +++ stock_request_mrp/models/stock_request.py | 56 +++ .../models/stock_request_order.py | 42 ++ stock_request_mrp/models/stock_rule.py | 34 ++ stock_request_mrp/readme/CONTRIBUTORS.rst | 1 + stock_request_mrp/readme/DESCRIPTION.rst | 2 + stock_request_mrp/readme/ROADMAP.rst | 2 + stock_request_mrp/readme/USAGE.rst | 3 + .../security/ir.model.access.csv | 4 + stock_request_mrp/static/description/icon.png | Bin 0 -> 15218 bytes .../static/description/index.html | 435 ++++++++++++++++++ stock_request_mrp/tests/__init__.py | 1 + .../tests/test_stock_request_mrp.py | 218 +++++++++ .../views/mrp_production_views.xml | 37 ++ .../views/stock_request_order_views.xml | 25 + .../views/stock_request_views.xml | 25 + 22 files changed, 1212 insertions(+) create mode 100644 stock_request_mrp/README.rst create mode 100644 stock_request_mrp/__init__.py create mode 100644 stock_request_mrp/__manifest__.py create mode 100644 stock_request_mrp/hooks.py create mode 100644 stock_request_mrp/i18n/stock_request_mrp.pot create mode 100644 stock_request_mrp/models/__init__.py create mode 100644 stock_request_mrp/models/mrp_production.py create mode 100644 stock_request_mrp/models/stock_request.py create mode 100644 stock_request_mrp/models/stock_request_order.py create mode 100644 stock_request_mrp/models/stock_rule.py create mode 100644 stock_request_mrp/readme/CONTRIBUTORS.rst create mode 100644 stock_request_mrp/readme/DESCRIPTION.rst create mode 100644 stock_request_mrp/readme/ROADMAP.rst create mode 100644 stock_request_mrp/readme/USAGE.rst create mode 100644 stock_request_mrp/security/ir.model.access.csv create mode 100644 stock_request_mrp/static/description/icon.png create mode 100644 stock_request_mrp/static/description/index.html create mode 100644 stock_request_mrp/tests/__init__.py create mode 100644 stock_request_mrp/tests/test_stock_request_mrp.py create mode 100644 stock_request_mrp/views/mrp_production_views.xml create mode 100644 stock_request_mrp/views/stock_request_order_views.xml create mode 100644 stock_request_mrp/views/stock_request_views.xml diff --git a/stock_request_mrp/README.rst b/stock_request_mrp/README.rst new file mode 100644 index 000000000..3ca40cef5 --- /dev/null +++ b/stock_request_mrp/README.rst @@ -0,0 +1,87 @@ +================= +Stock Request MRP +================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_request_mrp + :alt: OCA/stock-logistics-warehouse +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-13-0/stock-logistics-warehouse-13-0-stock_request_mrp + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/153/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows for users to be able to display manufacturing orders that have +been created as a consequence of Stock Requests. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +In case that the confirmation of the Stock Request results in an immediate +Manufacturing Order, the user will be able to display the MO's from the Stock +Request form view. + +Known issues / Roadmap +====================== + +* When a Stock Request is cancelled, it does not cancel the quantity included + in the Manufacturing Order. + +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 +~~~~~~~ + +* ForgeFlow + +Contributors +~~~~~~~~~~~~ + +* Héctor Villarreal . + +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/stock-logistics-warehouse `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_request_mrp/__init__.py b/stock_request_mrp/__init__.py new file mode 100644 index 000000000..cc6b6354a --- /dev/null +++ b/stock_request_mrp/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import post_init_hook diff --git a/stock_request_mrp/__manifest__.py b/stock_request_mrp/__manifest__.py new file mode 100644 index 000000000..9f5673c2f --- /dev/null +++ b/stock_request_mrp/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2017-20 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +{ + "name": "Stock Request MRP", + "summary": "Manufacturing request for stock", + "version": "13.0.1.0.0", + "license": "LGPL-3", + "website": "https://github.com/stock-logistics-warehouse", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "category": "Warehouse Management", + "depends": ["stock_request", "mrp"], + "data": [ + "security/ir.model.access.csv", + "views/stock_request_views.xml", + "views/stock_request_order_views.xml", + "views/mrp_production_views.xml", + ], + "installable": True, + "auto_install": True, + "post_init_hook": "post_init_hook", +} diff --git a/stock_request_mrp/hooks.py b/stock_request_mrp/hooks.py new file mode 100644 index 000000000..5f2a68913 --- /dev/null +++ b/stock_request_mrp/hooks.py @@ -0,0 +1,73 @@ +# Copyright 2020 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +import logging + +from odoo import SUPERUSER_ID, api + +logger = logging.getLogger(__name__) + + +def post_init_hook(cr, registry): + """ + The objective of this hook is to link existing MOs + coming from a Stock Request. + """ + logger.info("Linking existing MOs coming from a Stock Request") + link_existing_mos_to_stock_request(cr) + + +def link_existing_mos_to_stock_request(cr): + env = api.Environment(cr, SUPERUSER_ID, dict()) + stock_request_obj = env["stock.request"] + stock_request_order_obj = env["stock.request.order"] + stock_request_allocation_obj = env["stock.request.allocation"] + mrp_production_obj = env["mrp.production"] + mos_with_sr = mrp_production_obj.search([("origin", "ilike", "SR/%")]) + logger.info("Linking %s MOs records" % len(mos_with_sr)) + stock_requests = stock_request_obj.search( + [("name", "in", [mo.origin for mo in mos_with_sr])] + ) + for mo in mos_with_sr: + stock_request = stock_requests.filtered(lambda x: x.name == mo.origin) + if stock_request: + # Link SR to MO + mo.stock_request_ids = [(6, 0, stock_request.ids)] + logger.info("MO {} linked to SR {}".format(mo.name, stock_request.name)) + if ( + not stock_request_allocation_obj.search( + [("stock_request_id", "=", stock_request.id)] + ) + and mo.state != "cancel" + ): + # Create allocation for finish move + logger.info("Create allocation for {}".format(stock_request.name)) + mo.move_finished_ids[0].allocation_ids = [ + ( + 0, + 0, + { + "stock_request_id": request.id, + "requested_product_uom_qty": request.product_qty, + }, + ) + for request in mo.stock_request_ids + ] + + # Update allocations + logger.info("Updating Allocations for SR %s" % stock_request.name) + for ml in mo.finished_move_line_ids.filtered( + lambda m: m.exists() and m.move_id.allocation_ids + ): + qty_done = ml.product_uom_id._compute_quantity( + ml.qty_done, ml.product_id.uom_id + ) + to_allocate_qty = ml.qty_done + for allocation in ml.move_id.allocation_ids: + if allocation.open_product_qty: + allocated_qty = min(allocation.open_product_qty, qty_done) + allocation.allocated_product_qty += allocated_qty + to_allocate_qty -= allocated_qty + stock_request.check_done() + # Update production_ids from SROs + stock_request_order_obj.search([])._compute_production_ids() diff --git a/stock_request_mrp/i18n/stock_request_mrp.pot b/stock_request_mrp/i18n/stock_request_mrp.pot new file mode 100644 index 000000000..5fe73f15b --- /dev/null +++ b/stock_request_mrp/i18n/stock_request_mrp.pot @@ -0,0 +1,70 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_request_mrp +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.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: stock_request_mrp +#: model_terms:ir.ui.view,arch_db:stock_request_mrp.stock_request_order_form +#: model_terms:ir.ui.view,arch_db:stock_request_mrp.view_stock_request_form +msgid "MOs" +msgstr "" + +#. module: stock_request_mrp +#: model:ir.model.fields,field_description:stock_request_mrp.field_stock_request__production_ids +#: model:ir.model.fields,field_description:stock_request_mrp.field_stock_request_order__production_ids +msgid "Manufacturing Orders" +msgstr "" + +#. module: stock_request_mrp +#: model:ir.model.fields,field_description:stock_request_mrp.field_stock_request__production_count +#: model:ir.model.fields,field_description:stock_request_mrp.field_stock_request_order__production_count +msgid "Manufacturing Orders count" +msgstr "" + +#. module: stock_request_mrp +#: model:ir.model,name:stock_request_mrp.model_mrp_production +msgid "Production Order" +msgstr "" + +#. module: stock_request_mrp +#: model:ir.model,name:stock_request_mrp.model_stock_request +msgid "Stock Request" +msgstr "" + +#. module: stock_request_mrp +#: model:ir.model.fields,field_description:stock_request_mrp.field_mrp_production__stock_request_count +msgid "Stock Request #" +msgstr "" + +#. module: stock_request_mrp +#: model:ir.model,name:stock_request_mrp.model_stock_request_order +msgid "Stock Request Order" +msgstr "" + +#. module: stock_request_mrp +#: model:ir.model.fields,field_description:stock_request_mrp.field_mrp_production__stock_request_ids +#: model_terms:ir.ui.view,arch_db:stock_request_mrp.mrp_production_form_view +msgid "Stock Requests" +msgstr "" + +#. module: stock_request_mrp +#: model:ir.model,name:stock_request_mrp.model_stock_rule +msgid "Stock Rule" +msgstr "" + +#. module: stock_request_mrp +#: code:addons/stock_request_mrp/models/stock_request.py:0 +#, python-format +msgid "" +"You have linked to a Manufacture Order that belongs to another company." +msgstr "" diff --git a/stock_request_mrp/models/__init__.py b/stock_request_mrp/models/__init__.py new file mode 100644 index 000000000..ee37123b3 --- /dev/null +++ b/stock_request_mrp/models/__init__.py @@ -0,0 +1,4 @@ +from . import mrp_production +from . import stock_rule +from . import stock_request +from . import stock_request_order diff --git a/stock_request_mrp/models/mrp_production.py b/stock_request_mrp/models/mrp_production.py new file mode 100644 index 000000000..c2ab9249c --- /dev/null +++ b/stock_request_mrp/models/mrp_production.py @@ -0,0 +1,69 @@ +# Copyright 2020 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class MrpProduction(models.Model): + _inherit = "mrp.production" + + stock_request_ids = fields.Many2many( + "stock.request", + "mrp_production_stock_request_rel", + "mrp_production_id", + "stock_request_id", + string="Stock Requests", + ) + stock_request_count = fields.Integer( + "Stock Request #", compute="_compute_stock_request_ids" + ) + + @api.depends("stock_request_ids") + def _compute_stock_request_ids(self): + for rec in self: + rec.stock_request_count = len(rec.stock_request_ids) + + def action_view_stock_request(self): + """ + :return dict: dictionary value for created view + """ + action = self.env.ref("stock_request.action_stock_request_form").read()[0] + + requests = self.mapped("stock_request_ids") + if len(requests) > 1: + action["domain"] = [("id", "in", requests.ids)] + elif requests: + action["views"] = [ + (self.env.ref("stock_request.view_stock_request_form").id, "form") + ] + action["res_id"] = requests.id + return action + + def _get_finished_move_value( + self, + product_id, + product_uom_qty, + product_uom, + operation_id=False, + byproduct_id=False, + ): + res = super()._get_finished_move_value( + product_id, + product_uom_qty, + product_uom, + operation_id=operation_id, + byproduct_id=byproduct_id, + ) + if self.stock_request_ids: + res["allocation_ids"] = [ + ( + 0, + 0, + { + "stock_request_id": request.id, + "requested_product_uom_qty": request.product_qty, + }, + ) + for request in self.stock_request_ids + ] + return res diff --git a/stock_request_mrp/models/stock_request.py b/stock_request_mrp/models/stock_request.py new file mode 100644 index 000000000..3d7e483c9 --- /dev/null +++ b/stock_request_mrp/models/stock_request.py @@ -0,0 +1,56 @@ +# Copyright 2020 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class StockRequest(models.Model): + _inherit = "stock.request" + + production_ids = fields.Many2many( + "mrp.production", + "mrp_production_stock_request_rel", + "stock_request_id", + "mrp_production_id", + string="Manufacturing Orders", + readonly=True, + ) + production_count = fields.Integer( + string="Manufacturing Orders count", + compute="_compute_production_ids", + readonly=True, + ) + + @api.depends("production_ids") + def _compute_production_ids(self): + for request in self: + request.production_count = len(request.production_ids) + + @api.constrains("production_ids", "company_id") + def _check_production_company_constrains(self): + if any( + any( + production.company_id != req.company_id + for production in req.production_ids + ) + for req in self + ): + raise ValidationError( + _( + "You have linked to a Manufacture Order " + "that belongs to another company." + ) + ) + + def action_view_mrp_production(self): + action = self.env.ref("mrp.mrp_production_action").read()[0] + productions = self.mapped("production_ids") + if len(productions) > 1: + action["domain"] = [("id", "in", productions.ids)] + elif productions: + action["views"] = [ + (self.env.ref("mrp.mrp_production_form_view").id, "form") + ] + action["res_id"] = productions.id + return action diff --git a/stock_request_mrp/models/stock_request_order.py b/stock_request_mrp/models/stock_request_order.py new file mode 100644 index 000000000..5ad225c09 --- /dev/null +++ b/stock_request_mrp/models/stock_request_order.py @@ -0,0 +1,42 @@ +# Copyright 2020 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class StockRequestOrder(models.Model): + _inherit = "stock.request.order" + + production_ids = fields.One2many( + "mrp.production", + compute="_compute_production_ids", + string="Manufacturing Orders", + readonly=True, + ) + production_count = fields.Integer( + string="Manufacturing Orders count", + compute="_compute_production_ids", + readonly=True, + ) + + @api.depends("stock_request_ids") + def _compute_production_ids(self): + for req in self: + req.production_ids = req.stock_request_ids.mapped("production_ids") + req.production_count = len(req.production_ids) + + def action_view_mrp_production(self): + action = self.env.ref("mrp.mrp_production_action").read()[0] + productions = self.mapped("production_ids") + if len(productions) > 1: + action["domain"] = [("id", "in", productions.ids)] + action["views"] = [ + (self.env.ref("mrp.mrp_production_tree_view").id, "tree"), + (self.env.ref("mrp.mrp_production_form_view").id, "form"), + ] + elif productions: + action["views"] = [ + (self.env.ref("mrp.mrp_production_form_view").id, "form") + ] + action["res_id"] = productions.id + return action diff --git a/stock_request_mrp/models/stock_rule.py b/stock_request_mrp/models/stock_rule.py new file mode 100644 index 000000000..3cf358b4e --- /dev/null +++ b/stock_request_mrp/models/stock_rule.py @@ -0,0 +1,34 @@ +# Copyright 2020 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +from odoo import models + + +class StockRule(models.Model): + _inherit = "stock.rule" + + def _prepare_mo_vals( + self, + product_id, + product_qty, + product_uom, + location_id, + name, + origin, + company_id, + values, + bom, + ): + res = super()._prepare_mo_vals( + product_id, + product_qty, + product_uom, + location_id, + name, + origin, + company_id, + values, + bom, + ) + if "stock_request_id" in values: + res["stock_request_ids"] = [(4, values["stock_request_id"])] + return res diff --git a/stock_request_mrp/readme/CONTRIBUTORS.rst b/stock_request_mrp/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..45e6392e8 --- /dev/null +++ b/stock_request_mrp/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Héctor Villarreal . diff --git a/stock_request_mrp/readme/DESCRIPTION.rst b/stock_request_mrp/readme/DESCRIPTION.rst new file mode 100644 index 000000000..99ce46166 --- /dev/null +++ b/stock_request_mrp/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows for users to be able to display manufacturing orders that have +been created as a consequence of Stock Requests. diff --git a/stock_request_mrp/readme/ROADMAP.rst b/stock_request_mrp/readme/ROADMAP.rst new file mode 100644 index 000000000..879b69590 --- /dev/null +++ b/stock_request_mrp/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* When a Stock Request is cancelled, it does not cancel the quantity included + in the Manufacturing Order. diff --git a/stock_request_mrp/readme/USAGE.rst b/stock_request_mrp/readme/USAGE.rst new file mode 100644 index 000000000..c3eeaf348 --- /dev/null +++ b/stock_request_mrp/readme/USAGE.rst @@ -0,0 +1,3 @@ +In case that the confirmation of the Stock Request results in an immediate +Manufacturing Order, the user will be able to display the MO's from the Stock +Request form view. diff --git a/stock_request_mrp/security/ir.model.access.csv b/stock_request_mrp/security/ir.model.access.csv new file mode 100644 index 000000000..207639cd6 --- /dev/null +++ b/stock_request_mrp/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_request_mrp_user,stock.request purchase user,stock_request.model_stock_request,mrp.group_mrp_user,1,0,0,0 +access_stock_request_allocation_mrp_user,stock request allocation purchase user,stock_request.model_stock_request_allocation,mrp.group_mrp_user,1,0,0,0 +access_stock_request_mrp_bom_user,stock request mrp bom user,mrp.model_mrp_bom,stock_request.group_stock_request_user,1,0,0,0 diff --git a/stock_request_mrp/static/description/icon.png b/stock_request_mrp/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d4f6a65aca34c7c1a714b1f88277d7295a992a72 GIT binary patch literal 15218 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 g52D<|BY2@uaBkmZTYB37d_NWhkyVxXDP + + + + + +Stock Request MRP + + + +
+

Stock Request MRP

+ + +

Beta License: LGPL-3 OCA/stock-logistics-warehouse Translate me on Weblate Try me on Runbot

+

This module allows for users to be able to display manufacturing orders that have +been created as a consequence of Stock Requests.

+

Table of contents

+ +
+

Usage

+

In case that the confirmation of the Stock Request results in an immediate +Manufacturing Order, the user will be able to display the MO’s from the Stock +Request form view.

+
+
+

Known issues / Roadmap

+
    +
  • When a Stock Request is cancelled, it does not cancel the quantity included +in the Manufacturing Order.
  • +
+
+
+

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

+
    +
  • ForgeFlow
  • +
+
+
+

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/stock-logistics-warehouse project on GitHub.

+

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

+
+
+
+ + diff --git a/stock_request_mrp/tests/__init__.py b/stock_request_mrp/tests/__init__.py new file mode 100644 index 000000000..cf5db6649 --- /dev/null +++ b/stock_request_mrp/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_request_mrp diff --git a/stock_request_mrp/tests/test_stock_request_mrp.py b/stock_request_mrp/tests/test_stock_request_mrp.py new file mode 100644 index 000000000..94d997623 --- /dev/null +++ b/stock_request_mrp/tests/test_stock_request_mrp.py @@ -0,0 +1,218 @@ +# Copyright 2016-20 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0). + +from odoo import fields +from odoo.tests import Form, common + + +class TestStockRequestMrp(common.TransactionCase): + def setUp(self): + super().setUp() + + # common models + self.stock_request = self.env["stock.request"] + self.produce_wiz = self.env["mrp.product.produce"] + + # refs + self.stock_request_user_group = self.env.ref( + "stock_request.group_stock_request_user" + ) + self.stock_request_manager_group = self.env.ref( + "stock_request.group_stock_request_manager" + ) + self.mrp_user_group = self.env.ref("mrp.group_mrp_user") + self.main_company = self.env.ref("base.main_company") + self.warehouse = self.env.ref("stock.warehouse0") + self.categ_unit = self.env.ref("uom.product_uom_categ_unit") + + # common data + self.company_2 = self.env["res.company"].create({"name": "Comp2"}) + self.wh2 = self.env["stock.warehouse"].search( + [("company_id", "=", self.company_2.id)], limit=1 + ) + self.stock_request_user = self._create_user( + "stock_request_user", + [self.stock_request_user_group.id, self.mrp_user_group.id], + [self.main_company.id, self.company_2.id], + ) + self.stock_request_manager = self._create_user( + "stock_request_manager", + [self.stock_request_manager_group.id, self.mrp_user_group.id], + [self.main_company.id, self.company_2.id], + ) + self.route_manufacture = self.warehouse.manufacture_pull_id.route_id + self.product = self._create_product( + "SH", "Shoes", False, self.route_manufacture.ids + ) + + self.raw_1 = self._create_product("SL", "Sole", False, []) + self._update_qty_in_location(self.warehouse.lot_stock_id, self.raw_1, 10) + self.raw_2 = self._create_product("LC", "Lace", False, []) + self._update_qty_in_location(self.warehouse.lot_stock_id, self.raw_2, 10) + + self.bom = self._create_mrp_bom(self.product, [self.raw_1, self.raw_2]) + + self.uom_pair = self.env["uom.uom"].create( + { + "name": "Test-Pair", + "category_id": self.categ_unit.id, + "factor_inv": 2, + "uom_type": "bigger", + "rounding": 0.001, + } + ) + + def _update_qty_in_location(self, location, product, quantity): + self.env["stock.quant"]._update_available_quantity(product, location, quantity) + + def _create_user(self, name, group_ids, company_ids): + return ( + self.env["res.users"] + .with_context({"no_reset_password": True}) + .create( + { + "name": name, + "password": "demo", + "login": name, + "email": str(name) + "@test.com", + "groups_id": [(6, 0, group_ids)], + "company_ids": [(6, 0, company_ids)], + } + ) + ) + + def _create_product(self, default_code, name, company_id, route_ids): + return self.env["product.product"].create( + { + "name": name, + "default_code": default_code, + "uom_id": self.env.ref("uom.product_uom_unit").id, + "company_id": company_id, + "type": "product", + "route_ids": [(6, 0, route_ids)], + } + ) + + def _create_mrp_bom(self, product_id, raw_materials): + bom = self.env["mrp.bom"].create( + { + "product_id": product_id.id, + "product_tmpl_id": product_id.product_tmpl_id.id, + "product_uom_id": product_id.uom_id.id, + "product_qty": 1.0, + "type": "normal", + } + ) + for raw_mat in raw_materials: + self.env["mrp.bom.line"].create( + {"bom_id": bom.id, "product_id": raw_mat.id, "product_qty": 1} + ) + + return bom + + def _produce(self, mo, qty=0.0): + wiz = Form( + self.produce_wiz.with_context({"active_id": mo.id, "active_ids": [mo.id]}) + ) + wiz.qty_producing = qty or mo.product_qty + produce_wizard = wiz.save() + produce_wizard.do_produce() + return True + + def test_create_request_01(self): + """Single Stock request with buy rule""" + expected_date = fields.Datetime.now() + vals = { + "company_id": self.main_company.id, + "warehouse_id": self.warehouse.id, + "location_id": self.warehouse.lot_stock_id.id, + "expected_date": expected_date, + "stock_request_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "product_uom_qty": 5.0, + "company_id": self.main_company.id, + "warehouse_id": self.warehouse.id, + "location_id": self.warehouse.lot_stock_id.id, + "expected_date": expected_date, + }, + ) + ], + } + + order = ( + self.env["stock.request.order"] + .with_user(self.stock_request_user) + .create(vals) + ) + + order.action_confirm() + self.assertEqual(order.state, "open") + self.assertEqual(order.stock_request_ids.state, "open") + + order.refresh() + + self.assertEqual(len(order.production_ids), 1) + self.assertEqual(len(order.stock_request_ids.production_ids), 1) + self.assertEqual(order.stock_request_ids.qty_in_progress, 5.0) + + manufacturing_order = order.production_ids[0] + self.assertEqual( + manufacturing_order.company_id, order.stock_request_ids[0].company_id + ) + + self._produce(manufacturing_order, 5.0) + self.assertEqual(order.stock_request_ids.qty_in_progress, 5.0) + self.assertEqual(order.stock_request_ids.qty_done, 0.0) + + manufacturing_order.button_mark_done() + self.assertEqual(order.stock_request_ids.qty_in_progress, 0.0) + self.assertEqual(order.stock_request_ids.qty_done, 5.0) + + def test_view_actions(self): + expected_date = fields.Datetime.now() + vals = { + "company_id": self.main_company.id, + "warehouse_id": self.warehouse.id, + "location_id": self.warehouse.lot_stock_id.id, + "expected_date": expected_date, + "stock_request_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "product_uom_qty": 5.0, + "company_id": self.main_company.id, + "warehouse_id": self.warehouse.id, + "location_id": self.warehouse.lot_stock_id.id, + "expected_date": expected_date, + }, + ) + ], + } + + order = self.env["stock.request.order"].create(vals) + + order.action_confirm() + + stock_request = order.stock_request_ids + + action = stock_request.action_view_mrp_production() + + self.assertEqual("views" in action.keys(), True) + self.assertEqual(action["res_id"], stock_request.production_ids[0].id) + + action = stock_request.production_ids[0].action_view_stock_request() + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_id"], stock_request.id) + + action = order.action_view_mrp_production() + + self.assertEqual("views" in action.keys(), True) + self.assertEqual(action["res_id"], order.production_ids[0].id) diff --git a/stock_request_mrp/views/mrp_production_views.xml b/stock_request_mrp/views/mrp_production_views.xml new file mode 100644 index 000000000..879c86062 --- /dev/null +++ b/stock_request_mrp/views/mrp_production_views.xml @@ -0,0 +1,37 @@ + + + + + mrp.production.form.inherit.stock.request.mrp + mrp.production + + + + + + + + + + + + + + diff --git a/stock_request_mrp/views/stock_request_order_views.xml b/stock_request_mrp/views/stock_request_order_views.xml new file mode 100644 index 000000000..d8aa04770 --- /dev/null +++ b/stock_request_mrp/views/stock_request_order_views.xml @@ -0,0 +1,25 @@ + + + + + stock.request.order.form + stock.request.order + + + +
+ + +
+
+
+
diff --git a/stock_request_mrp/views/stock_request_views.xml b/stock_request_mrp/views/stock_request_views.xml new file mode 100644 index 000000000..b3cf2ee27 --- /dev/null +++ b/stock_request_mrp/views/stock_request_views.xml @@ -0,0 +1,25 @@ + + + + + stock.request.form + stock.request + + + +
+ + +
+
+
+