diff --git a/mrp_unbuild_subcontracting/README.rst b/mrp_unbuild_subcontracting/README.rst new file mode 100644 index 0000000000..956c69a9d7 --- /dev/null +++ b/mrp_unbuild_subcontracting/README.rst @@ -0,0 +1,80 @@ +========================================= +Unbuild orders with return subcontracting +========================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:11f2e6d4a0a7c585038a75213c3ea46945a33fa40ecde95aef22f0cc999c8853 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fmanufacture-lightgray.png?logo=github + :target: https://github.com/OCA/manufacture/tree/16.0/mrp_unbuild_subcontracting + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-16-0/manufacture-16-0-mrp_unbuild_subcontracting + :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/manufacture&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module automatically creates an unbuild in draft state when a subcontracting picking return is created. In addition, when the picking is validated, the unbuild is also validated. +To view the unbuilds created, you have to select the operation Subcontracted Unbuild Orders in debug mode + +**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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ForgeFlow + +Contributors +~~~~~~~~~~~~ + +* `ForgeFlow `_: + + * Thiago Mulero + * Bernat Puig + +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/manufacture `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mrp_unbuild_subcontracting/__init__.py b/mrp_unbuild_subcontracting/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/mrp_unbuild_subcontracting/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mrp_unbuild_subcontracting/__manifest__.py b/mrp_unbuild_subcontracting/__manifest__.py new file mode 100644 index 0000000000..ce35007ec1 --- /dev/null +++ b/mrp_unbuild_subcontracting/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +{ + "name": "Unbuild orders with return subcontracting", + "version": "16.0.1.0.0", + "license": "LGPL-3", + "category": "Manufacture", + "summary": "Unbuild orders are created automatically " + "when is returned a product subcontracted", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/manufacture", + "depends": ["mrp_account", "mrp_subcontracting_purchase"], + "data": ["views/mrp_unbuild_views.xml"], + "installable": True, +} diff --git a/mrp_unbuild_subcontracting/i18n/it.po b/mrp_unbuild_subcontracting/i18n/it.po new file mode 100644 index 0000000000..a527d29a3b --- /dev/null +++ b/mrp_unbuild_subcontracting/i18n/it.po @@ -0,0 +1,94 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_unbuild_subcontracting +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2022-12-23 13:45+0000\n" +"Last-Translator: mymage \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.14.1\n" + +#. module: mrp_unbuild_subcontracting +#: model_terms:ir.actions.act_window,help:mrp_unbuild_subcontracting.mrp_unbuild_subcontracted +msgid "" +"An unbuild order is used to break down a finished product into its " +"components." +msgstr "" +"Un ordine di smontaggio viene utilizzato per separare un prodotto finito nei " +"sui componenti." + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild__display_name +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_move__display_name +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_picking__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild__id +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_move__id +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_picking__id +msgid "ID" +msgstr "ID" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild__is_subcontracted +msgid "Is Subcontracted" +msgstr "In conto lavoro" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild____last_update +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_move____last_update +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_picking____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: mrp_unbuild_subcontracting +#: model_terms:ir.actions.act_window,help:mrp_unbuild_subcontracting.mrp_unbuild_subcontracted +msgid "No unbuild order found" +msgstr "Nessun ordine di smontaggio trovato" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model,name:mrp_unbuild_subcontracting.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" + +#. module: mrp_unbuild_subcontracting +#: model:ir.ui.menu,name:mrp_unbuild_subcontracting.menu_mrp_unbuild_subcontracted +msgid "Subcontracted Unbuild Orders" +msgstr "Ordini di smontaggio in conto lavoro" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_picking__subcontracted_unbuild_ids +msgid "Suncontracted unbuilds" +msgstr "Smontaggi in conto lavoro" + +#. module: mrp_unbuild_subcontracting +#: code:addons/mrp_unbuild_subcontracting/models/stock_move.py:0 +#, python-format +msgid "To subcontract, use a planned transfer." +msgstr "Per conto lavoro, utilizzare un trasferimento pianificato." + +#. module: mrp_unbuild_subcontracting +#: model:ir.model,name:mrp_unbuild_subcontracting.model_stock_picking +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild__picking_id +msgid "Transfer" +msgstr "Trasferimento" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model,name:mrp_unbuild_subcontracting.model_mrp_unbuild +msgid "Unbuild Order" +msgstr "Ordine di smontaggio" + +#. module: mrp_unbuild_subcontracting +#: model:ir.actions.act_window,name:mrp_unbuild_subcontracting.mrp_unbuild_subcontracted +msgid "Unbuild Orders - Subcontracted" +msgstr "Ordini smontaggio - In conto lavoro" diff --git a/mrp_unbuild_subcontracting/i18n/mrp_unbuild_subcontracting.pot b/mrp_unbuild_subcontracting/i18n/mrp_unbuild_subcontracting.pot new file mode 100644 index 0000000000..e642b3ff8e --- /dev/null +++ b/mrp_unbuild_subcontracting/i18n/mrp_unbuild_subcontracting.pot @@ -0,0 +1,89 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_unbuild_subcontracting +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.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: mrp_unbuild_subcontracting +#: model_terms:ir.actions.act_window,help:mrp_unbuild_subcontracting.mrp_unbuild_subcontracted +msgid "" +"An unbuild order is used to break down a finished product into its " +"components." +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild__display_name +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_move__display_name +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_picking__display_name +msgid "Display Name" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild__id +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_move__id +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_picking__id +msgid "ID" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild__is_subcontracted +msgid "Is Subcontracted" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild____last_update +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_move____last_update +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_picking____last_update +msgid "Last Modified on" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model_terms:ir.actions.act_window,help:mrp_unbuild_subcontracting.mrp_unbuild_subcontracted +msgid "No unbuild order found" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model,name:mrp_unbuild_subcontracting.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.ui.menu,name:mrp_unbuild_subcontracting.menu_mrp_unbuild_subcontracted +msgid "Subcontracted Unbuild Orders" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_stock_picking__subcontracted_unbuild_ids +msgid "Suncontracted unbuilds" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: code:addons/mrp_unbuild_subcontracting/models/stock_move.py:0 +#, python-format +msgid "To subcontract, use a planned transfer." +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model,name:mrp_unbuild_subcontracting.model_stock_picking +#: model:ir.model.fields,field_description:mrp_unbuild_subcontracting.field_mrp_unbuild__picking_id +msgid "Transfer" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.model,name:mrp_unbuild_subcontracting.model_mrp_unbuild +msgid "Unbuild Order" +msgstr "" + +#. module: mrp_unbuild_subcontracting +#: model:ir.actions.act_window,name:mrp_unbuild_subcontracting.mrp_unbuild_subcontracted +msgid "Unbuild Orders - Subcontracted" +msgstr "" diff --git a/mrp_unbuild_subcontracting/models/__init__.py b/mrp_unbuild_subcontracting/models/__init__.py new file mode 100644 index 0000000000..99843fd643 --- /dev/null +++ b/mrp_unbuild_subcontracting/models/__init__.py @@ -0,0 +1,3 @@ +from . import stock_picking +from . import mrp_unbuild +from . import stock_move diff --git a/mrp_unbuild_subcontracting/models/mrp_unbuild.py b/mrp_unbuild_subcontracting/models/mrp_unbuild.py new file mode 100644 index 0000000000..2872538fc6 --- /dev/null +++ b/mrp_unbuild_subcontracting/models/mrp_unbuild.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class MrpUnbuild(models.Model): + _inherit = "mrp.unbuild" + + picking_id = fields.Many2one("stock.picking", "Transfer", readonly=True) + is_subcontracted = fields.Boolean("Is Subcontracted?", readonly=True) diff --git a/mrp_unbuild_subcontracting/models/stock_move.py b/mrp_unbuild_subcontracting/models/stock_move.py new file mode 100644 index 0000000000..05aa452095 --- /dev/null +++ b/mrp_unbuild_subcontracting/models/stock_move.py @@ -0,0 +1,52 @@ +from collections import defaultdict + +from odoo import _, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_is_zero + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _action_confirm(self, merge=True, merge_into=False): + if self.origin_returned_move_id: + subcontract_details_per_picking = defaultdict(list) + move_to_not_merge = self.env["stock.move"] + for move in self: + if ( + move.location_dest_id.usage == "supplier" + and move.location_id + == self.picking_id.picking_type_id.default_location_src_id + ): + continue + if move.move_orig_ids.production_id: + continue + bom = move._get_subcontract_bom() + if not bom: + continue + if ( + float_is_zero( + move.product_qty, precision_rounding=move.product_uom.rounding + ) + and move.picking_id.immediate_transfer is True + ): + raise UserError(_("To subcontract, use a planned transfer.")) + subcontract_details_per_picking[move.picking_id].append((move, bom)) + move_to_not_merge |= move + for picking, subcontract_details in subcontract_details_per_picking.items(): + picking._subcontracted_produce_unbuild(subcontract_details) + + # We avoid merging move due to complication with stock.rule. + res = super(StockMove, move_to_not_merge)._action_confirm(merge=False) + res |= super(StockMove, self - move_to_not_merge)._action_confirm( + merge=merge, merge_into=merge_into + ) + if subcontract_details_per_picking: + self.env["stock.picking"].concat( + *list(subcontract_details_per_picking.keys()) + ).action_assign() + return res + result = super(StockMove, self)._action_confirm( + merge=merge, merge_into=merge_into + ) + return result diff --git a/mrp_unbuild_subcontracting/models/stock_picking.py b/mrp_unbuild_subcontracting/models/stock_picking.py new file mode 100644 index 0000000000..f8a40567ac --- /dev/null +++ b/mrp_unbuild_subcontracting/models/stock_picking.py @@ -0,0 +1,111 @@ +from datetime import timedelta + +from odoo import _, fields, models +from odoo.exceptions import UserError +from odoo.osv.expression import OR + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + subcontracted_unbuild_ids = fields.One2many( + "mrp.unbuild", "picking_id", readonly=True, string="Subcontracted unbuilds" + ) + + def _prepare_subcontract_unbuild_vals(self, subcontract_move, bom): + subcontract_move.ensure_one() + product = subcontract_move.product_id + mos = subcontract_move.mapped( + "origin_returned_move_id.move_orig_ids.production_id" + ) + if len(mos) > 1: + raise UserError( + _( + "It's not possible to create the subcontracting unbuild order\n" + "The subcontract move %(smn)s is linked with more than " + "one manufacturing order: %(jmm)s" + ) + % {"smn": subcontract_move.name, "jmm": ",".join(mos.mapped("name"))} + ) + vals = { + "company_id": subcontract_move.company_id.id, + "product_id": product.id, + "product_uom_id": subcontract_move.product_uom.id, + "bom_id": bom.id, + "location_id": subcontract_move.picking_id.partner_id.with_company( + subcontract_move.company_id + ).property_stock_subcontractor.id, + "location_dest_id": subcontract_move.picking_id.partner_id.with_company( + subcontract_move.company_id + ).property_stock_subcontractor.id, + "product_qty": subcontract_move.product_uom_qty, + "picking_id": self.id, + "is_subcontracted": True, + "mo_id": mos.id, + "lot_id": subcontract_move.move_orig_ids.lot_ids.id, + } + return vals + + def _subcontracted_produce_unbuild(self, subcontract_details): + self.ensure_one() + for move, bom in subcontract_details: + unbuild = ( + self.env["mrp.unbuild"] + .with_company(move.company_id) + .create(self._prepare_subcontract_unbuild_vals(move, bom)) + ) + self.subcontracted_unbuild_ids |= unbuild + + def _action_done(self): + res = super(StockPicking, self)._action_done() + for picking in self: + unbuilds_to_done = picking.subcontracted_unbuild_ids.filtered( + lambda x: x.state == "draft" + ) + if not unbuilds_to_done: + continue + unbuild_ids_backorder = [] + if not self.env.context.get("cancel_backorder"): + unbuild_ids_backorder = unbuilds_to_done.filtered( + lambda u: u.state == "draft" + ).ids + for unbuild in unbuilds_to_done: + unbuild.with_context( + subcontract_move_id=True, mo_ids_to_backorder=unbuild_ids_backorder + ).action_validate() + moves = picking.move_ids.filtered(lambda move: move.is_subcontract) + finished_move = unbuilds_to_done.produce_line_ids.filtered( + lambda m: m.product_id.id in moves.mapped("product_id").ids + ) + for move in moves: + finished_move.write({"move_dest_ids": [(4, move.id, False)]}) + # For concistency, set the date on production move before the date + # on picking. (Traceability report + Product Moves menu item) + minimum_date = min(picking.move_line_ids.mapped("date")) + unbuild_moves = ( + unbuilds_to_done.produce_line_ids | unbuilds_to_done.consume_line_ids + ) + unbuild_moves.write({"date": minimum_date - timedelta(seconds=1)}) + unbuild_moves.move_line_ids.write( + {"date": minimum_date - timedelta(seconds=1)} + ) + return res + + def action_view_stock_valuation_layers(self): + action = super(StockPicking, self).action_view_stock_valuation_layers() + subcontracted_unbuilds = self.subcontracted_unbuild_ids + if not subcontracted_unbuilds: + return action + domain = action["domain"] + domain_subcontracting = [ + ( + "id", + "in", + ( + subcontracted_unbuilds.produce_line_ids + | subcontracted_unbuilds.consume_line_ids + ).stock_valuation_layer_ids.ids, + ) + ] + domain = OR([domain, domain_subcontracting]) + return dict(action, domain=domain) diff --git a/mrp_unbuild_subcontracting/readme/CONTRIBUTORS.rst b/mrp_unbuild_subcontracting/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..61c70d3613 --- /dev/null +++ b/mrp_unbuild_subcontracting/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `ForgeFlow `_: + + * Thiago Mulero + * Bernat Puig diff --git a/mrp_unbuild_subcontracting/readme/DESCRIPTION.rst b/mrp_unbuild_subcontracting/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..e23d01a2c5 --- /dev/null +++ b/mrp_unbuild_subcontracting/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module automatically creates an unbuild in draft state when a subcontracting picking return is created. In addition, when the picking is validated, the unbuild is also validated. +To view the unbuilds created, you have to select the operation Subcontracted Unbuild Orders in debug mode diff --git a/mrp_unbuild_subcontracting/static/description/icon.png b/mrp_unbuild_subcontracting/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/mrp_unbuild_subcontracting/static/description/icon.png differ diff --git a/mrp_unbuild_subcontracting/static/description/index.html b/mrp_unbuild_subcontracting/static/description/index.html new file mode 100644 index 0000000000..5df0698cd3 --- /dev/null +++ b/mrp_unbuild_subcontracting/static/description/index.html @@ -0,0 +1,426 @@ + + + + + + +Unbuild orders with return subcontracting + + + +
+

Unbuild orders with return subcontracting

+ + +

Beta License: LGPL-3 OCA/manufacture Translate me on Weblate Try me on Runboat

+

This module automatically creates an unbuild in draft state when a subcontracting picking return is created. In addition, when the picking is validated, the unbuild is also validated. +To view the unbuilds created, you have to select the operation Subcontracted Unbuild Orders in debug mode

+

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 to smash 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/manufacture project on GitHub.

+

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

+
+
+
+ + diff --git a/mrp_unbuild_subcontracting/tests/__init__.py b/mrp_unbuild_subcontracting/tests/__init__.py new file mode 100644 index 0000000000..3fd50533fc --- /dev/null +++ b/mrp_unbuild_subcontracting/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mrp_unbuild_subcontracting diff --git a/mrp_unbuild_subcontracting/tests/test_mrp_unbuild_subcontracting.py b/mrp_unbuild_subcontracting/tests/test_mrp_unbuild_subcontracting.py new file mode 100644 index 0000000000..8b478c640d --- /dev/null +++ b/mrp_unbuild_subcontracting/tests/test_mrp_unbuild_subcontracting.py @@ -0,0 +1,412 @@ +from odoo.exceptions import UserError +from odoo.tests import Form, TransactionCase + + +class TestSubcontractingPurchaseFlows(TransactionCase): + def setUp(self): + super().setUp() + + self.subcontractor = self.env["res.partner"].create( + {"name": "SuperSubcontractor"} + ) + + self.finished, self.compo = self.env["product.product"].create( + [ + { + "name": "SuperProduct", + "type": "product", + }, + { + "name": "Component", + "type": "consu", + }, + ] + ) + + self.bom = self.env["mrp.bom"].create( + { + "product_tmpl_id": self.finished.product_tmpl_id.id, + "type": "subcontract", + "subcontractor_ids": [(6, 0, self.subcontractor.ids)], + "bom_line_ids": [ + ( + 0, + 0, + { + "product_id": self.compo.id, + "product_qty": 1, + }, + ) + ], + } + ) + + def test_purchase_and_return(self): + """ + The user buys 10 x a subcontracted product P. He receives the 10 + products and then does a return with 3 x P. The test ensures that + the unbuild is created with the correct quantities and states + """ + po = self.env["purchase.order"].create( + { + "partner_id": self.subcontractor.id, + "order_line": [ + ( + 0, + 0, + { + "name": self.finished.name, + "product_id": self.finished.id, + "product_uom_qty": 10, + "product_uom": self.finished.uom_id.id, + "price_unit": 1, + }, + ) + ], + } + ) + po.button_confirm() + + mo = self.env["mrp.production"].search([("bom_id", "=", self.bom.id)]) + self.assertTrue(mo) + + receipt = po.picking_ids + receipt.move_ids.quantity_done = 10 + receipt.button_validate() + + return_form = Form( + self.env["stock.return.picking"].with_context( + active_id=receipt.id, active_model="stock.picking" + ) + ) + with return_form.product_return_moves.edit(0) as line: + line.quantity = 3 + return_wizard = return_form.save() + return_id, _ = return_wizard._create_returns() + + return_picking = self.env["stock.picking"].browse(return_id) + return_picking.move_ids.quantity_done = 3 + subcontractor_location = self.subcontractor.property_stock_subcontractor + unbuild = self.env["mrp.unbuild"].search([("bom_id", "=", self.bom.id)]) + + self.assertTrue(unbuild) + self.assertEqual( + unbuild.state, "draft", "The state of the unbuild should be draft" + ) + self.assertEqual( + unbuild.product_qty, 3, "The quantity of the unbuild should be 3" + ) + self.assertEqual( + unbuild.location_id, + subcontractor_location, + "The source location of the unbuild should be the property stock " + "of the subcontractor", + ) + self.assertEqual( + unbuild.location_dest_id, + subcontractor_location, + "The destination location of the unbuild should be the property " + "stock of the subcontractor", + ) + + return_picking.button_validate() + + self.assertEqual(self.finished.qty_available, 7.0) + self.assertEqual(po.order_line.qty_received, 7.0) + self.assertEqual( + unbuild.state, "done", "The state of the unbuild should be done" + ) + + move = return_picking.move_ids + self.assertEqual( + move.location_id, + receipt.location_dest_id, + "The source location of the stock move should be the same as " + "destination location of the original purchase", + ) + self.assertEqual( + move.location_dest_id, + subcontractor_location, + "The destination location of the stock move should be the property " + "stock of the subcontractor", + ) + + # Call the action to view the layers associated to the pickings + result1 = return_picking.action_view_stock_valuation_layers() + result2 = receipt.action_view_stock_valuation_layers() + layers1 = result1["domain"][2][2] + layers2 = result2["domain"][2][2] + self.assertTrue( + layers1, + ) + self.assertTrue( + layers2, + ) + + def test_purchase_partial_receipt_and_refund(self): + po = self.env["purchase.order"].create( + { + "partner_id": self.subcontractor.id, + "order_line": [ + ( + 0, + 0, + { + "name": self.finished.name, + "product_id": self.finished.id, + "product_uom_qty": 10, + "product_qty": 10, + "product_uom": self.finished.uom_id.id, + "price_unit": 1, + }, + ) + ], + } + ) + po.button_confirm() + + mo = self.env["mrp.production"].search([("bom_id", "=", self.bom.id)]) + self.assertTrue(mo) + + receipt = po.picking_ids.filtered(lambda x: x.state != "done") + receipt.move_ids.quantity_done = 3 + result_dict = receipt.button_validate() + self.env["stock.backorder.confirmation"].with_context( + **result_dict["context"] + ).process() + self.assertEqual(po.order_line.qty_received, 3) + + receipt = po.picking_ids.filtered(lambda x: x.state != "done") + receipt.move_ids.quantity_done = 3 + picking_to_return = receipt + result_dict = receipt.button_validate() + self.env["stock.backorder.confirmation"].with_context( + **result_dict["context"] + ).process() + self.assertEqual(po.order_line.qty_received, 6) + + receipt = po.picking_ids.filtered(lambda x: x.state != "done") + receipt.move_ids.quantity_done = 3 + result_dict = receipt.button_validate() + self.env["stock.backorder.confirmation"].with_context( + **result_dict["context"] + ).process() + self.assertEqual(po.order_line.qty_received, 9) + + self.assertEqual(len(po.picking_ids), 4) + + return_wizard = ( + self.env["stock.return.picking"] + .with_context( + active_id=picking_to_return.id, active_ids=picking_to_return.ids + ) + .create( + { + "location_id": picking_to_return.location_id.id, + "picking_id": picking_to_return.id, + } + ) + ) + return_wizard._onchange_picking_id() + with self.assertRaises(UserError): + return_id, _ = return_wizard._create_returns() + + # This part cannot be tested since we cannot unbuild + # subcontracting orders with more than one origin. + # + # return_picking = self.env["stock.picking"].browse(return_id) + # return_picking.move_ids.quantity_done = 3 + # return_picking.button_validate() + # + # self.assertEqual(po.order_line.qty_received, 6) + # + # mo = picking_to_return.mapped("move_ids.move_orig_ids.production_id") + # unbuild = self.env["mrp.unbuild"].search([("mo_id", "in", mo.ids)]) + # self.assertTrue(unbuild.exists()) + + +class TestSubcontractingTracking(TransactionCase): + def setUp(self): + super(TestSubcontractingTracking, self).setUp() + # 1: Create a subcontracting partner + main_company_1 = self.env["res.partner"].create({"name": "main_partner"}) + self.subcontractor_partner1 = self.env["res.partner"].create( + { + "name": "Subcontractor 1", + "parent_id": main_company_1.id, + "company_id": self.env.ref("base.main_company").id, + } + ) + + # 2. Create a BOM of subcontracting type + # 2.1. Comp1 has tracking by lot + self.comp1_sn = self.env["product.product"].create( + { + "name": "Component1", + "type": "product", + "categ_id": self.env.ref("product.product_category_all").id, + "tracking": "serial", + } + ) + self.comp2 = self.env["product.product"].create( + { + "name": "Component2", + "type": "product", + "categ_id": self.env.ref("product.product_category_all").id, + } + ) + + # 2.2. Finished prodcut has tracking by serial number + self.finished_product = self.env["product.product"].create( + { + "name": "finished", + "type": "product", + "categ_id": self.env.ref("product.product_category_all").id, + "tracking": "lot", + } + ) + bom_form = Form(self.env["mrp.bom"]) + bom_form.type = "subcontract" + bom_form.subcontractor_ids.add(self.subcontractor_partner1) + bom_form.product_tmpl_id = self.finished_product.product_tmpl_id + with bom_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.comp1_sn + bom_line.product_qty = 1 + with bom_form.bom_line_ids.new() as bom_line: + bom_line.product_id = self.comp2 + bom_line.product_qty = 1 + self.bom_tracked = bom_form.save() + + def test_purchase_and_return_with_serial_numbers(self): + """ + The user buys one subcontracted product P with serial number. + Then does the return . The test ensures that the unbuild is + created with the correct quantities, serial number of the product and states + """ + # Create a receipt picking from the subcontractor + picking_form = Form(self.env["stock.picking"]) + picking_form.picking_type_id = self.env.ref("stock.picking_type_in") + picking_form.partner_id = self.subcontractor_partner1 + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.finished_product + move.product_uom_qty = 1 + picking_receipt = picking_form.save() + picking_receipt.action_confirm() + + # We should be able to call the 'record_components' button + self.assertTrue(picking_receipt.display_action_record_components) + + # Check the created manufacturing order + mo = self.env["mrp.production"].search([("bom_id", "=", self.bom_tracked.id)]) + self.assertEqual(len(mo), 1) + self.assertEqual(len(mo.picking_ids), 0) + wh = picking_receipt.picking_type_id.warehouse_id + self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id) + self.assertFalse(mo.picking_type_id.active) + + # Create a RR + pg1 = self.env["procurement.group"].create({}) + self.env["stock.warehouse.orderpoint"].create( + { + "name": "xxx", + "product_id": self.comp1_sn.id, + "product_min_qty": 0, + "product_max_qty": 0, + "location_id": self.env.user.company_id.subcontracting_location_id.id, + "group_id": pg1.id, + } + ) + + # Run the scheduler and check the created picking + self.env["procurement.group"].run_scheduler() + picking = self.env["stock.picking"].search([("group_id", "=", pg1.id)]) + self.assertEqual(len(picking), 1) + self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id) + + lot_id = self.env["stock.lot"].create( + { + "name": "lot1", + "product_id": self.finished_product.id, + "company_id": self.env.company.id, + } + ) + serial_id = self.env["stock.lot"].create( + { + "name": "lot1", + "product_id": self.comp1_sn.id, + "company_id": self.env.company.id, + } + ) + + action = picking_receipt.action_record_components() + mo = self.env["mrp.production"].browse(action["res_id"]) + mo_form = Form(mo.with_context(**action["context"]), view=action["view_id"]) + mo_form.qty_producing = 1 + mo_form.lot_producing_id = lot_id + with mo_form.move_line_raw_ids.edit(0) as ml: + ml.lot_id = serial_id + mo = mo_form.save() + mo.subcontracting_record_component() + + # We should not be able to call the 'record_components' button + self.assertEqual(picking_receipt.display_action_record_components, "hide") + + picking_receipt.button_validate() + self.assertEqual(mo.state, "done") + + return_form = Form( + self.env["stock.return.picking"].with_context( + active_id=picking_receipt.id, active_model="stock.picking" + ) + ) + with return_form.product_return_moves.edit(0) as line: + line.quantity = 1 + return_wizard = return_form.save() + return_id, _ = return_wizard._create_returns() + + return_picking = self.env["stock.picking"].browse(return_id) + return_picking.move_ids.quantity_done = 1 + subcontractor_location = ( + self.subcontractor_partner1.property_stock_subcontractor + ) + unbuild = self.env["mrp.unbuild"].search([("bom_id", "=", self.bom_tracked.id)]) + + self.assertTrue(unbuild) + self.assertEqual( + unbuild.state, "draft", "The state of the unbuild should be draft" + ) + self.assertEqual( + unbuild.product_qty, 1, "The quantity of the unbuild should be 1" + ) + self.assertEqual( + unbuild.location_id, + subcontractor_location, + "The source location of the unbuild should be the property stock " + "of the subcontractor", + ) + self.assertEqual( + unbuild.location_dest_id, + subcontractor_location, + "The destination location of the unbuild should be the property " + "stock of the subcontractor", + ) + return_picking.move_line_ids_without_package.lot_id = lot_id + return_picking.button_validate() + + self.assertEqual( + unbuild.state, "done", "The state of the unbuild should be done" + ) + + move = return_picking.move_ids + self.assertEqual( + move.location_id, + picking_receipt.location_dest_id, + "The source location of the stock move should be the same as " + "destination location of the original purchase", + ) + self.assertEqual( + move.location_dest_id, + subcontractor_location, + "The destination location of the stock move should be the property " + "stock of the subcontractor", + ) diff --git a/mrp_unbuild_subcontracting/views/mrp_unbuild_views.xml b/mrp_unbuild_subcontracting/views/mrp_unbuild_views.xml new file mode 100644 index 0000000000..f0fa42b71f --- /dev/null +++ b/mrp_unbuild_subcontracting/views/mrp_unbuild_views.xml @@ -0,0 +1,30 @@ + + + + [('is_subcontracted', '=', False)] + + + + Unbuild Orders - Subcontracted + ir.actions.act_window + mrp.unbuild + tree,kanban,form + [('is_subcontracted', '=', True)] + +

+ No unbuild order found +

+ An unbuild order is used to break down a finished product into its components. +

+
+
+ + +
diff --git a/setup/mrp_unbuild_subcontracting/odoo/addons/mrp_unbuild_subcontracting b/setup/mrp_unbuild_subcontracting/odoo/addons/mrp_unbuild_subcontracting new file mode 120000 index 0000000000..67381ad17b --- /dev/null +++ b/setup/mrp_unbuild_subcontracting/odoo/addons/mrp_unbuild_subcontracting @@ -0,0 +1 @@ +../../../../mrp_unbuild_subcontracting \ No newline at end of file diff --git a/setup/mrp_unbuild_subcontracting/setup.py b/setup/mrp_unbuild_subcontracting/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/mrp_unbuild_subcontracting/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)