From afdeb6937135f8cb90faaea3cf1df80118078727 Mon Sep 17 00:00:00 2001 From: David Beal Date: Thu, 31 Oct 2019 15:49:38 +0100 Subject: [PATCH] ADD module mrp_unbuild_tracked_raw_material --- mrp_unbuild_tracked_raw_material/README.rst | 5 + mrp_unbuild_tracked_raw_material/__init__.py | 1 + .../__manifest__.py | 19 +++ .../models/__init__.py | 2 + .../models/product.py | 14 +++ .../models/unbuild.py | 117 ++++++++++++++++++ .../readme/CONFIGURE.rst | 1 + .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 17 +++ .../readme/ROADMAP.rst | 3 + .../readme/USAGE.rst | 6 + .../tests/__init__.py | 1 + .../tests/test_unbuild.py | 71 +++++++++++ .../views/product_view.xml | 16 +++ 14 files changed, 276 insertions(+) create mode 100644 mrp_unbuild_tracked_raw_material/README.rst create mode 100644 mrp_unbuild_tracked_raw_material/__init__.py create mode 100644 mrp_unbuild_tracked_raw_material/__manifest__.py create mode 100644 mrp_unbuild_tracked_raw_material/models/__init__.py create mode 100644 mrp_unbuild_tracked_raw_material/models/product.py create mode 100644 mrp_unbuild_tracked_raw_material/models/unbuild.py create mode 100644 mrp_unbuild_tracked_raw_material/readme/CONFIGURE.rst create mode 100644 mrp_unbuild_tracked_raw_material/readme/CONTRIBUTORS.rst create mode 100644 mrp_unbuild_tracked_raw_material/readme/DESCRIPTION.rst create mode 100644 mrp_unbuild_tracked_raw_material/readme/ROADMAP.rst create mode 100644 mrp_unbuild_tracked_raw_material/readme/USAGE.rst create mode 100644 mrp_unbuild_tracked_raw_material/tests/__init__.py create mode 100644 mrp_unbuild_tracked_raw_material/tests/test_unbuild.py create mode 100644 mrp_unbuild_tracked_raw_material/views/product_view.xml diff --git a/mrp_unbuild_tracked_raw_material/README.rst b/mrp_unbuild_tracked_raw_material/README.rst new file mode 100644 index 000000000..ee0cc78b9 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/README.rst @@ -0,0 +1,5 @@ +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + diff --git a/mrp_unbuild_tracked_raw_material/__init__.py b/mrp_unbuild_tracked_raw_material/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mrp_unbuild_tracked_raw_material/__manifest__.py b/mrp_unbuild_tracked_raw_material/__manifest__.py new file mode 100644 index 000000000..930bb35c3 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2019 Akretion (http://www.akretion.com). All Rights Reserved +# @author David BEAL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Mrp Unbuild Tracked Raw Material", + "summary": "Allow to unbuild tracked purchased products", + "author": "Akretion, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/manufacture", + "category": "Manufacturing", + "version": "12.0.1.0.0", + "license": "AGPL-3", + "depends": ["mrp"], + "maintainers": ["bealdav"], + "data": [ + "views/product_view.xml", + ], + "installable": True, +} diff --git a/mrp_unbuild_tracked_raw_material/models/__init__.py b/mrp_unbuild_tracked_raw_material/models/__init__.py new file mode 100644 index 000000000..7adb5e1e2 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/models/__init__.py @@ -0,0 +1,2 @@ +from . import product +from . import unbuild diff --git a/mrp_unbuild_tracked_raw_material/models/product.py b/mrp_unbuild_tracked_raw_material/models/product.py new file mode 100644 index 000000000..6d8f102cd --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/models/product.py @@ -0,0 +1,14 @@ +# Copyright (C) 2019 Akretion (http://www.akretion.com). All Rights Reserved +# @author David BEAL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + allow_unbuild_purchased = fields.Boolean( + help="If checked, unbuild orders doesn't assume a previous " + "manufacturing order have built this product.\n" + "In this case it's a purchased product and you want unbuild it") diff --git a/mrp_unbuild_tracked_raw_material/models/unbuild.py b/mrp_unbuild_tracked_raw_material/models/unbuild.py new file mode 100644 index 000000000..65116232f --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/models/unbuild.py @@ -0,0 +1,117 @@ +# Copyright (C) 2019 Akretion (http://www.akretion.com). All Rights Reserved +# @author David BEAL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import datetime +from odoo import _, models +from odoo.exceptions import UserError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DT_FORMAT + +logger = logging.getLogger(__name__) + +MESSAGE = "Some of your components are tracked, you have to specify " \ + "a manufacturing order in order to retrieve " \ + "the correct components." +ALTER_MESSAGE = "Alternatively, you may unbuild '%s' tracked product " \ + "if you set it as '%s'.\n" \ + "In this case lots'll be automatically created for you." + + +class MrpUnbuild(models.Model): + _inherit = "mrp.unbuild" + + def action_unbuild(self): + """ We need to catch raise behavior when tracked products + are unbuild without an original manufacturing order and + go on with another workflow with + _bypass_tracked_product_without_mo() + """ + try: + res = super().action_unbuild() + except UserError as err: + # Unbuild is impossible because of MESSAGE + # original condition of this raise is : + # any(produce_move.has_tracking != 'none' + # and not self.mo_id for produce_move in produce_moves) + if hasattr(err, 'name') and err.name == _(MESSAGE): + if self.product_id.allow_unbuild_purchased: + # In this case it becomes possible to unbuild + return self._bypass_tracked_product_without_mo() + # Here other option to resolve unbuild conditions + new_message = _(ALTER_MESSAGE) % ( + self.product_id.name, _("Unbuild Purchased")) + # We teach user the 2 conditions that make unbuild possible + raise UserError(_("%s \n\n%s") % (err.name, new_message)) + # Here is the Odoo native raise + raise err + return res + + def _bypass_tracked_product_without_mo(self): + # These moves are already generated with call to + # super().action_unbuild(). We catch them + consume_move = self.env["stock.move"].search( + [("consume_unbuild_id", "=", self.id)]) + produce_moves = self.env["stock.move"].search( + [("unbuild_id", "=", self.id)]) + + # Comes from + # https://github.com/OCA/ocb/blob/12.0/addons/mrp/models/... + # mrp_unbuild.py#L117 + if consume_move.has_tracking != 'none': + self.env['stock.move.line'].create({ + 'move_id': consume_move.id, + 'lot_id': self.lot_id.id, + 'qty_done': consume_move.product_uom_qty, + 'product_id': consume_move.product_id.id, + 'product_uom_id': consume_move.product_uom.id, + 'location_id': consume_move.location_id.id, + 'location_dest_id': consume_move.location_dest_id.id, + }) + else: + consume_move.quantity_done = consume_move.product_uom_qty + consume_move._action_done() + + # Comment from odoo original module: + # TODO: Will fail if user do more than one unbuild with lot + # on the same MO. Need to check what other unbuild has aready took + for produce_move in produce_moves: + if produce_move.has_tracking != 'none': + if produce_move.product_id.tracking == "serial": + # TODO + raise UserError(_( + "Unbuild of component of tracked as serial " + "is not supported for now: contact maintainers of " + "'mrp_unbuild_tracked_raw_material' module " + "if you want this feature")) + lot = self.env['stock.production.lot'].create( + self._prepare_lots_for_purchased_unbuild( + produce_move.product_id)) + self.env['stock.move.line'].create({ + 'move_id': produce_move.id, + 'lot_id': lot.id, + 'qty_done': produce_move.product_uom_qty, + 'product_id': produce_move.product_id.id, + 'product_uom_id': produce_move.product_uom.id, + 'location_id': produce_move.location_id.id, + 'location_dest_id': produce_move.location_dest_id.id, + }) + else: + produce_move.quantity_done = produce_move.product_uom_qty + # comes from native code + produce_moves._action_done() + produced_move_line_ids = produce_moves.mapped( + 'move_line_ids').filtered(lambda ml: ml.qty_done > 0) + consume_move.move_line_ids.write( + {'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]}) + self.message_post( + body=_("Product has been unbuilt without previous " + "manufacturing order")) + return self.write({'state': 'done'}) + + def _prepare_lots_for_purchased_unbuild(self, product): + # Customize your data lot with your own code + return { + "name": datetime.now().strftime(DT_FORMAT), + "product_id": product.id, + } diff --git a/mrp_unbuild_tracked_raw_material/readme/CONFIGURE.rst b/mrp_unbuild_tracked_raw_material/readme/CONFIGURE.rst new file mode 100644 index 000000000..573718713 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/readme/CONFIGURE.rst @@ -0,0 +1 @@ +Customize method `_prepare_lots_for_purchased_unbuild()` to define your own data lot diff --git a/mrp_unbuild_tracked_raw_material/readme/CONTRIBUTORS.rst b/mrp_unbuild_tracked_raw_material/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..55e5f2ff8 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +Akretion: + + * David Béal diff --git a/mrp_unbuild_tracked_raw_material/readme/DESCRIPTION.rst b/mrp_unbuild_tracked_raw_material/readme/DESCRIPTION.rst new file mode 100644 index 000000000..3b73611b1 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/readme/DESCRIPTION.rst @@ -0,0 +1,17 @@ +Odoo has a limitation on tracked product's components +which are not manufactured in the ERP. + +When you try to do it, you get this warning: + +Some of your components are tracked, you have to specify a manufacturing order in order to retrieve the correct components. + +Unfortunately, it doesn't cover all the use cases. + +Example: +You receive eggs and you want to unbuild them in 2 parts: + + - yellow part + - white part + +Each of the parts are tracked and not linked to a previous manufacturing order +because, you don't build the eggs yourself, you subcontract it to a chicken. diff --git a/mrp_unbuild_tracked_raw_material/readme/ROADMAP.rst b/mrp_unbuild_tracked_raw_material/readme/ROADMAP.rst new file mode 100644 index 000000000..14e25ae53 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +This module doesn't take account product `allow_unbuild_purchased` checked +which use `serial` tracking + diff --git a/mrp_unbuild_tracked_raw_material/readme/USAGE.rst b/mrp_unbuild_tracked_raw_material/readme/USAGE.rst new file mode 100644 index 000000000..bbb3ed384 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/readme/USAGE.rst @@ -0,0 +1,6 @@ +# Check 'Allow Unbuild Purchased' field in Inventory tab on product form +for any product with a bom you didn't manufactured but you want unbuild. + +# Go to Manufacturing > Operations > Unbuild Orders + +# Encode an unbuild order with the product used in first step diff --git a/mrp_unbuild_tracked_raw_material/tests/__init__.py b/mrp_unbuild_tracked_raw_material/tests/__init__.py new file mode 100644 index 000000000..96a4196aa --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/tests/__init__.py @@ -0,0 +1 @@ +from . import test_unbuild diff --git a/mrp_unbuild_tracked_raw_material/tests/test_unbuild.py b/mrp_unbuild_tracked_raw_material/tests/test_unbuild.py new file mode 100644 index 000000000..2fd15850d --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/tests/test_unbuild.py @@ -0,0 +1,71 @@ +# Copyright (C) 2019 Akretion (http://www.akretion.com). All Rights Reserved +# @author David BEAL +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +# from odoo.tests.common import TransactionCase +from odoo.addons.mrp.tests.common import TestMrpCommon +from datetime import datetime + + +class TestUnbuildUnmanufacturedProduct(TestMrpCommon): + + def setUp(self, *args, **kwargs): + self.loc = self.env.ref("stock.stock_location_stock") + super().setUp(*args, **kwargs) + + def create_data(self): + prd_to_build = self.env["product.product"].create({ + "name": "To unbuild", + "type": "product", + "allow_unbuild_purchased": True, + "tracking": "lot", + }) + prd_to_use1 = self.env["product.product"].create({ + "name": "component 1", + "type": "product", + "tracking": "lot", + }) + prd_to_use2 = self.env["product.product"].create({ + "name": "component 2", + "type": "product", + "tracking": "none", + }) + bom = self.env["mrp.bom"].create({ + "product_id": prd_to_build.id, + "product_tmpl_id": prd_to_build.product_tmpl_id.id, + "product_uom_id": self.uom_unit.id, + "product_qty": 1.0, + "type": "normal", + "bom_line_ids": [ + (0, 0, {"product_id": prd_to_use2.id, + "product_qty": 1}), + (0, 0, {"product_id": prd_to_use1.id, + "product_qty": 2}) + ]}) + return (bom, prd_to_build, prd_to_use1, prd_to_use2) + + def test_unbuild(self): + """ """ + bom, prd_to_build, prd_to_use1, prd_to_use2 = self.create_data() + lot = self.env['stock.production.lot'].create( + {"name": "%s" % datetime.now(), + "product_id": prd_to_build.id}) + self.env["stock.quant"]._update_available_quantity( + prd_to_build, self.loc, 10, lot_id=lot) + unbuild = self.env["mrp.unbuild"].create({ + "product_id": prd_to_build.id, + "bom_id": bom.id, + "product_qty": 1.0, + "lot_id": lot.id, + "product_uom_id": self.uom_unit.id, + }) + unbuild.action_validate() + self._check_qty(9, prd_to_build) + self._check_qty(2, prd_to_use1) + self._check_qty(1, prd_to_use2) + + def _check_qty(self, qty, product): + self.assertEqual(self.env["stock.quant"]._get_available_quantity( + product, self.loc), qty, + "You should have the %s product '%s' in stock" % ( + qty, product.name)) diff --git a/mrp_unbuild_tracked_raw_material/views/product_view.xml b/mrp_unbuild_tracked_raw_material/views/product_view.xml new file mode 100644 index 000000000..ec4fb0ad5 --- /dev/null +++ b/mrp_unbuild_tracked_raw_material/views/product_view.xml @@ -0,0 +1,16 @@ + + + + + + product.template + + + + + + + + +