[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 <arm@odoo.com>
This commit is contained in:
Nicolas Pierre
2022-04-11 11:01:50 +02:00
parent 9b53cbe575
commit 99b56ec706
43 changed files with 1134 additions and 43 deletions
+7 -5
View File
@@ -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']}
+1 -6
View File
@@ -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()
+1
View File
@@ -3,6 +3,7 @@
from . import models
from . import report
from . import wizard
from . import controllers
from odoo import SUPERUSER_ID, api
+23
View File
@@ -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',
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import portal
@@ -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/<int: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/<int:picking_id>", 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/<int:picking_id>/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},
)
@@ -5,7 +5,7 @@
<field name="product_tmpl_id" ref="product.product_delivery_02"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
<field name="type">subcontract</field>
<field name="subcontractor_ids" eval="[(4, ref('base.res_partner_12'))]"/>
<field name="subcontractor_ids" eval="[(4, ref('base.res_partner_12')), (4, ref('base.partner_demo_portal'))]"/>
</record>
<record id="mrp_bom_line_subcontract" model="mrp.bom.line">
@@ -30,4 +30,4 @@
</record>
</data>
</odoo>
</odoo>
@@ -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
@@ -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']
@@ -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),
]))
@@ -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()
+27 -2
View File
@@ -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'."))
@@ -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,
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_subcontracting_portal_stock_picking subcontracting.portal.picking stock.model_stock_picking base.group_portal 1 0 0 0
3 access_subcontracting_portal_picking_type subcontracting.portal.picking.type stock.model_stock_picking_type base.group_portal 1 0 0 0
4 access_subcontracting_portal_stock_move subcontracting.portal.move stock.model_stock_move base.group_portal 1 1 1 0
5 access_subcontracting_portal_stock_move_line subcontracting.portal.move.line stock.model_stock_move_line base.group_portal 1 1 1 1
6 access_subcontracting_portal_warehouse subcontracting.portal.warehouse stock.model_stock_warehouse base.group_portal 1 0 0 0
7 access_subcontracting_portal_lot subcontracting.portal.lot stock.model_stock_lot base.group_portal 1 0 1 0
8 access_subcontracting_portal_location subcontracting.portal.location stock.model_stock_location base.group_portal 1 0 0 0
9 access_subcontracting_portal_production subcontracting.portal.production mrp.model_mrp_production base.group_portal 1 1 0 0
10 access_subcontracting_portal_bom subcontracting.portal.bom mrp.model_mrp_bom base.group_portal 1 0 0 0
11 access_subcontracting_portal_bom_line subcontracting.portal.bom.line mrp.model_mrp_bom_line base.group_portal 1 0 0 0
12 access_subcontracting_portal_consumption_warning subcontracting.portal.consumption.warning mrp.model_mrp_consumption_warning base.group_portal 1 1 1 0
13 access_subcontracting_portal_consumption_warning_line subcontracting.portal.consumption.warning.line mrp.model_mrp_consumption_warning_line base.group_portal 1 1 1 0
14 access_subcontracting_portal_product subcontracting.portal.product product.model_product_product base.group_portal 1 0 0 0
15 access_subcontracting_portal_product_template subcontracting.portal.product.template product.model_product_template base.group_portal 1 0 0 0
16 access_subcontracting_portal_uom subcontracting.portal.uom uom.model_uom_uom base.group_portal 1 0 0 0
17 access_subcontracting_portal_barcode_nomenclature_stock_user subcontracting.portal.barcode.nomenclature barcodes.model_barcode_nomenclature base.group_portal 1 0 0 0
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.rule" id="production_subcontractor_rule">
<field name="name">MRP Productions Subcontractor</field>
<field name="model_id" ref="model_mrp_production"/>
<field name="domain_force">[('subcontractor_id', '=', user.partner_id.commercial_partner_id.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="bom_subcontractor_rule">
<field name="name">MRP BoMs Subcontractor</field>
<field name="model_id" ref="mrp.model_mrp_bom"/>
<field name="domain_force">[('id', 'in', user.partner_id.commercial_partner_id.bom_ids.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="bom_line_subcontractor_rule">
<field name="name">MRP BoM Lines Subcontractor</field>
<field name="model_id" ref="mrp.model_mrp_bom_line"/>
<field name="domain_force">[('id', 'in', user.partner_id.commercial_partner_id.bom_ids.bom_line_ids.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="consumption_warning_subcontractor_rule">
<field name="name">MRP Consumption Warnings Subcontractor</field>
<field name="model_id" ref="mrp.model_mrp_consumption_warning"/>
<field name="domain_force">[('mrp_production_ids', 'in', user.partner_id.commercial_partner_id.production_ids.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="consumption_warning_line_subcontractor_rule">
<field name="name">MRP Consumption Warning Lines Subcontractor</field>
<field name="model_id" ref="mrp.model_mrp_consumption_warning_line"/>
<field name="domain_force">[('mrp_production_id', 'in', user.partner_id.commercial_partner_id.production_ids.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="stock_move_subcontractor_rule">
<field name="name">Stock Moves Subcontractor</field>
<field name="model_id" ref="stock.model_stock_move"/>
<field name="domain_force">[
'|',
'|',
('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)
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="stock_move_line_subcontractor_rule">
<field name="name">Stock Move Lines Subcontractor</field>
<field name="model_id" ref="model_stock_move_line"/>
<field name="domain_force">[
'|',
'|',
('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),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="picking_subcontractor_rule">
<field name="name">Stock Pickings Subcontractor</field>
<field name="model_id" ref="model_stock_picking"/>
<field name="domain_force">[('partner_id.commercial_partner_id', '=', user.partner_id.commercial_partner_id.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="picking_type_subcontractor_rule">
<field name="name">Stock Picking Types Subcontractor</field>
<field name="model_id" ref="stock.model_stock_picking_type"/>
<field name="domain_force">['|', ('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)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="stock_location_subcontractor_rule">
<field name="name">Stock Locations Subcontractor</field>
<field name="model_id" ref="stock.model_stock_location"/>
<field name="domain_force">[
'|',
'|',
'|',
'|',
('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),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="stock_warehouse_subcontractor_rule">
<field name="name">Warehouses Subcontractor</field>
<field name="model_id" ref="stock.model_stock_warehouse"/>
<field name="domain_force">[('id', 'in', user.partner_id.commercial_partner_id.picking_ids.picking_type_id.warehouse_id.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="stock_lot_subcontracting_rule" model="ir.rule">
<field name="name">Stock Lot Subcontractor</field>
<field name="model_id" ref="stock.model_stock_lot"/>
<field name="domain_force">[
'|',
'|',
('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),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="product_template_subcontracting_rule" model="ir.rule">
<field name="name">Product Template Subcontractor</field>
<field name="model_id" ref="product.model_product_template"/>
<field name="domain_force">[
'|',
'|',
('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),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="uom_subcontracting_rule" model="ir.rule">
<field name="name">UoM Subcontractor</field>
<field name="model_id" ref="uom.model_uom_uom"/>
<field name="domain_force">[
'|',
'|',
('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),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</odoo>
@@ -0,0 +1,5 @@
/** @odoo-module **/
import { startWebClient } from '@web/start';
import { SubcontractingPortalWebClient } from './subcontracting_portal';
startWebClient(SubcontractingPortalWebClient);
@@ -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;
@@ -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
};
@@ -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';
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="mrp_subcontracting.SubcontractingPortalWebClient" owl="1">
<ActionContainer/>
<MainComponentsContainer/>
</t>
</templates>
@@ -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);
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import test_subcontracting
from . import test_subcontracting_portal_ui
@@ -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)
@@ -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")
@@ -30,19 +30,16 @@
<xpath expr="//div[@name='bom_div']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='move_byproduct_ids']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='move_byproduct_ids']" position="replace"/>
<xpath expr="//field[@name='workorder_ids']" position="replace"/>
<xpath expr="//button[@name='action_generate_serial']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='move_raw_ids']" position="attributes">
<attribute name="invisible">1</attribute>
<attribute name="readonly">1</attribute>
</xpath>
<xpath expr="//field[@name='move_raw_ids']" position="after">
<xpath expr="//field[@name='move_finished_ids']" position="replace"/>
<xpath expr="//field[@name='move_raw_ids']" position="replace">
<field name="bom_product_ids" invisible="1"/>
<field name="move_line_raw_ids" force_save="1"
context="{'tree_view_ref': 'mrp_subcontracting.mrp_subcontracting_stock_move_line_tree_view', 'default_company_id': company_id, 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id}"
context="{'tree_view_ref': 'mrp_subcontracting.mrp_subcontracting_stock_move_line_tree_view', 'default_company_id': company_id, 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'bom_product_ids': bom_product_ids}"
/>
</xpath>
<xpath expr="//sheet" position="inside">
@@ -52,6 +49,27 @@
<button string="Discard" special="cancel" data-hotkey="z" />
</footer>
</xpath>
<xpath expr="//div[hasclass('oe_chatter')]" position="replace"/>
</field>
</record>
<record id="mrp_production_subcontracting_portal_form_view" model="ir.ui.view">
<field name="name">mrp.production.subcontracting.portal.form.view</field>
<field name="model">mrp.production</field>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="inherit_id" ref="mrp_production_subcontracting_form_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='lot_producing_id']" position="attributes">
<attribute name="domain">[('id', '=', False)]</attribute>
<attribute name="options">{'no_create_edit': True, 'no_open': True}</attribute>
</xpath>
<xpath expr="//field[@name='move_line_raw_ids']" position="attributes">
<attribute name="context">{'tree_view_ref': 'mrp_subcontracting.mrp_subcontracting_portal_stock_move_line_tree_view', 'default_company_id': company_id, 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'bom_product_ids': bom_product_ids}</attribute>
</xpath>
<xpath expr="//field[@name='forecasted_issue']" position="replace"/>
<xpath expr="//button[@name='action_product_forecast_report']" position="replace"/>
<xpath expr="//button[@name='action_product_forecast_report']" position="replace"/>
</field>
</record>
@@ -18,7 +18,7 @@
<field name="state" invisible="1"/>
<!-- Don't put move_id here to avoid that the framework send falsy move_id -->
<field name="id" invisible="1"/>
<field name="product_id" required="1"/>
<field name="product_id" required="1" domain="[('id', 'in', context.get('bom_product_ids'))] if context.get('is_subcontracting_portal') else []"/>
<field name="lot_id"
attrs="{'invisible': [('tracking', 'not in', ('serial', 'lot'))], 'required': [('tracking', 'in', ('serial', 'lot'))]}"
context="{'default_product_id': product_id, 'default_company_id': company_id}"/>
@@ -30,6 +30,57 @@
</tree>
</field>
</record>
<record id="mrp_subcontracting_portal_stock_move_line_tree_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.portal.stock.move.line.tree.view</field>
<field name="model">stock.move.line</field>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="inherit_id" ref="mrp_subcontracting_stock_move_line_tree_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='lot_id']" position="attributes">
<attribute name="options">{'no_create_edit': True, 'no_open': True}</attribute>
</xpath>
<xpath expr="//field[@name='product_id']" position="attributes">
<attribute name="options">{'no_open': True}</attribute>
</xpath>
</field>
</record>
<record id="mrp_subcontracting_view_stock_move_line_operation_tree" model="ir.ui.view">
<field name="name">mrp.subcontracting.stock.move.line.operations.tree</field>
<field name="model">stock.move.line</field>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="inherit_id" ref="stock.view_stock_move_line_operation_tree" />
<field name="arch" type="xml">
<xpath expr="//field[@name='lot_id']" position="attributes">
<attribute name="options">{'no_create_edit': True, 'no_open': True}</attribute>
</xpath>
</field>
</record>
<record id="mrp_subcontracting_view_stock_move_operations" model="ir.ui.view">
<field name="name">mrp.subcontracting.stock.move.operations.form</field>
<field name="model">stock.move</field>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="inherit_id" ref="stock.view_stock_move_operations" />
<field name="arch" type="xml">
<xpath expr="//field[@name='move_line_ids']" position="attributes">
<attribute name="context">{'tree_view_ref': 'mrp_subcontracting.mrp_subcontracting_view_stock_move_line_operation_tree', 'default_product_uom_id': product_uom, 'default_picking_id': picking_id, 'default_move_id': id, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_company_id': company_id}</attribute>
</xpath>
</field>
</record>
<record id="mrp_subcontracting_view_stock_move_nosuggest_operations" model="ir.ui.view">
<field name="name">mrp.subcontracting.stock.move.operations.nosuggest.form</field>
<field name="model">stock.move</field>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="inherit_id" ref="stock.view_stock_move_nosuggest_operations" />
<field name="arch" type="xml">
<xpath expr="//field[@name='move_line_nosuggest_ids']" position="attributes">
<attribute name="context">{'tree_view_ref': 'mrp_subcontracting.mrp_subcontracting_view_stock_move_line_operation_tree','default_picking_id': picking_id, 'default_move_id': id, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_company_id': company_id}</attribute>
</xpath>
</field>
</record>
<record id="mrp_subcontracting_move_form_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.move.form.view</field>
<field name="model">stock.move</field>
@@ -37,7 +88,7 @@
<field name="arch" type="xml">
<form create="0" delete="0">
<header>
<field name="state" widget="statusbar"/>
<field name="state" widget="statusbar" groups="base.group_user"/>
</header>
<sheet>
<field name="product_uom_category_id" invisible="1"/>
@@ -50,7 +101,7 @@
<field name="has_tracking" invisible="1"/>
<field name="product_uom_qty" invisible="1"/>
<group>
<field name="order_finished_lot_ids" widget="many2many_tags"/>
<field name="order_finished_lot_id"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="quantity_done" string="Total Consumed" readonly="1"/>
</group>
@@ -76,12 +127,30 @@
</form>
</field>
</record>
<record id="mrp_subcontracting_portal_move_form_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.portal.move.form.view</field>
<field name="model">stock.move</field>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="inherit_id" ref="mrp_subcontracting_move_form_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='order_finished_lot_id']" position="attributes">
<attribute name="options">{'no_open': True}</attribute>
</xpath>
<xpath expr="//tree" position="attributes">
<attribute name="no_open">1</attribute>
</xpath>
<xpath expr="//tree/field[@name='lot_id']" position="attributes">
<attribute name="options">{'no_create_edit': True, 'no_open': True}</attribute>
</xpath>
</field>
</record>
<record id="mrp_subcontracting_move_tree_view" model="ir.ui.view">
<field name="name">mrp.subcontracting.move.tree.view</field>
<field name="model">stock.move</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<tree delete="0" create="0" decoration-muted="is_done" decoration-warning="quantity_done - product_uom_qty &gt; 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty &lt; 0.0001">
<tree delete="0" create="0" decoration-muted="is_done" decoration-warning="quantity_done - product_uom_qty &gt; 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty &lt; 0.0001" js_class="subcontracting_portal_move_list_view">
<field name="company_id" invisible="1"/>
<field name="sequence" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
@@ -100,11 +169,22 @@
<field name="state" invisible="1" force_save="1"/>
<field name="raw_material_production_id" invisible="1"/>
<field name="product_id" required="1"/>
<field name="order_finished_lot_ids" widget="many2many_tags"/>
<field name="order_finished_lot_id"/>
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)]}" string="Reserved"/>
<field name="quantity_done" string="Consumed" readonly="1"/>
<field name="product_uom" groups="uom.group_uom"/>
</tree>
</field>
</record>
<record id="view_move_search" model="ir.ui.view">
<field name="name">stock.move.search</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_move_search" />
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="order_finished_lot_id"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_my_home_productions" name="Productions" customize_show="True" inherit_id="portal.portal_my_home" priority="20">
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
<t t-call="portal.portal_docs_entry">
<t t-set="title">Productions</t>
<t t-set="url" t-value="'/my/productions'"/>
<t t-set="placeholder_count" t-value="'production_count'"/>
</t>
</xpath>
</template>
<template id="portal_my_home_menu_production" name="Portal layout : production menu entries" inherit_id="portal.portal_breadcrumbs" priority="25">
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
<li t-if="page_name == 'production' or production" t-attf-class="breadcrumb-item #{'active' if not pickings else ''}">
<t>Productions</t>
</li>
</xpath>
</template>
<template id="portal_my_productions" name="My Productions">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="portal.portal_searchbar">
<t t-set="title">Productions</t>
</t>
<t t-if="not pickings">
<p>There are currently no productions for your account.</p>
</t>
<t t-if="pickings" t-call="portal.portal_table">
<thead>
<tr class="active">
<th>Order</th>
<th class="text-right">Source Document</th>
<th class="text-right">Scheduled Date</th>
<th class="text-right">Deadline Date</th>
<th class="text-right">State</th>
</tr>
</thead>
<t t-foreach="pickings" t-as="picking">
<tr>
<td><a t-attf-href="/my/productions/#{picking.id}?{{ keep_query() }}"><span t-field="picking.name"/></a></td>
<td class="text-right"><span t-field="picking.origin"/></td>
<td class="text-right"><span t-field="picking.scheduled_date" t-options='{"widget": "date"}'/></td>
<td class="text-right"><span t-field="picking.date_deadline" t-options='{"widget": "date"}'/></td>
<td class="text-right"><span t-field="picking.state"/></td>
</tr>
</t>
</t>
</t>
</template>
<template id="subcontracting_portal" name="Subcontracting View in Portal">
<t t-call="portal.frontend_layout">
<t t-set="no_footer" t-value="true"/>
<t t-call="mrp_subcontracting.subcontracting"/>
</t>
</template>
<template id="subcontracting" name="Subcontracting Portal View">
<iframe width="100%" height="100%" frameborder="0" t-attf-src="/my/productions/{{ picking_id }}/subcontracting_portal"/>
</template>
<template id="subcontracting_portal_embed" name="Subcontracting Portal">
<t t-call="web.layout">
<t t-set="head_subcontracting_portal">
<script type="text/javascript">
odoo.__session_info__ = <t t-out="json.dumps(session_info)"/>;
odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.__session_info__.cache_hashes.qweb}?bundle=mrp_subcontracting.assets_qweb`).then(doc => doc.text());
</script>
<base target="_parent"/>
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="mrp_subcontracting.webclient" t-js="false"/>
<t t-call-assets="web.assets_common" t-css="false"/>
<t t-call-assets="mrp_subcontracting.webclient" t-css="false"/>
<t t-call="web.conditional_assets_tests"/>
</t>
<t t-set="head" t-value="head_subcontracting_portal + (head or '')"/>
<t t-set="body_classname" t-value="'o_web_client o_subcontracting_portal'"/>
</t>
</template>
</odoo>
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="subcontracting_portal_production_form_view" model="ir.ui.view">
<field name="name">subcontracting.portal.production.view.form</field>
<field name="model">stock.picking</field>
<field name="priority">999</field>
<field name="arch" type="xml">
<form js_class="subcontracting_portal_picking_form_view" string="Manufacturing Orders">
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Manufacturing Reference" nolabel="1"/>
</h1>
</div>
<group>
<field name="state" invisible='1'/>
<field name="scheduled_date" attrs="{'required': [('id', '!=', False)]}" decoration-warning="state not in ('done', 'cancel') and scheduled_date &lt; now" decoration-danger="state not in ('done', 'cancel') and scheduled_date &lt; current_date" decoration-bf="state not in ('done', 'cancel') and (scheduled_date &lt; current_date or scheduled_date &lt; now)"/>
<field name="date_deadline" attrs="{'invisible': ['|', ('state', 'in', ('done', 'cancel')), ('date_deadline', '=', False)]}" decoration-danger="date_deadline and date_deadline &lt; current_date" decoration-bf="date_deadline and date_deadline &lt; current_date"/>
<field name="origin" placeholder="e.g. PO0032"/>
</group>
<notebook>
<page string="Operations" name="operations">
<field name="move_ids_without_package" mode="tree">
<tree create="0">
<field name="id" readonly="1" invisible="1"/>
<field name="product_id" readonly="1"/>
<field name="show_details_visible" invisible="1"/>
<field name="description_picking" string="Description" optional="hide"/>
<field name="date" optional="hide"/>
<field name="date_deadline" optional="hide"/>
<field name="product_packaging_id" groups="product.group_stock_packaging"/>
<field name="product_uom_qty" string="Demand" readonly="1"/>
<field name="product_qty" invisible="1" readonly="1"/>
<field name="quantity_done" string="Done"/>
<field name="product_uom" groups="uom.group_uom"/>
<button name="action_show_details" type="object" icon="fa-list" width="0.1" title="Details"
attrs="{'invisible': [('show_details_visible', '=', False)]}" context="{'is_subcontracting_portal': 1}"/>
<field name="show_subcontracting_details_visible" invisible="1"/>
<button name="action_show_subcontract_details" string="Register components for subcontracted product" type="object" icon="fa-sitemap"
width="0.1" attrs="{'invisible': [('show_subcontracting_details_visible', '=', False)]}"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="subcontracting_portal_view_production_action" model="ir.actions.act_window">
<field name="name">Subcontracting Portal</field>
<field name="res_model">stock.picking</field>
<field name="view_mode">form</field>
<field name="view_id" ref="subcontracting_portal_production_form_view"/>
</record>
</odoo>
@@ -12,4 +12,8 @@
'installable': True,
'auto_install': True,
'license': 'LGPL-3',
'data': [
'security/mrp_subcontracting_account_security.xml',
'security/ir.model.access.csv',
],
}
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_subcontracting_portal_analytic_account,subcontracting.portal.analytic_account,mrp_account.model_account_analytic_account,base.group_portal,1,0,0,0
access_subcontracting_portal_analytic_line,subcontracting.portal.analytic_line,mrp_account.model_account_analytic_line,base.group_portal,1,1,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_subcontracting_portal_analytic_account subcontracting.portal.analytic_account mrp_account.model_account_analytic_account base.group_portal 1 0 0 0
3 access_subcontracting_portal_analytic_line subcontracting.portal.analytic_line mrp_account.model_account_analytic_line base.group_portal 1 1 0 0
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.rule" id="analytic_accout_subcontractor_rule">
<field name="name">Analytic Account Subcontractor</field>
<field name="model_id" ref="mrp_account.model_account_analytic_account"/>
<field name="domain_force">[('bom_ids', 'in', user.partner_id.commercial_partner_id.bom_ids.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="analytic_accout_line_subcontractor_rule">
<field name="name">Analytic Account Line Subcontractor</field>
<field name="model_id" ref="mrp_account.model_account_analytic_line"/>
<field name="domain_force">[('account_id.bom_ids', 'in', user.partner_id.commercial_partner_id.bom_ids.ids)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</odoo>
@@ -13,6 +13,9 @@
'views/purchase_order_views.xml',
'views/stock_picking_views.xml',
],
'demo': [
'data/mrp_subcontracting_purchase_demo.xml',
],
'installable': True,
'auto_install': True,
'license': 'LGPL-3',
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="purchase_order_subcontracting" model="purchase.order">
<field name="partner_id" ref="base.partner_demo_portal"/>
<field name="user_id" ref="base.user_admin"/>
<field name="state">purchase</field>
<field name="order_line" model="purchase.order.line" eval="[(5, 0, 0),
(0, 0, {
'product_id': ref('product.product_delivery_02'),
'price_unit': 10.0,
'name': 'Office Lamp',
'product_qty': 5.0,
'date_planned': time.strftime('%Y-%m-%d')}),
]"/>
</record>
</data>
</odoo>
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'MRP Subcontracting Repair',
'version': '1.0',
'category': 'Manufacturing/Repair',
'description': """
Bridge module between MRP subcontracting and Repair
""",
'depends': [
'mrp_subcontracting', 'repair'
],
'data': [
'security/ir.model.access.csv',
'security/mrp_subcontracting_repair_security.xml',
],
'installable': True,
'auto_install': True,
'license': 'OEEL-1',
}
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_subcontracting_portal_repair_line,subcontracting.portal.repair_line,repair.model_repair_line,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_subcontracting_portal_repair_line subcontracting.portal.repair_line repair.model_repair_line base.group_portal 1 0 0 0
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="repair_line_subcontracting_rule" model="ir.rule">
<field name="name">Repair Line Subcontractor</field>
<field name="model_id" ref="repair.model_repair_line"/>
<field name="domain_force">[
'|',
'|',
('product_id', 'in', user.partner_id.bom_ids.product_id.ids),
('product_id', 'in', user.partner_id.bom_ids.product_tmpl_id.product_variant_ids.ids),
('product_id', 'in', user.partner_id.bom_ids.bom_line_ids.product_id.ids),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</odoo>
+6 -6
View File
@@ -249,12 +249,12 @@ class ProductProduct(models.Model):
@api.depends_context('partner_id')
def _compute_product_code(self):
for product in self:
for supplier_info in product.seller_ids:
if supplier_info.partner_id.id == product._context.get('partner_id'):
product.code = supplier_info.product_code or product.default_code
break
else:
product.code = product.default_code
product.code = product.default_code
if self.env['ir.model.access'].check('product.supplierinfo', 'read', False):
for supplier_info in product.seller_ids:
if supplier_info.partner_id.id == product._context.get('partner_id'):
product.code = supplier_info.product_code or product.default_code
break
@api.depends_context('partner_id')
def _compute_partner_ref(self):
+3
View File
@@ -212,6 +212,9 @@
<record id="base.group_user" model="res.groups">
<field name="implied_ids" eval="[(4, ref('stock.group_production_lot'))]"/>
</record>
<record id="base.group_portal" model="res.groups">
<field name="implied_ids" eval="[(4, ref('stock.group_production_lot'))]"/>
</record>
</data>
</odoo>
+1 -1
View File
@@ -11,7 +11,7 @@ class ResConfigSettings(models.TransientModel):
module_product_expiry = fields.Boolean("Expiration Dates",
help="Track following dates on lots & serial numbers: best before, removal, end of life, alert. \n Such dates are set automatically at lot/serial number creation based on values set on the product (in days).")
group_stock_production_lot = fields.Boolean("Lots & Serial Numbers",
implied_group='stock.group_production_lot')
implied_group='stock.group_production_lot', group="base.group_user,base.group_portal")
group_stock_lot_print_gs1 = fields.Boolean("Print GS1 Barcodes for Lots & Serial Numbers",
implied_group='stock.group_stock_lot_print_gs1')
group_lot_on_delivery_slip = fields.Boolean("Display Lots & Serial Numbers on Delivery Slips",
+5 -1
View File
@@ -223,6 +223,7 @@ class Location(models.Model):
The quantity should be in the default UOM of the product, it is used when
no package is specified.
"""
self = self._check_access_putaway()
# find package type on package or packaging
package_type = self.env['stock.package.type']
if package:
@@ -235,7 +236,7 @@ class Location(models.Model):
categ = product.categ_id
while categ:
putaway_rules |= self.putaway_rule_ids.filtered(lambda x: x.category_id == categ and (package_type in x.package_type_ids or package_type == x.package_type_ids))
categ = categ.parent_id
categ = categ.sudo().parent_id
if package_type:
putaway_rules |= self.putaway_rule_ids.filtered(lambda x: not x.product_id and (package_type in x.package_type_ids or package_type == x.package_type_ids))
@@ -319,6 +320,9 @@ class Location(models.Model):
self.ensure_one()
return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location or (self.usage == 'transit' and not self.company_id)
def _check_access_putaway(self):
return self
def _check_can_be_used(self, product, quantity=0, package=None, location_qty=0):
"""Check if product/package can be stored in the location. Quantity
should in the default uom of product, it's only used when no package is
+1 -1
View File
@@ -174,7 +174,7 @@ class StockMoveLine(models.Model):
message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
else:
# check if in correct source location
message, recommended_location = self.env['stock.quant']._check_serial_number(self.product_id,
message, recommended_location = self.env['stock.quant'].sudo()._check_serial_number(self.product_id,
self.lot_id,
self.company_id,
self.location_id,
+3 -1
View File
@@ -320,7 +320,9 @@ class Picking(models.Model):
check_company=True, readonly=True, required=True,
states={'draft': [('readonly', False)]})
move_ids = fields.One2many('stock.move', 'picking_id', string="Stock Moves", copy=True)
move_ids_without_package = fields.One2many('stock.move', 'picking_id', string="Stock moves not in package", compute='_compute_move_without_package', inverse='_set_move_without_package')
move_ids_without_package = fields.One2many(
'stock.move', 'picking_id', string="Stock moves not in package", compute='_compute_move_without_package',
inverse='_set_move_without_package', compute_sudo=True)
has_scrap_move = fields.Boolean(
'Has Scrap Moves', compute='_has_scrap_move')
picking_type_id = fields.Many2one(