diff --git a/mrp_subcontracting_partner_management/README.rst b/mrp_subcontracting_partner_management/README.rst new file mode 100644 index 000000000..38929e877 --- /dev/null +++ b/mrp_subcontracting_partner_management/README.rst @@ -0,0 +1,35 @@ +**This file is going to be generated by oca-gen-addon-readme.** + +*Manual changes will be overwritten.* + +Please provide content in the ``readme`` directory: + +* **DESCRIPTION.rst** (required) +* INSTALL.rst (optional) +* CONFIGURE.rst (optional) +* **USAGE.rst** (optional, highly recommended) +* DEVELOP.rst (optional) +* ROADMAP.rst (optional) +* HISTORY.rst (optional, recommended) +* **CONTRIBUTORS.rst** (optional, highly recommended) +* CREDITS.rst (optional) + +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. + +A good, one sentence summary in the manifest is also highly recommended. + + +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`HISTORY.rst` can be auto generated using `towncrier `_. + +Just put towncrier compatible changelog fragments into `readme/newsfragments` +and the changelog file will be automatically generated and updated when a new fragment is added. + +Please refer to `towncrier` documentation to know more. + +NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. +If you need to run it manually, refer to `OCA/maintainer-tools README `_. diff --git a/mrp_subcontracting_partner_management/__init__.py b/mrp_subcontracting_partner_management/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/mrp_subcontracting_partner_management/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mrp_subcontracting_partner_management/__manifest__.py b/mrp_subcontracting_partner_management/__manifest__.py new file mode 100644 index 000000000..e6a86df1f --- /dev/null +++ b/mrp_subcontracting_partner_management/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "Subcontracting Partner Management", + "version": "14.0.1.0.0", + "summary": "Subcontracting Partner Management", + "author": "Ooops404, Cetmix, Odoo Community Association (OCA)", + "license": "LGPL-3", + "category": "Inventory", + "website": "https://github.com/OCA/manufacture", + "depends": ["purchase_stock", "mrp_subcontracting", "sale_stock"], + "external_dependencies": {}, + "demo": [], + "data": [ + "views/res_partner.xml", + ], + "qweb": [], + "installable": True, + "application": False, +} diff --git a/mrp_subcontracting_partner_management/models/__init__.py b/mrp_subcontracting_partner_management/models/__init__.py new file mode 100644 index 000000000..91fed54d4 --- /dev/null +++ b/mrp_subcontracting_partner_management/models/__init__.py @@ -0,0 +1 @@ +from . import res_partner diff --git a/mrp_subcontracting_partner_management/models/res_partner.py b/mrp_subcontracting_partner_management/models/res_partner.py new file mode 100644 index 000000000..ec3393403 --- /dev/null +++ b/mrp_subcontracting_partner_management/models/res_partner.py @@ -0,0 +1,267 @@ +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + is_subcontractor_partner = fields.Boolean(string="Subcontractor") + subcontracted_created_location_id = fields.Many2one("stock.location") + partner_picking_type_id = fields.Many2one("stock.picking.type") + partner_buy_rule_id = fields.Many2one("stock.rule") + partner_resupply_rule_id = fields.Many2one("stock.rule") + + def _set_subcontracting_values_active(self, active): + self.ensure_one() + if self.subcontracted_created_location_id: + self.subcontracted_created_location_id.active = active + if self.partner_picking_type_id: + self.partner_picking_type_id.active = active + if self.partner_buy_rule_id: + self.partner_buy_rule_id.active = active + if self.partner_resupply_rule_id: + self.partner_resupply_rule_id.active = active + + def unlink(self): + """ + This Method is override to archive all subcotracting field + """ + for record in self: + record._set_subcontracting_values_active(False) + result = super(ResPartner, self).unlink() + return result + + def write(self, values): + for record in self: + is_subcontractor_partner = values.get("is_subcontractor_partner") + active = values.get("active") + if is_subcontractor_partner is not None: + values.update(record._update_subcontractor_entities_for_record(values)) + if active is not None: + record._set_subcontracting_values_active(active) + super(ResPartner, self).write(values) + + @api.model + def create(self, values): + partner = super(ResPartner, self).create(values) + if values.get("is_subcontractor_partner", False): + partner._create_subcontractor_entities() + return partner + + def _create_location(self, parent_location, company): + """Creating Subcontracting Location starts here""" + + name = "Subcontractor {}".format(self.name) + location_vals = { + "name": name, + "usage": "internal", + "location_id": parent_location or False, + "company_id": company.id, + "active": True, + } + location_rec = self.subcontracted_created_location_id + if not location_rec: + location_rec = self.env["stock.location"].create(location_vals) + return location_rec + + def _create_subcontracted_operation_type(self, warehouse, location): + """Creating Operation Type for Subcontracting""" + first_name = self.name.split(" ")[0] or "" + operation_type_name = "Subcontractor {} {}".format(str(first_name), " IN") + sequence_code = "" + for code in list(filter(None, operation_type_name.split(" "))): + sequence_code += code[0] + operation_type_rec = self.partner_picking_type_id + if not operation_type_rec: + operation_type_vals = { + "name": operation_type_name, + "code": "incoming", + "sequence_code": sequence_code, + } + if warehouse: + operation_type_vals.update({"warehouse_id": warehouse.id}) + if location: + operation_type_vals.update({"default_location_dest_id": location.id}) + operation_type_rec = self.env["stock.picking.type"].create( + operation_type_vals + ) + return operation_type_rec + + def _create_subcontracted_buy_rule(self, operation_type_rec, location): + """Creating Route Rule for Subcontracting starts here""" + first_name = self.name.split(" ")[0] or "" + buy_route = self.env.ref( + "purchase_stock.route_warehouse0_buy", raise_if_not_found=False + ) + rule_vals = { + "name": "Subcontractor {}".format(first_name), + "action": "buy", + } + rule = self.partner_buy_rule_id + if operation_type_rec: + rule_vals.update({"picking_type_id": operation_type_rec.id}) + if location: + rule_vals.update({"location_id": location.id}) + if buy_route: + rule_vals.update({"route_id": buy_route.id}) + if not rule and rule_vals: + rule = self.env["stock.rule"].create(rule_vals) + return rule + + def _create_subcontracted_resupply_rule(self, location): + """# Creating Route Rule for Subcontracting resupply on order starts here""" + first_name = self.name.split(" ")[0] or "" + resupply_on_order_route = self.env.ref( + "mrp_subcontracting.route_resupply_subcontractor_mto", + raise_if_not_found=False, + ) + delivery_type = self.env.ref("stock.picking_type_out", raise_if_not_found=False) + production = self.env["ir.property"]._get( + "property_stock_production", "product.template" + ) + resupply_rule_vals = { + "name": "Subcontractor {}".format(first_name), + "action": "pull", + "partner_address_id": self._origin.id, + } + pull_rule = self.partner_resupply_rule_id + if delivery_type: + resupply_rule_vals.update( + { + "picking_type_id": delivery_type.id, + } + ) + if location: + resupply_rule_vals.update( + { + "location_id": location.id, + } + ) + if production: + resupply_rule_vals.update( + { + "location_src_id": production.id, + } + ) + if resupply_on_order_route: + resupply_rule_vals.update( + { + "route_id": resupply_on_order_route.id, + } + ) + + if not pull_rule and resupply_rule_vals: + pull_rule = self.env["stock.rule"].create(resupply_rule_vals) + return pull_rule + + def _create_subcontractor_entities(self): + """ + Create entities for the subcontractor + - Stock location + - Stock operation type + - "Buy" stock rule + """ + for rec in self.filtered(lambda p: p.company_type == "company"): + partner_update_vals = rec._create_subcontractor_entities_for_record() + rec.write(partner_update_vals) + + def _update_subcontractor_entities_for_record(self, values): + self.ensure_one() + is_subcontractor_partner = values.get("is_subcontractor_partner") + + check_data = { + # Updating Subcontracting Location + "subcontracted_created_location_id": self._create_subcontracting_location_data, + # Updating Subcontracting operation type + "partner_picking_type_id": self._create_operation_type_for_subcontracting, + # Updating Route Rule for Subcontracting buy + "partner_buy_rule_id": self._create_route_rule_for_subcontracting, + # Updating Route Rule for Subcontracting resupply + "partner_resupply_rule_id": self._create_route_rule_for_subcontracting_resupply, + } + for field_name in check_data: + if is_subcontractor_partner is True and getattr(self, field_name): + getattr(self, field_name).active = True + elif is_subcontractor_partner is True and not getattr(self, field_name): + values.update(check_data[field_name]()) + elif is_subcontractor_partner is False and getattr(self, field_name): + getattr(self, field_name).active = False + + return values + + def _create_subcontractor_entities_for_record(self): + self.ensure_one() + partner_update_vals = {"is_subcontractor_partner": True} + # Creating Subcontracting Location ends here + partner_update_vals.update(self._create_subcontracting_location_data()) + partner_update_vals.update(self._create_operation_type_for_subcontracting()) + # Creating Route Rule for Subcontracting starts here + partner_update_vals.update(self._create_route_rule_for_subcontracting()) + # Creating Route Rule for Subcontracting resupply on order starts here + partner_update_vals.update( + self._create_route_rule_for_subcontracting_resupply() + ) + return partner_update_vals + + def _get_location_for_record(self): + self.ensure_one() + location = self.subcontracted_created_location_id + if not location: + default_company = self.env.company + company = self.company_id or default_company + parent_location = ( + company.subcontracting_location_id + and company.subcontracting_location_id.id + ) + location = self._create_location(parent_location, company) + self.subcontracted_created_location_id = location + return location + + def _get_warehouse_for_record(self): + self.ensure_one() + default_company = self.env.company + default_warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", default_company.id)] + )[0] + company = self.company_id or default_company + warehouse = ( + self.env["stock.warehouse"].search([("company_id", "=", company.id)])[0] + if self.company_id + else default_warehouse + ) # noqa + return warehouse + + def _create_subcontracting_location_data(self): + self.ensure_one() + location = self._get_location_for_record() + return { + "property_stock_subcontractor": location.id, + "subcontracted_created_location_id": location.id, + } + + def _create_operation_type_for_subcontracting(self): + self.ensure_one() + operation_type_rec = self.partner_picking_type_id + if not operation_type_rec: + # Creating Operation Type for Subcontracting starts here + location = self._get_location_for_record() + warehouse = self._get_warehouse_for_record() + operation_type_rec = self._create_subcontracted_operation_type( + warehouse, location + ) + self.partner_picking_type_id = operation_type_rec + return {"partner_picking_type_id": operation_type_rec.id} + + def _create_route_rule_for_subcontracting(self): + location = self._get_location_for_record() + warehouse = self._get_warehouse_for_record() + operation_type_rec = self._create_subcontracted_operation_type( + warehouse, location + ) + buy_rule = self._create_subcontracted_buy_rule(operation_type_rec, location) + + return {"partner_buy_rule_id": buy_rule.id} + + def _create_route_rule_for_subcontracting_resupply(self): + location = self._get_location_for_record() + resupply_rule = self._create_subcontracted_resupply_rule(location) + return {"partner_resupply_rule_id": resupply_rule.id} diff --git a/mrp_subcontracting_partner_management/readme/CONFIGURE.rst b/mrp_subcontracting_partner_management/readme/CONFIGURE.rst new file mode 100644 index 000000000..0862077f4 --- /dev/null +++ b/mrp_subcontracting_partner_management/readme/CONFIGURE.rst @@ -0,0 +1 @@ +* No configuration is required diff --git a/mrp_subcontracting_partner_management/readme/CONTRIBUTORS.rst b/mrp_subcontracting_partner_management/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..a3385a623 --- /dev/null +++ b/mrp_subcontracting_partner_management/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Ooops404 +* Cetmix diff --git a/mrp_subcontracting_partner_management/readme/DESCRIPTION.rst b/mrp_subcontracting_partner_management/readme/DESCRIPTION.rst new file mode 100644 index 000000000..fe086ffb2 --- /dev/null +++ b/mrp_subcontracting_partner_management/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +The goal of this module is to simplify the management of the partner properties used in MRP Subcontracting. + +It adds a new checkbox "Subcontractor" which when enabled creates the following entities: + +* A child location in the "Subcontracting" location +* A Stock Operation Type of type 'receipt' for this location +* A new 'Buy' stock rule +* A new 'Resupply Subcontractor on Order' rule diff --git a/mrp_subcontracting_partner_management/readme/HISTORY.rst b/mrp_subcontracting_partner_management/readme/HISTORY.rst new file mode 100644 index 000000000..a7312928a --- /dev/null +++ b/mrp_subcontracting_partner_management/readme/HISTORY.rst @@ -0,0 +1,4 @@ +14.0.1.0.0 +~~~~~~~~~~ + +* Initial release diff --git a/mrp_subcontracting_partner_management/readme/USAGE.rst b/mrp_subcontracting_partner_management/readme/USAGE.rst new file mode 100644 index 000000000..b5502316a --- /dev/null +++ b/mrp_subcontracting_partner_management/readme/USAGE.rst @@ -0,0 +1,4 @@ +* Select a partner of type "Company" +* Enable the "Subcontractor" checkbox +* New entities are created or existing are used if were created previously +* When disabled all associated enties will be archived diff --git a/mrp_subcontracting_partner_management/static/description/icon.png b/mrp_subcontracting_partner_management/static/description/icon.png new file mode 100644 index 000000000..ea708b0a2 Binary files /dev/null and b/mrp_subcontracting_partner_management/static/description/icon.png differ diff --git a/mrp_subcontracting_partner_management/tests/__init__.py b/mrp_subcontracting_partner_management/tests/__init__.py new file mode 100644 index 000000000..c7819b815 --- /dev/null +++ b/mrp_subcontracting_partner_management/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_create_sybcontractor_partner_location diff --git a/mrp_subcontracting_partner_management/tests/test_create_sybcontractor_partner_location.py b/mrp_subcontracting_partner_management/tests/test_create_sybcontractor_partner_location.py new file mode 100644 index 000000000..edf060089 --- /dev/null +++ b/mrp_subcontracting_partner_management/tests/test_create_sybcontractor_partner_location.py @@ -0,0 +1,170 @@ +from odoo.tests import common, tagged + + +@tagged("post_install", "-at_install") +class TestSubcontractedPartner(common.SavepointCase): + @classmethod + def setUpClass(cls): + """ + - Create a Partner record “Wood Corner” + - Type will be Company and new boolean is_subcontractor_partner is Set True + """ + super().setUpClass() + cls.partner_id = cls.env.ref("base.res_partner_12") + cls.partner_obj = cls.env["res.partner"] + + def _get_partner(self): + return self.partner_obj.create( + { + "name": "Test partner", + "is_company": True, + "is_subcontractor_partner": True, + } + ) + + def test_is_subcontractor_partner_first_time(self): + self.partner_id.update( + { + "is_subcontractor_partner": True, + } + ) + + location = self.partner_id.subcontracted_created_location_id + self.assertTrue(location, "Location is not created") + self.assertTrue(location.active, "Location must be active") + + partner_picking_type = self.partner_id.partner_picking_type_id + self.assertTrue(partner_picking_type, "Picking type is not created") + self.assertTrue(partner_picking_type.active, "Picking type must be active") + + partner_buy_rule = self.partner_id.partner_buy_rule_id + self.assertTrue(partner_buy_rule, "Partner Buy rule is not created") + self.assertTrue(partner_buy_rule.active, "Partner Buy rule must be active") + + partner_resupply_rule = self.partner_id.partner_resupply_rule_id + self.assertTrue(partner_resupply_rule, "Partner Resupply rule is not created") + self.assertTrue( + partner_resupply_rule.active, "Partner Resupply rule must be active" + ) + + def test_is_subcontractor_partner_switch_off(self): + self.partner_id.write( + { + "is_subcontractor_partner": True, + } + ) + self.partner_id.update( + { + "is_subcontractor_partner": False, + } + ) + + location = self.partner_id.subcontracted_created_location_id + self.assertFalse(location.active, "Location must be not active") + + partner_picking_type = self.partner_id.partner_picking_type_id + self.assertFalse(partner_picking_type.active, "Picking type must be not active") + + partner_buy_rule = self.partner_id.partner_buy_rule_id + self.assertFalse(partner_buy_rule.active, "Partner Buy rule must be not active") + + partner_resupply_rule = self.partner_id.partner_resupply_rule_id + self.assertFalse( + partner_resupply_rule.active, "Partner Resupply rule must be not active" + ) + + def test_is_subcontractor_partner_switch_on(self): + self.partner_id.update( + { + "is_subcontractor_partner": True, + } + ) + + location = self.partner_id.subcontracted_created_location_id + self.assertTrue(location.active, "Location must be active") + + partner_picking_type = self.partner_id.partner_picking_type_id + self.assertTrue(partner_picking_type.active, "Picking type must be active") + + partner_buy_rule = self.partner_id.partner_buy_rule_id + self.assertTrue(partner_buy_rule.active, "Partner Buy rule must be active") + + partner_resupply_rule = self.partner_id.partner_resupply_rule_id + self.assertTrue( + partner_resupply_rule.active, "Partner Resupply rule must be active" + ) + + def test_is_subcontractor_partner_aсtive_switch_off(self): + self.partner_id.write( + { + "is_subcontractor_partner": True, + } + ) + self.partner_id.update( + { + "active": False, + } + ) + + location = self.partner_id.subcontracted_created_location_id + self.assertFalse(location.active, "Location must be not active") + + partner_picking_type = self.partner_id.partner_picking_type_id + self.assertFalse(partner_picking_type.active, "Picking type must be not active") + + partner_buy_rule = self.partner_id.partner_buy_rule_id + self.assertFalse(partner_buy_rule.active, "Partner Buy rule must be not active") + + partner_resupply_rule = self.partner_id.partner_resupply_rule_id + self.assertFalse( + partner_resupply_rule.active, "Partner Resupply rule must be not active" + ) + + def test_is_subcontractor_partner_aсtive_switch_on(self): + self.partner_id.write( + { + "is_subcontractor_partner": True, + } + ) + self.partner_id.write( + { + "active": True, + } + ) + + location = self.partner_id.subcontracted_created_location_id + self.assertTrue(location.active, "Location must be active") + + partner_picking_type = self.partner_id.partner_picking_type_id + self.assertTrue(partner_picking_type.active, "Picking type must be active") + + partner_buy_rule = self.partner_id.partner_buy_rule_id + self.assertTrue(partner_buy_rule.active, "Partner Buy rule must be active") + + partner_resupply_rule = self.partner_id.partner_resupply_rule_id + self.assertTrue( + partner_resupply_rule.active, "Partner Resupply rule must be active" + ) + + def test_is_subcontractor_partner_delete(self): + partner_id = self.partner_obj.create( + { + "name": "Test partner", + "is_company": True, + "is_subcontractor_partner": True, + } + ) + + location = partner_id.subcontracted_created_location_id + partner_picking_type = partner_id.partner_picking_type_id + partner_buy_rule = partner_id.partner_buy_rule_id + partner_resupply_rule = partner_id.partner_resupply_rule_id + + partner_id.unlink() + + self.assertFalse(location.active, "Location must be not active") + self.assertFalse(partner_picking_type.active, "Picking type must be not active") + self.assertFalse(partner_buy_rule.active, "Partner Buy rule must be not active") + self.assertFalse( + partner_resupply_rule.active, "Partner Resupply rule must be not active" + ) diff --git a/mrp_subcontracting_partner_management/views/res_partner.xml b/mrp_subcontracting_partner_management/views/res_partner.xml new file mode 100644 index 000000000..77f89331e --- /dev/null +++ b/mrp_subcontracting_partner_management/views/res_partner.xml @@ -0,0 +1,20 @@ + + + + + res.partner.form.inherit.subcontractor + res.partner + + + + + + + + + + diff --git a/setup/mrp_subcontracting_partner_management/odoo/addons/mrp_subcontracting_partner_management b/setup/mrp_subcontracting_partner_management/odoo/addons/mrp_subcontracting_partner_management new file mode 120000 index 000000000..cb8463957 --- /dev/null +++ b/setup/mrp_subcontracting_partner_management/odoo/addons/mrp_subcontracting_partner_management @@ -0,0 +1 @@ +../../../../mrp_subcontracting_partner_management \ No newline at end of file diff --git a/setup/mrp_subcontracting_partner_management/setup.py b/setup/mrp_subcontracting_partner_management/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/mrp_subcontracting_partner_management/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)