[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:
@@ -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']}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
from . import controllers
|
||||
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
@@ -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 > 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty < 0.0001">
|
||||
<tree delete="0" create="0" decoration-muted="is_done" decoration-warning="quantity_done - product_uom_qty > 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty < 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 < now" decoration-danger="state not in ('done', 'cancel') and scheduled_date < current_date" decoration-bf="state not in ('done', 'cancel') and (scheduled_date < current_date or scheduled_date < now)"/>
|
||||
<field name="date_deadline" attrs="{'invisible': ['|', ('state', 'in', ('done', 'cancel')), ('date_deadline', '=', False)]}" decoration-danger="date_deadline and date_deadline < current_date" decoration-bf="date_deadline and date_deadline < 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
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user