From 99b56ec7069b4d34ae1fc04305d3876c98648f51 Mon Sep 17 00:00:00 2001 From: Nicolas Pierre Date: Mon, 20 Dec 2021 09:42:35 +0000 Subject: [PATCH] [IMP] mrp_subcontracting: subcontracting portal Creation of a subcontracting portal to let the subcontractor register himself all the components that were used for the production. The portal is reusing the backend view of the stock picking where components were registered before. closes odoo/odoo#83465 Task: 2645298 Community-pr: https://github.com/odoo/odoo/pull/83465 Enterprise-pr: https://github.com/odoo/enterprise/pull/24647 Related: odoo/upgrade#3402 Related: odoo/enterprise#24647 Signed-off-by: Arnold Moyaux --- addons/mrp/models/mrp_production.py | 12 +- addons/mrp/models/stock_move.py | 7 +- addons/mrp_subcontracting/__init__.py | 1 + addons/mrp_subcontracting/__manifest__.py | 23 +++ .../controllers/__init__.py | 4 + .../mrp_subcontracting/controllers/portal.py | 119 ++++++++++++++ .../data/mrp_subcontracting_demo.xml | 4 +- addons/mrp_subcontracting/models/__init__.py | 1 + .../models/mrp_production.py | 26 +++- .../mrp_subcontracting/models/res_partner.py | 44 +++++- .../models/stock_location.py | 15 ++ .../mrp_subcontracting/models/stock_move.py | 29 +++- .../models/stock_picking.py | 3 + .../security/ir.model.access.csv | 17 ++ .../security/mrp_subcontracting_security.xml | 146 ++++++++++++++++++ .../static/src/subcontracting_portal/main.js | 5 + .../subcontracting_portal/move_list_view.js | 12 ++ .../picking_form_controller.js | 29 ++++ .../subcontracting_portal.js | 58 +++++++ .../subcontracting_portal.xml | 9 ++ .../tests/tours/subcontracting_portal_tour.js | 21 +++ addons/mrp_subcontracting/tests/__init__.py | 1 + .../tests/test_subcontracting.py | 144 +++++++++++++++++ .../tests/test_subcontracting_portal_ui.py | 61 ++++++++ .../views/mrp_production_views.xml | 36 +++-- .../views/stock_move_views.xml | 90 ++++++++++- .../views/subcontracting_portal_templates.xml | 87 +++++++++++ .../views/subcontracting_portal_views.xml | 58 +++++++ .../__manifest__.py | 4 + .../security/ir.model.access.csv | 3 + .../mrp_subcontracting_account_security.xml | 18 +++ .../__manifest__.py | 3 + .../data/mrp_subcontracting_purchase_demo.xml | 18 +++ addons/mrp_subcontracting_repair/__init__.py | 0 .../mrp_subcontracting_repair/__manifest__.py | 21 +++ .../security/ir.model.access.csv | 2 + .../mrp_subcontracting_repair_security.xml | 17 ++ addons/product/models/product_product.py | 12 +- addons/stock/data/stock_demo.xml | 3 + addons/stock/models/res_config_settings.py | 2 +- addons/stock/models/stock_location.py | 6 +- addons/stock/models/stock_move_line.py | 2 +- addons/stock/models/stock_picking.py | 4 +- 43 files changed, 1134 insertions(+), 43 deletions(-) create mode 100644 addons/mrp_subcontracting/controllers/__init__.py create mode 100644 addons/mrp_subcontracting/controllers/portal.py create mode 100644 addons/mrp_subcontracting/models/stock_location.py create mode 100644 addons/mrp_subcontracting/security/ir.model.access.csv create mode 100644 addons/mrp_subcontracting/security/mrp_subcontracting_security.xml create mode 100644 addons/mrp_subcontracting/static/src/subcontracting_portal/main.js create mode 100644 addons/mrp_subcontracting/static/src/subcontracting_portal/move_list_view.js create mode 100644 addons/mrp_subcontracting/static/src/subcontracting_portal/picking_form_controller.js create mode 100644 addons/mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.js create mode 100644 addons/mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.xml create mode 100644 addons/mrp_subcontracting/static/tests/tours/subcontracting_portal_tour.js create mode 100644 addons/mrp_subcontracting/tests/test_subcontracting_portal_ui.py create mode 100644 addons/mrp_subcontracting/views/subcontracting_portal_templates.xml create mode 100644 addons/mrp_subcontracting/views/subcontracting_portal_views.xml create mode 100644 addons/mrp_subcontracting_account/security/ir.model.access.csv create mode 100644 addons/mrp_subcontracting_account/security/mrp_subcontracting_account_security.xml create mode 100644 addons/mrp_subcontracting_purchase/data/mrp_subcontracting_purchase_demo.xml create mode 100644 addons/mrp_subcontracting_repair/__init__.py create mode 100644 addons/mrp_subcontracting_repair/__manifest__.py create mode 100644 addons/mrp_subcontracting_repair/security/ir.model.access.csv create mode 100644 addons/mrp_subcontracting_repair/security/mrp_subcontracting_repair_security.xml diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 00b2752f8a3..483be6c2e2f 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -728,9 +728,9 @@ class MrpProduction(models.Model): @api.onchange('lot_producing_id') def _onchange_lot_producing(self): if self.product_id.tracking == 'serial' and self.lot_producing_id: - message, dummy = self.env['stock.quant']._check_serial_number(self.product_id, - self.lot_producing_id, - self.company_id) + message, dummy = self.env['stock.quant'].sudo()._check_serial_number(self.product_id, + self.lot_producing_id, + self.company_id) if message: return {'warning': {'title': _('Warning'), 'message': message}} @@ -782,8 +782,7 @@ class MrpProduction(models.Model): finished_move_lines.write({'lot_id': vals.get('lot_producing_id')}) if 'qty_producing' in vals: finished_move_lines.write({'qty_done': vals.get('qty_producing')}) - - if not production.workorder_ids.operation_id and vals.get('date_planned_start') and not vals.get('date_planned_finished'): + if self._has_workorders() and not production.workorder_ids.operation_id and vals.get('date_planned_start') and not vals.get('date_planned_finished'): new_date_planned_start = fields.Datetime.to_datetime(vals.get('date_planned_start')) if not production.date_planned_finished or new_date_planned_start >= production.date_planned_finished: production.date_planned_finished = new_date_planned_start + datetime.timedelta(hours=1) @@ -2036,6 +2035,9 @@ class MrpProduction(models.Model): 'target': 'main', } + def _has_workorders(self): + return self.workorder_ids + @api.model def _prepare_procurement_group_vals(self, values): return {'name': values['name']} diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 6e299685e37..b314e9ab5c4 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -132,7 +132,7 @@ class StockMove(models.Model): 'Done', compute='_compute_is_done', store=True, help='Technical Field to order moves') - order_finished_lot_ids = fields.Many2many('stock.lot', string="Finished Lot/Serial Number", compute='_compute_order_finished_lot_ids') + order_finished_lot_id = fields.Many2one('stock.lot', string="Finished Lot/Serial Number", related="raw_material_production_id.lot_producing_id", store=True) should_consume_qty = fields.Float('Quantity To Consume', compute='_compute_should_consume_qty', digits='Product Unit of Measure') cost_share = fields.Float( "Cost Share (%)", digits=(5, 2), # decimal = 2 is important for rounding calculations!! @@ -180,11 +180,6 @@ class StockMove(models.Model): if move.raw_material_production_id or move.production_id: move.picking_type_id = (move.raw_material_production_id or move.production_id).picking_type_id - @api.depends('raw_material_production_id.lot_producing_id') - def _compute_order_finished_lot_ids(self): - for move in self: - move.order_finished_lot_ids = move.raw_material_production_id.lot_producing_id - @api.depends('raw_material_production_id.is_locked', 'production_id.is_locked') def _compute_is_locked(self): super(StockMove, self)._compute_is_locked() diff --git a/addons/mrp_subcontracting/__init__.py b/addons/mrp_subcontracting/__init__.py index ae586949745..dfbd96001d5 100644 --- a/addons/mrp_subcontracting/__init__.py +++ b/addons/mrp_subcontracting/__init__.py @@ -3,6 +3,7 @@ from . import models from . import report from . import wizard +from . import controllers from odoo import SUPERUSER_ID, api diff --git a/addons/mrp_subcontracting/__manifest__.py b/addons/mrp_subcontracting/__manifest__.py index 34a473c4b60..55bb8dcf534 100644 --- a/addons/mrp_subcontracting/__manifest__.py +++ b/addons/mrp_subcontracting/__manifest__.py @@ -11,6 +11,8 @@ 'depends': ['mrp'], 'data': [ 'data/mrp_subcontracting_data.xml', + 'security/mrp_subcontracting_security.xml', + 'security/ir.model.access.csv', 'views/mrp_bom_views.xml', 'views/res_partner_views.xml', 'views/stock_warehouse_views.xml', @@ -20,12 +22,33 @@ 'views/supplier_info_views.xml', 'views/product_views.xml', 'views/mrp_production_views.xml', + 'views/subcontracting_portal_views.xml', + 'views/subcontracting_portal_templates.xml', 'wizard/stock_picking_return_views.xml', 'report/mrp_report_bom_structure.xml', ], 'demo': [ 'data/mrp_subcontracting_demo.xml', ], + 'assets': { + 'web.assets_tests': [ + 'mrp_subcontracting/static/tests/tours/subcontracting_portal_tour.js', + ], + 'mrp_subcontracting.assets_qweb': [ + ('include', 'web.assets_qweb'), + 'mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.xml', + ], + 'mrp_subcontracting.webclient': [ + ('include', 'web.assets_backend'), + ('remove', 'web/static/src/webclient/menus/*.js'), + 'mrp_subcontracting/static/src/subcontracting_portal/main.js', + 'mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.js', + 'mrp_subcontracting/static/src/subcontracting_portal/picking_form_controller.js', + 'mrp_subcontracting/static/src/subcontracting_portal/move_list_view.js', + 'web/static/src/start.js', + 'web/static/src/legacy/legacy_setup.js', + ], + }, 'uninstall_hook': 'uninstall_hook', 'license': 'LGPL-3', } diff --git a/addons/mrp_subcontracting/controllers/__init__.py b/addons/mrp_subcontracting/controllers/__init__.py new file mode 100644 index 00000000000..903b755e71e --- /dev/null +++ b/addons/mrp_subcontracting/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import portal diff --git a/addons/mrp_subcontracting/controllers/portal.py b/addons/mrp_subcontracting/controllers/portal.py new file mode 100644 index 00000000000..a24b41c6401 --- /dev/null +++ b/addons/mrp_subcontracting/controllers/portal.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import werkzeug +from collections import OrderedDict + +from odoo import conf, http, _ +from odoo.http import request +from odoo.exceptions import AccessError, MissingError +from odoo.addons.portal.controllers import portal +from odoo.addons.portal.controllers.portal import pager as portal_pager +from odoo.addons.web.controllers.main import HomeStaticTemplateHelpers + + +class CustomerPortal(portal.CustomerPortal): + + def _prepare_home_portal_values(self, counters): + values = super()._prepare_home_portal_values(counters) + if 'production_count' in counters: + commercial_partner = request.env.user.partner_id.commercial_partner_id + values['production_count'] = request.env['stock.picking'].search_count([('partner_id.commercial_partner_id', '=', commercial_partner.id), ('move_ids.is_subcontract', '=', True)]) + return values + + @http.route(['/my/productions', '/my/productions/page/'], type='http', auth="user", website=True) + def portal_my_productions(self, page=1, date_begin=None, date_end=None, sortby='date', filterby='all'): + commercial_partner = request.env.user.partner_id.commercial_partner_id + StockPicking = request.env['stock.picking'] + domain = [('partner_id.commercial_partner_id', '=', commercial_partner.id), ('move_ids.is_subcontract', '=', True)] + + if date_begin and date_end: + domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)] + + searchbar_filters = { + 'all': {'label': _('All'), 'domain': []}, + 'done': {'label': _('Done'), 'domain': [('state', '=', 'done')]}, + 'ready': {'label': _('Ready'), 'domain': [('state', '=', 'assigned')]}, + } + domain += searchbar_filters[filterby]['domain'] + + searchbar_sortings = { + 'date': {'label': _('Newest'), 'order': 'create_date desc, id desc'}, + 'name': {'label': _('Name'), 'order': 'name asc, id asc'}, + } + order = searchbar_sortings[sortby]['order'] + # count for pager + count = StockPicking.search_count(domain) + # make pager + pager = portal_pager( + url='/my/productions', + url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby}, + total=count, + page=page, + step=self._items_per_page + ) + # search the pickings to display, according to the pager data + pickings = StockPicking.search( + domain, + order=order, + limit=self._items_per_page, + offset=pager['offset'] + ) + + values = { + 'date': date_begin, + 'pickings': pickings, + 'page_name': 'production', + 'pager': pager, + 'searchbar_sortings': searchbar_sortings, + 'sortby': sortby, + 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), + 'filterby': filterby, + 'default_url': '/my/productions', + } + + return http.request.render("mrp_subcontracting.portal_my_productions", values) + + @http.route("/my/productions/", type="http", auth="user", methods=['GET'], website=True) + def portal_my_production(self, picking_id): + try: + self._document_check_access('stock.picking', picking_id) + except (AccessError, MissingError): + raise werkzeug.exceptions.NotFound + return request.render("mrp_subcontracting.subcontracting_portal", {'picking_id': picking_id}) + + @http.route("/my/productions//subcontracting_portal", type="http", auth="user", methods=['GET']) + def render_production_backend_view(self, picking_id): + try: + picking = self._document_check_access('stock.picking', picking_id) + except (AccessError, MissingError): + raise werkzeug.exceptions.NotFound + session_info = request.env['ir.http'].session_info() + user_context = dict(request.env.context) if request.session.uid else {} + mods = conf.server_wide_modules or [] + qweb_checksum = HomeStaticTemplateHelpers.get_qweb_templates_checksum(debug=request.session.debug, bundle="mrp_subcontracting.assets_qweb") + lang = user_context.get("lang") + translation_hash = request.env['ir.translation'].get_web_translations_hash(mods, lang) + cache_hashes = { + "qweb": qweb_checksum, + "translations": translation_hash, + } + production_company = picking.company_id + session_info.update( + cache_hashes=cache_hashes, + action_name='mrp_subcontracting.subcontracting_portal_view_production_action', + picking_id=picking.id, + user_companies={ + 'current_company': production_company.id, + 'allowed_companies': { + production_company.id: { + 'id': production_company.id, + 'name': production_company.name, + }, + }, + }) + + return request.render( + 'mrp_subcontracting.subcontracting_portal_embed', + {'session_info': session_info}, + ) diff --git a/addons/mrp_subcontracting/data/mrp_subcontracting_demo.xml b/addons/mrp_subcontracting/data/mrp_subcontracting_demo.xml index fc5f25a9b12..ca6a885ecf8 100644 --- a/addons/mrp_subcontracting/data/mrp_subcontracting_demo.xml +++ b/addons/mrp_subcontracting/data/mrp_subcontracting_demo.xml @@ -5,7 +5,7 @@ subcontract - + @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/addons/mrp_subcontracting/models/__init__.py b/addons/mrp_subcontracting/models/__init__.py index fc944c6b955..33434a1c1f6 100644 --- a/addons/mrp_subcontracting/models/__init__.py +++ b/addons/mrp_subcontracting/models/__init__.py @@ -4,6 +4,7 @@ from . import mrp_bom from . import product from . import res_company from . import res_partner +from . import stock_location from . import stock_move from . import stock_move_line from . import stock_picking diff --git a/addons/mrp_subcontracting/models/mrp_production.py b/addons/mrp_subcontracting/models/mrp_production.py index 6ddafcd1674..45720decf3c 100644 --- a/addons/mrp_subcontracting/models/mrp_production.py +++ b/addons/mrp_subcontracting/models/mrp_production.py @@ -3,7 +3,7 @@ from collections import defaultdict from odoo import fields, models, _, api -from odoo.exceptions import UserError, ValidationError +from odoo.exceptions import UserError, ValidationError, AccessError from odoo.tools.float_utils import float_compare, float_is_zero @@ -16,6 +16,8 @@ class MrpProduction(models.Model): inverse='_inverse_move_line_raw_ids', compute='_compute_move_line_raw_ids' ) subcontracting_has_been_recorded = fields.Boolean("Has been recorded?", copy=False) + subcontractor_id = fields.Many2one('res.partner', string="Subcontractor", help="Used to restrict access to the portal user through Record Rules") + bom_product_ids = fields.Many2many('product.product', compute="_compute_bom_product_ids", help="List of Products used in the BoM, used to filter the list of products in the subcontracting portal view") incoming_picking = fields.Many2one(related='move_finished_ids.move_dest_ids.picking_id') @@ -31,6 +33,10 @@ class MrpProduction(models.Model): for production in self: production.move_line_raw_ids = production.move_raw_ids.move_line_ids + def _compute_bom_product_ids(self): + for production in self: + production.bom_product_ids = production.bom_id.bom_line_ids.product_id + def _inverse_move_line_raw_ids(self): for production in self: line_by_product = defaultdict(lambda: self.env['stock.move.line']) @@ -45,6 +51,13 @@ class MrpProduction(models.Model): production.move_raw_ids = [(0, 0, move)] production.move_raw_ids.filtered(lambda m: m.product_id == product_id)[:1].move_line_ids = lines + def write(self, vals): + if self.env.user.has_group('base.group_portal') and not self.env.su: + unauthorized_fields = set(vals.keys()) - set(self._get_writeable_fields_portal_user()) + if unauthorized_fields: + raise AccessError(_("You cannot write on fields %s in mrp.production.", ', '.join(unauthorized_fields))) + return super().write(vals) + def action_merge(self): if any(production._get_subcontract_move() for production in self): raise ValidationError(_("Subcontracted manufacturing orders cannot be merged.")) @@ -72,7 +85,7 @@ class MrpProduction(models.Model): quantity_issues = self._get_quantity_produced_issues() if quantity_issues: - backorder = self._split_productions()[1:] + backorder = self.sudo()._split_productions()[1:] # No qty to consume to avoid propagate additional move # TODO avoid : stock move created in backorder with 0 as qty backorder.move_raw_ids.filtered(lambda m: m.additional).product_uom_qty = 0.0 @@ -175,5 +188,14 @@ class MrpProduction(models.Model): def _has_tracked_component(self): return any(m.has_tracking != 'none' for m in self.move_raw_ids) + def _has_workorders(self): + if self.subcontractor_id: + return False + else: + return super()._has_workorders() + def _get_subcontract_move(self): return self.move_finished_ids.move_dest_ids.filtered(lambda m: m.is_subcontract) + + def _get_writeable_fields_portal_user(self): + return ['move_line_raw_ids', 'lot_producing_id', 'subcontracting_has_been_recorded', 'qty_producing', 'product_qty'] diff --git a/addons/mrp_subcontracting/models/res_partner.py b/addons/mrp_subcontracting/models/res_partner.py index 67756a80175..e2aa3f1278f 100644 --- a/addons/mrp_subcontracting/models/res_partner.py +++ b/addons/mrp_subcontracting/models/res_partner.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class ResPartner(models.Model): @@ -12,7 +12,37 @@ class ResPartner(models.Model): help="The stock location used as source and destination when sending\ goods to this contact during a subcontracting process.") is_subcontractor = fields.Boolean( - string="Subcontractor", store=False, search="_search_is_subcontractor") + string="Subcontractor", store=False, search="_search_is_subcontractor", compute="_compute_is_subcontractor") + bom_ids = fields.Many2many('mrp.bom', compute='_compute_bom_ids', string="BoMs for which the Partner is one of the subcontractors") + production_ids = fields.Many2many('mrp.production', compute='_compute_production_ids', string="MRP Productions for which the Partner is the subcontractor") + picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string="Stock Pickings for which the Partner is the subcontractor") + + def _compute_bom_ids(self): + results = self.env['mrp.bom'].read_group([('subcontractor_ids.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['ids:array_agg(id)', 'subcontractor_ids'], ['subcontractor_ids']) + for partner in self: + bom_ids = [] + for res in results: + if partner.id == res['subcontractor_ids'][0] or res['subcontractor_ids'][0] in partner.child_ids.ids: + bom_ids += res['ids'] + partner.bom_ids = bom_ids + + def _compute_production_ids(self): + results = self.env['mrp.production'].read_group([('subcontractor_id.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['ids:array_agg(id)'], ['subcontractor_id']) + for partner in self: + production_ids = [] + for res in results: + if partner.id == res['subcontractor_id'][0] or res['subcontractor_id'][0] in partner.child_ids.ids: + production_ids += res['ids'] + partner.production_ids = production_ids + + def _compute_picking_ids(self): + results = self.env['stock.picking'].read_group([('partner_id.commercial_partner_id', 'in', self.commercial_partner_id.ids)], ['ids:array_agg(id)'], ['partner_id']) + for partner in self: + picking_ids = [] + for res in results: + if partner.id == res['partner_id'][0] or res['partner_id'][0] in partner.child_ids.ids: + picking_ids += res['ids'] + partner.picking_ids = picking_ids def _search_is_subcontractor(self, operator, value): assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported' @@ -23,3 +53,13 @@ class ResPartner(models.Model): else: search_operator = 'not in' return [('id', search_operator, subcontractor_ids)] + + @api.depends_context('uid') + def _compute_is_subcontractor(self): + """ Check if the user is a subcontractor before giving sudo access + """ + for partner in self: + partner.is_subcontractor = (partner.user_has_groups('base.group_portal') and partner.env['mrp.bom'].search_count([ + ('type', '=', 'subcontract'), + ('subcontractor_ids', 'in', (partner.env.user.partner_id | partner.env.user.partner_id.commercial_partner_id).ids), + ])) diff --git a/addons/mrp_subcontracting/models/stock_location.py b/addons/mrp_subcontracting/models/stock_location.py new file mode 100644 index 00000000000..c012885b9a0 --- /dev/null +++ b/addons/mrp_subcontracting/models/stock_location.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class StockLocation(models.Model): + _inherit = 'stock.location' + + def _check_access_putaway(self): + """ Use sudo mode for subcontractor """ + if self.env.user.partner_id.is_subcontractor: + return self.sudo() + else: + return super()._check_access_putaway() diff --git a/addons/mrp_subcontracting/models/stock_move.py b/addons/mrp_subcontracting/models/stock_move.py index 6adff2b907f..c8e1ff04535 100644 --- a/addons/mrp_subcontracting/models/stock_move.py +++ b/addons/mrp_subcontracting/models/stock_move.py @@ -3,8 +3,8 @@ from collections import defaultdict -from odoo import fields, models, _ -from odoo.exceptions import UserError +from odoo import fields, models, api, _ +from odoo.exceptions import UserError, AccessError from odoo.tools.float_utils import float_compare, float_is_zero @@ -37,6 +37,9 @@ class StockMove(models.Model): for move in self: if not move.is_subcontract: continue + if self.env.user.has_group('base.group_portal'): + move.show_details_visible = any(not p._has_been_recorded() for p in move._get_subcontract_production()) + continue if not move._get_subcontract_production()._has_tracked_component(): continue move.show_details_visible = True @@ -55,6 +58,7 @@ class StockMove(models.Model): """ If the initial demand is updated then also update the linked subcontract order to the new quantity. """ + self._check_access_if_subcontractor(values) if 'product_uom_qty' in values and self.env.context.get('cancel_backorder') is not False: self.filtered(lambda m: m.is_subcontract and m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty(values['product_uom_qty']) res = super().write(values) @@ -68,6 +72,12 @@ class StockMove(models.Model): }) return res + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + self._check_access_if_subcontractor(vals) + return super().create(vals_list) + def action_show_details(self): """ Open the produce wizard in order to register tracked components for subcontracted product. Otherwise use standard behavior. @@ -82,6 +92,11 @@ class StockMove(models.Model): 'show_lots_m2o': self.has_tracking != 'none', 'show_lots_text': False, }) + elif self.env.user.has_group('base.group_portal'): + if self.picking_type_id.show_reserved: + action['views'] = [(self.env.ref('mrp_subcontracting.mrp_subcontracting_view_stock_move_operations').id, 'form')] + else: + action['views'] = [(self.env.ref('mrp_subcontracting.mrp_subcontracting_view_stock_move_nosuggest_operations').id, 'form')] return action def action_show_subcontract_details(self): @@ -90,6 +105,9 @@ class StockMove(models.Model): tree_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_move_tree_view') form_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view') ctx = dict(self._context, search_default_by_product=True) + if self.env.user.has_group('base.group_portal'): + form_view = self.env.ref('mrp_subcontracting.mrp_subcontracting_portal_move_form_view') + ctx.update(no_breadcrumbs=False) return { 'name': _('Raw Materials for %s') % (self.product_id.display_name), 'type': 'ir.actions.act_window', @@ -147,6 +165,8 @@ class StockMove(models.Model): self.ensure_one() production = self._get_subcontract_production()[-1:] view = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_form_view') + if self.env.user.has_group('base.group_portal'): + view = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_portal_form_view') return { 'name': _('Subcontract'), 'type': 'ir.actions.act_window', @@ -221,3 +241,8 @@ class StockMove(models.Model): 'mo_id': production.id, 'product_qty': production.product_uom_qty - quantity_to_remove }).change_prod_qty() + + def _check_access_if_subcontractor(self, vals): + if self.env.user.has_group('base.group_portal') and not self.env.su: + if vals.get('state') == 'done': + raise AccessError(_("Portal users cannot create a stock move with a state 'Done' or change the current state to 'Done'.")) diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py index c1f0803b43c..4662125086a 100644 --- a/addons/mrp_subcontracting/models/stock_picking.py +++ b/addons/mrp_subcontracting/models/stock_picking.py @@ -10,6 +10,7 @@ from dateutil.relativedelta import relativedelta class StockPicking(models.Model): + _name = 'stock.picking' _inherit = 'stock.picking' display_action_record_components = fields.Selection( @@ -123,6 +124,8 @@ class StockPicking(models.Model): vals = { 'company_id': subcontract_move.company_id.id, 'procurement_group_id': group.id, + 'subcontractor_id': subcontract_move.picking_id.partner_id.commercial_partner_id.id, + 'picking_ids': [subcontract_move.picking_id.id], 'product_id': product.id, 'product_uom_id': subcontract_move.product_uom.id, 'bom_id': bom.id, diff --git a/addons/mrp_subcontracting/security/ir.model.access.csv b/addons/mrp_subcontracting/security/ir.model.access.csv new file mode 100644 index 00000000000..41409c2b5ee --- /dev/null +++ b/addons/mrp_subcontracting/security/ir.model.access.csv @@ -0,0 +1,17 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_subcontracting_portal_stock_picking,subcontracting.portal.picking,stock.model_stock_picking,base.group_portal,1,0,0,0 +access_subcontracting_portal_picking_type,subcontracting.portal.picking.type,stock.model_stock_picking_type,base.group_portal,1,0,0,0 +access_subcontracting_portal_stock_move,subcontracting.portal.move,stock.model_stock_move,base.group_portal,1,1,1,0 +access_subcontracting_portal_stock_move_line,subcontracting.portal.move.line,stock.model_stock_move_line,base.group_portal,1,1,1,1 +access_subcontracting_portal_warehouse,subcontracting.portal.warehouse,stock.model_stock_warehouse,base.group_portal,1,0,0,0 +access_subcontracting_portal_lot,subcontracting.portal.lot,stock.model_stock_lot,base.group_portal,1,0,1,0 +access_subcontracting_portal_location,subcontracting.portal.location,stock.model_stock_location,base.group_portal,1,0,0,0 +access_subcontracting_portal_production,subcontracting.portal.production,mrp.model_mrp_production,base.group_portal,1,1,0,0 +access_subcontracting_portal_bom,subcontracting.portal.bom,mrp.model_mrp_bom,base.group_portal,1,0,0,0 +access_subcontracting_portal_bom_line,subcontracting.portal.bom.line,mrp.model_mrp_bom_line,base.group_portal,1,0,0,0 +access_subcontracting_portal_consumption_warning,subcontracting.portal.consumption.warning,mrp.model_mrp_consumption_warning,base.group_portal,1,1,1,0 +access_subcontracting_portal_consumption_warning_line,subcontracting.portal.consumption.warning.line,mrp.model_mrp_consumption_warning_line,base.group_portal,1,1,1,0 +access_subcontracting_portal_product,subcontracting.portal.product,product.model_product_product,base.group_portal,1,0,0,0 +access_subcontracting_portal_product_template,subcontracting.portal.product.template,product.model_product_template,base.group_portal,1,0,0,0 +access_subcontracting_portal_uom,subcontracting.portal.uom,uom.model_uom_uom,base.group_portal,1,0,0,0 +access_subcontracting_portal_barcode_nomenclature_stock_user,subcontracting.portal.barcode.nomenclature,barcodes.model_barcode_nomenclature,base.group_portal,1,0,0,0 diff --git a/addons/mrp_subcontracting/security/mrp_subcontracting_security.xml b/addons/mrp_subcontracting/security/mrp_subcontracting_security.xml new file mode 100644 index 00000000000..86fbf33e8ee --- /dev/null +++ b/addons/mrp_subcontracting/security/mrp_subcontracting_security.xml @@ -0,0 +1,146 @@ + + + + + MRP Productions Subcontractor + + [('subcontractor_id', '=', user.partner_id.commercial_partner_id.id)] + + + + + MRP BoMs Subcontractor + + [('id', 'in', user.partner_id.commercial_partner_id.bom_ids.ids)] + + + + + MRP BoM Lines Subcontractor + + [('id', 'in', user.partner_id.commercial_partner_id.bom_ids.bom_line_ids.ids)] + + + + + MRP Consumption Warnings Subcontractor + + [('mrp_production_ids', 'in', user.partner_id.commercial_partner_id.production_ids.ids)] + + + + + MRP Consumption Warning Lines Subcontractor + + [('mrp_production_id', 'in', user.partner_id.commercial_partner_id.production_ids.ids)] + + + + + Stock Moves Subcontractor + + [ + '|', + '|', + ('production_id.subcontractor_id', '=', user.partner_id.commercial_partner_id.id), + ('move_orig_ids.production_id.subcontractor_id', 'in', user.partner_id.commercial_partner_id.ids), + ('raw_material_production_id.subcontractor_id', 'in', user.partner_id.commercial_partner_id.ids) + ] + + + + + Stock Move Lines Subcontractor + + [ + '|', + '|', + ('move_id.production_id.subcontractor_id', '=', user.partner_id.commercial_partner_id.id), + ('move_id.move_orig_ids.production_id.subcontractor_id', 'in', user.partner_id.commercial_partner_id.ids), + ('move_id.raw_material_production_id.subcontractor_id', 'in', user.partner_id.commercial_partner_id.ids), + ] + + + + + Stock Pickings Subcontractor + + [('partner_id.commercial_partner_id', '=', user.partner_id.commercial_partner_id.id)] + + + + + Stock Picking Types Subcontractor + + ['|', ('id', 'in', user.partner_id.commercial_partner_id.picking_ids.picking_type_id.ids), ('id', 'in', user.partner_id.commercial_partner_id.production_ids.picking_type_id.ids)] + + + + + Stock Locations Subcontractor + + [ + '|', + '|', + '|', + '|', + ('child_ids', 'in', user.partner_id.commercial_partner_id.picking_ids.location_id.ids), + ('child_ids', 'in', user.partner_id.commercial_partner_id.picking_ids.location_dest_id.ids), + '|', + ('id', 'in', user.partner_id.commercial_partner_id.picking_ids.location_id.ids), + ('id', 'in', user.partner_id.commercial_partner_id.picking_ids.location_dest_id.ids), + '|', + ('id', 'in', user.partner_id.commercial_partner_id.picking_ids.picking_type_id.warehouse_id.view_location_id.ids), + ('id', 'in', user.partner_id.commercial_partner_id.production_ids.production_location_id.ids), + ('id', 'in', user.partner_id.commercial_partner_id.production_ids.move_finished_ids.move_dest_ids.location_id.ids), + ] + + + + + Warehouses Subcontractor + + [('id', 'in', user.partner_id.commercial_partner_id.picking_ids.picking_type_id.warehouse_id.ids)] + + + + + Stock Lot Subcontractor + + [ + '|', + '|', + ('product_id', 'in', user.partner_id.commercial_partner_id.bom_ids.product_id.ids), + ('product_id', 'in', user.partner_id.commercial_partner_id.bom_ids.product_tmpl_id.product_variant_ids.ids), + ('product_id', 'in', user.partner_id.commercial_partner_id.bom_ids.bom_line_ids.product_id.ids), + ] + + + + + Product Template Subcontractor + + [ + '|', + '|', + ('id', 'in', user.partner_id.commercial_partner_id.bom_ids.product_id.product_tmpl_id.ids), + ('id', 'in', user.partner_id.commercial_partner_id.bom_ids.product_tmpl_id.ids), + ('id', 'in', user.partner_id.commercial_partner_id.bom_ids.bom_line_ids.product_id.product_tmpl_id.ids), + ] + + + + + UoM Subcontractor + + [ + '|', + '|', + ('category_id', 'in', user.partner_id.commercial_partner_id.bom_ids.product_id.product_tmpl_id.uom_id.category_id.ids), + ('category_id', 'in', user.partner_id.commercial_partner_id.bom_ids.product_tmpl_id.uom_id.category_id.ids), + ('category_id', 'in', user.partner_id.commercial_partner_id.bom_ids.bom_line_ids.product_id.product_tmpl_id.uom_id.category_id.ids), + ] + + + + diff --git a/addons/mrp_subcontracting/static/src/subcontracting_portal/main.js b/addons/mrp_subcontracting/static/src/subcontracting_portal/main.js new file mode 100644 index 00000000000..bf2fe254fbd --- /dev/null +++ b/addons/mrp_subcontracting/static/src/subcontracting_portal/main.js @@ -0,0 +1,5 @@ +/** @odoo-module **/ +import { startWebClient } from '@web/start'; +import { SubcontractingPortalWebClient } from './subcontracting_portal'; + +startWebClient(SubcontractingPortalWebClient); diff --git a/addons/mrp_subcontracting/static/src/subcontracting_portal/move_list_view.js b/addons/mrp_subcontracting/static/src/subcontracting_portal/move_list_view.js new file mode 100644 index 00000000000..f87cdb7b166 --- /dev/null +++ b/addons/mrp_subcontracting/static/src/subcontracting_portal/move_list_view.js @@ -0,0 +1,12 @@ +/** @odoo-module**/ + +import ListView from 'web.ListView'; +import viewRegistry from 'web.view_registry'; + +const MoveListView = ListView.extend({ + searchMenuTypes: [], +}); + +viewRegistry.add('subcontracting_portal_move_list_view', MoveListView); + +export default MoveListView; diff --git a/addons/mrp_subcontracting/static/src/subcontracting_portal/picking_form_controller.js b/addons/mrp_subcontracting/static/src/subcontracting_portal/picking_form_controller.js new file mode 100644 index 00000000000..41ac21ce3e7 --- /dev/null +++ b/addons/mrp_subcontracting/static/src/subcontracting_portal/picking_form_controller.js @@ -0,0 +1,29 @@ +/** @odoo-module**/ + +import FormController from 'web.FormController'; +import FormView from 'web.FormView'; +import viewRegistry from 'web.view_registry'; + + +const PickingFormController = FormController.extend({ + /** + * @override + */ + init: function () { + this._super(...arguments); + this.hasActionMenus = false; + }, +}); + +const PickingFormView = FormView.extend({ + config: Object.assign({}, FormView.prototype.config, { + Controller: PickingFormController, + }), +}); + +viewRegistry.add('subcontracting_portal_picking_form_view', PickingFormView); + +export default { + PickingFormView: PickingFormView, + PickingFormController: PickingFormController +}; diff --git a/addons/mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.js b/addons/mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.js new file mode 100644 index 00000000000..ec1ad25fc98 --- /dev/null +++ b/addons/mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.js @@ -0,0 +1,58 @@ +/** @odoo-module **/ + +import { useService } from '@web/core/utils/hooks'; +import { ActionContainer } from '@web/webclient/actions/action_container'; +import { MainComponentsContainer } from "@web/core/main_components_container"; +import { useOwnDebugContext } from "@web/core/debug/debug_context"; +import { ErrorHandler } from "@web/core/utils/components"; +import { session } from '@web/session'; +import { LegacyComponent } from "@web/legacy/legacy_component"; + +const { useEffect} = owl; + +export class SubcontractingPortalWebClient extends LegacyComponent { + setup() { + window.parent.document.body.style.margin = "0"; // remove the margin in the parent body + this.actionService = useService('action'); + this.user = useService("user"); + useService("legacy_service_provider"); + useOwnDebugContext({ categories: ["default"] }); + useEffect( + () => { + this._showView(); + }, + () => [] + ); + } + + handleComponentError(error, C) { + // remove the faulty component + this.Components.splice(this.Components.indexOf(C), 1); + /** + * we rethrow the error to notify the user something bad happened. + * We do it after a tick to make sure owl can properly finish its + * rendering + */ + Promise.resolve().then(() => { + throw error; + }); + } + + async _showView() { + const { action_name, picking_id } = session; + await this.actionService.doAction( + action_name, + { + props: { + resId: picking_id, + }, + additionalContext: { + no_breadcrumbs: true, + } + } + ); + } +} + +SubcontractingPortalWebClient.components = { ActionContainer, ErrorHandler, MainComponentsContainer }; +SubcontractingPortalWebClient.template = 'mrp_subcontracting.SubcontractingPortalWebClient'; diff --git a/addons/mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.xml b/addons/mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.xml new file mode 100644 index 00000000000..783c0d0419f --- /dev/null +++ b/addons/mrp_subcontracting/static/src/subcontracting_portal/subcontracting_portal.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/addons/mrp_subcontracting/static/tests/tours/subcontracting_portal_tour.js b/addons/mrp_subcontracting/static/tests/tours/subcontracting_portal_tour.js new file mode 100644 index 00000000000..8574fad4005 --- /dev/null +++ b/addons/mrp_subcontracting/static/tests/tours/subcontracting_portal_tour.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +import tour from 'web_tour.tour'; + +const subcontractingPortalSteps = [{ + trigger: 'table > tbody > tr a:has(span:contains(WH/IN/00))', + content: 'Select the picking to open the backend view.', + run: 'click', +},{ + trigger: '.o_subcontracting_portal', + content: 'Wait the subcontracting portal to be loaded.', + run: function () {}, +}, { + trigger: 'button[name="action_show_details"]', + run: 'click', +}]; + +tour.register('subcontracting_portal_tour', { + test: true, + url: '/my/productions', +}, subcontractingPortalSteps); diff --git a/addons/mrp_subcontracting/tests/__init__.py b/addons/mrp_subcontracting/tests/__init__.py index 7cc46c985ec..ec12b1b7ca3 100644 --- a/addons/mrp_subcontracting/tests/__init__.py +++ b/addons/mrp_subcontracting/tests/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import test_subcontracting +from . import test_subcontracting_portal_ui diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py index 36494fc4b0d..a3144cd0f6f 100644 --- a/addons/mrp_subcontracting/tests/test_subcontracting.py +++ b/addons/mrp_subcontracting/tests/test_subcontracting.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import Command +from odoo.exceptions import AccessError, UserError from odoo.tests import Form from odoo.tests.common import TransactionCase from odoo.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon @@ -957,3 +958,146 @@ class TestSubcontractingTracking(TransactionCase): wizard.process() self.assertEqual(picking_receipt.state, 'done') + + +class TestSubcontractingPortal(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + # 1: Create a subcontracting partner + main_partner = cls.env['res.partner'].create({'name': 'main_partner'}) + cls.subcontractor_partner1 = cls.env['res.partner'].create({ + 'name': 'subcontractor_partner', + 'parent_id': main_partner.id, + 'company_id': cls.env.ref('base.main_company').id, + }) + # Make the subcontracting partner a portal user + cls.portal_user = cls.env['res.users'].create({ + 'name': 'portal user (subcontractor)', + 'partner_id': cls.subcontractor_partner1.id, + 'login': 'subcontractor', + 'password': 'subcontractor', + 'email': 'subcontractor@subcontracting.portal', + 'groups_id': [(6, 0, [cls.env.ref('base.group_portal').id])] + }) + + # 2. Create a BOM of subcontracting type + # 2.1. Comp1 has tracking by lot + cls.comp1_sn = cls.env['product.product'].create({ + 'name': 'Component1', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + 'tracking': 'serial' + }) + cls.comp2 = cls.env['product.product'].create({ + 'name': 'Component2', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + }) + cls.product_not_in_bom = cls.env['product.product'].create({ + 'name': 'Product not in the BoM', + 'type': 'product', + }) + + # 2.2. Finished prodcut has tracking by serial number + cls.finished_product = cls.env['product.product'].create({ + 'name': 'finished', + 'type': 'product', + 'categ_id': cls.env.ref('product.product_category_all').id, + 'tracking': 'lot' + }) + bom_form = Form(cls.env['mrp.bom']) + bom_form.type = 'subcontract' + bom_form.consumption = 'warning' + bom_form.subcontractor_ids.add(cls.subcontractor_partner1) + bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id + with bom_form.bom_line_ids.new() as bom_line: + bom_line.product_id = cls.comp1_sn + bom_line.product_qty = 1 + with bom_form.bom_line_ids.new() as bom_line: + bom_line.product_id = cls.comp2 + bom_line.product_qty = 1 + cls.bom_tracked = bom_form.save() + + def test_flow_subcontracting_portal(self): + # 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 = 2 + picking_receipt = picking_form.save() + picking_receipt.action_confirm() + + # Using the subcontractor (portal user) + lot1 = self.env['stock.lot'].with_user(self.portal_user).create({ + 'name': 'lot1', + 'product_id': self.finished_product.id, + 'company_id': self.env.company.id, + }) + lot2 = self.env['stock.lot'].with_user(self.portal_user).create({ + 'name': 'lot2', + 'product_id': self.finished_product.id, + 'company_id': self.env.company.id, + }) + serial1 = self.env['stock.lot'].with_user(self.portal_user).create({ + 'name': 'lot1', + 'product_id': self.comp1_sn.id, + 'company_id': self.env.company.id, + }) + serial2 = self.env['stock.lot'].with_user(self.portal_user).create({ + 'name': 'lot2', + 'product_id': self.comp1_sn.id, + 'company_id': self.env.company.id, + }) + serial3 = self.env['stock.lot'].with_user(self.portal_user).create({ + 'name': 'lot3', + 'product_id': self.comp1_sn.id, + 'company_id': self.env.company.id, + }) + action = picking_receipt.with_user(self.portal_user).with_context({'is_subcontracting_portal': 1}).move_ids.action_show_details() + mo = self.env['mrp.production'].with_user(self.portal_user).browse(action['res_id']) + mo_form = Form(mo.with_context(action['context']), view=action['view_id']) + # Registering components for the first manufactured product + mo_form.qty_producing = 1 + mo_form.lot_producing_id = lot1 + with mo_form.move_line_raw_ids.edit(0) as ml: + ml.lot_id = serial1 + mo = mo_form.save() + mo.subcontracting_record_component() + mo_form = Form(mo.with_context(action['context']), view=action['view_id']) + # Registering components for the second manufactured product with over-consumption, which leads to a warning + mo_form.qty_producing = 1 + mo_form.lot_producing_id = lot2 + with mo_form.move_line_raw_ids.edit(0) as ml: + ml.lot_id = serial2 + with mo_form.move_line_raw_ids.new() as ml: + ml.product_id = self.comp1_sn + ml.lot_id = serial3 + with mo_form.move_line_raw_ids.edit(1) as ml: + ml.qty_done = 2 + # The portal user should not be able to add a product not in the BoM + with self.assertRaises(AccessError): + with mo_form.move_line_raw_ids.new() as ml: + ml.product_id = self.product_not_in_bom + mo = mo_form.save() + action_warning = mo.subcontracting_record_component() + warning = Form(self.env['mrp.consumption.warning'].with_context(**action_warning['context'])) + warning = warning.save() + warning.action_confirm() + + # Attempt to validate from the portal user should give an error + with self.assertRaises(UserError): + picking_receipt.with_user(self.portal_user).button_validate() + + # Validation from the backend user + picking_receipt.button_validate() + self.assertEqual(mo.state, 'done') + self.assertEqual(mo.move_line_raw_ids[0].qty_done, 1) + self.assertEqual(mo.move_line_raw_ids[0].lot_id, serial2) + self.assertEqual(mo.move_line_raw_ids[1].qty_done, 1) + self.assertEqual(mo.move_line_raw_ids[1].lot_id, serial3) + self.assertEqual(mo.move_line_raw_ids[2].qty_done, 2) diff --git a/addons/mrp_subcontracting/tests/test_subcontracting_portal_ui.py b/addons/mrp_subcontracting/tests/test_subcontracting_portal_ui.py new file mode 100644 index 00000000000..5f84565d15e --- /dev/null +++ b/addons/mrp_subcontracting/tests/test_subcontracting_portal_ui.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +from odoo import Command +from odoo.tests import Form, HttpCase, tagged + + +@tagged('post_install', '-at_install') +class TestSubcontractingPortalUi(HttpCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + # 1. Create portal user + user = cls.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nolog': True}).create({ + 'name': 'Georges', + 'login': 'georges1', + 'password': 'georges1', + 'email': 'georges@project.portal', + 'signature': 'SignGeorges', + 'notification_type': 'email', + 'groups_id': [Command.set([cls.env.ref('base.group_portal').id])], + }) + + cls.partner_portal = cls.env['res.partner'].with_context({'mail_create_nolog': True}).create({ + 'name': 'Georges', + 'email': 'georges@project.portal', + 'company_id': False, + 'user_ids': [user.id], + }) + # 2. Create a BOM of subcontracting type + cls.comp = cls.env['product.product'].create({ + 'name': 'Component', + 'type': 'product', + }) + + cls.finished_product = cls.env['product.product'].create({ + 'name': 'Finished', + 'type': 'product', + }) + bom_form = Form(cls.env['mrp.bom']) + bom_form.type = 'subcontract' + bom_form.consumption = 'warning' + bom_form.subcontractor_ids.add(cls.partner_portal) + bom_form.product_tmpl_id = cls.finished_product.product_tmpl_id + with bom_form.bom_line_ids.new() as bom_line: + bom_line.product_id = cls.comp + bom_line.product_qty = 1 + cls.bom_tracked = bom_form.save() + + def test_subcontrating_portal(self): + # 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.partner_portal + with picking_form.move_ids_without_package.new() as move: + move.product_id = self.finished_product + move.product_uom_qty = 2 + picking_receipt = picking_form.save() + picking_receipt.action_confirm() + + self.start_tour("/my/productions", 'subcontracting_portal_tour', login="georges1") diff --git a/addons/mrp_subcontracting/views/mrp_production_views.xml b/addons/mrp_subcontracting/views/mrp_production_views.xml index 3377755ae05..ee68e2a3e39 100644 --- a/addons/mrp_subcontracting/views/mrp_production_views.xml +++ b/addons/mrp_subcontracting/views/mrp_production_views.xml @@ -30,19 +30,16 @@ 1 - - 1 - + + 1 - - 1 - 1 - - + + + @@ -52,6 +49,27 @@