diff --git a/addons/mrp_subcontracting/__init__.py b/addons/mrp_subcontracting/__init__.py
index dfbd96001d5..880e67434db 100644
--- a/addons/mrp_subcontracting/__init__.py
+++ b/addons/mrp_subcontracting/__init__.py
@@ -13,6 +13,10 @@ def uninstall_hook(cr, registry):
warehouses = env["stock.warehouse"].search([])
subcontracting_routes = warehouses.mapped("subcontracting_route_id")
warehouses.write({"subcontracting_route_id": False})
+ companies = env["res.company"].search([])
+ subcontracting_locations = companies.mapped("subcontracting_location_id")
+ subcontracting_locations.active = False
+ companies.write({"subcontracting_location_id": False})
operations_type_to_remove = (warehouses.subcontracting_resupply_type_id | warehouses.subcontracting_type_id)
operations_type_to_remove.active = False
# Fail unlink means that the route is used somewhere (e.g. route_id on stock.rule). In this case
diff --git a/addons/mrp_subcontracting/__manifest__.py b/addons/mrp_subcontracting/__manifest__.py
index 3813dc5f5ab..c5ec547e89d 100644
--- a/addons/mrp_subcontracting/__manifest__.py
+++ b/addons/mrp_subcontracting/__manifest__.py
@@ -23,6 +23,7 @@
'views/mrp_production_views.xml',
'views/subcontracting_portal_views.xml',
'views/subcontracting_portal_templates.xml',
+ 'views/stock_location_views.xml',
'wizard/stock_picking_return_views.xml',
'report/mrp_report_bom_structure.xml',
],
diff --git a/addons/mrp_subcontracting/models/res_company.py b/addons/mrp_subcontracting/models/res_company.py
index 583d2413916..58eb602fc9c 100644
--- a/addons/mrp_subcontracting/models/res_company.py
+++ b/addons/mrp_subcontracting/models/res_company.py
@@ -27,6 +27,7 @@ class ResCompany(models.Model):
'usage': 'internal',
'location_id': parent_location.id,
'company_id': company.id,
+ 'is_subcontracting_location': True,
})
self.env['ir.property']._set_default(
"property_stock_subcontractor",
diff --git a/addons/mrp_subcontracting/models/res_partner.py b/addons/mrp_subcontracting/models/res_partner.py
index e2aa3f1278f..7c46acb5072 100644
--- a/addons/mrp_subcontracting/models/res_partner.py
+++ b/addons/mrp_subcontracting/models/res_partner.py
@@ -9,6 +9,7 @@ class ResPartner(models.Model):
property_stock_subcontractor = fields.Many2one(
'stock.location', string="Subcontractor Location", company_dependent=True,
+ domain="[('is_subcontracting_location', '=', True)]",
help="The stock location used as source and destination when sending\
goods to this contact during a subcontracting process.")
is_subcontractor = fields.Boolean(
diff --git a/addons/mrp_subcontracting/models/stock_location.py b/addons/mrp_subcontracting/models/stock_location.py
index c012885b9a0..f1dac53a052 100644
--- a/addons/mrp_subcontracting/models/stock_location.py
+++ b/addons/mrp_subcontracting/models/stock_location.py
@@ -1,15 +1,107 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import models
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
class StockLocation(models.Model):
_inherit = 'stock.location'
+ is_subcontracting_location = fields.Boolean(
+ "Is a Subcontracting Location?",
+ help="Check this box to create a new dedicated subcontracting location for this company. Note that standard subcontracting routes will be adapted so as to take these into account automatically."
+ )
+
+ @api.constrains('is_subcontracting_location', 'usage', 'location_id')
+ def _check_subcontracting_location(self):
+ for location in self:
+ if location == location.company_id.subcontracting_location_id:
+ raise ValidationError(_("You cannot alter the company's subcontracting location"))
+ if location.is_subcontracting_location and (location.usage != 'internal' or location.warehouse_id):
+ raise ValidationError(_("In order to manage stock accurately, subcontracting locations must be type Internal, linked to the appropriate company and not specific to a warehouse."))
+
+ @api.constrains('is_subcontracting_location')
+ def _check_is_subcontracting_location(self):
+ for location in self:
+ if not location.is_subcontracting_location and self.env['res.partner'].search([('property_stock_subcontractor', '=', location.id)]):
+ raise ValidationError(_("You cannot change the subcontracting location as it is still linked to a subcontractor partner"))
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super().create(vals_list)
+ new_subcontracting_locations = res.filtered(lambda l: l.is_subcontracting_location)
+ new_subcontracting_locations._activate_subcontracting_location_rules()
+ return res
+
+ def write(self, values):
+ res = super().write(values)
+ if 'is_subcontracting_location' in values:
+ if values['is_subcontracting_location']:
+ self._activate_subcontracting_location_rules()
+ else:
+ self._archive_subcontracting_location_rules()
+ return res
+
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()
+
+ def _activate_subcontracting_location_rules(self):
+ """ Create or unarchive rules for the 'custom' subcontracting location(s).
+ The subcontracting location defined on the company is considered as the 'reference' one.
+ All rules defined on this 'reference' location will be replicated on 'custom' subcontracting locations.
+ """
+ locations_per_company = {}
+ for location in self:
+ if location.is_subcontracting_location and location != location.company_id.subcontracting_location_id:
+ locations_per_company.setdefault(location.company_id, []).extend(location)
+ new_rules_vals = []
+ rules_to_unarchive = self.env['stock.rule']
+ for company, locations in locations_per_company.items():
+ reference_location_id = company.subcontracting_location_id
+ if reference_location_id:
+ reference_rules_from = self.env['stock.rule'].search([('location_src_id', '=', reference_location_id.id)])
+ reference_rules_to = self.env['stock.rule'].search([('location_dest_id', '=', reference_location_id.id)])
+ for location in locations:
+ existing_rules = {
+ (rule.route_id, rule.picking_type_id, rule.action, rule.location_src_id): rule
+ for rule in self.env['stock.rule'].with_context(active_test=False).search([('location_src_id', '=', location.id)])
+ }
+ for rule in reference_rules_from:
+ if (rule.route_id, rule.picking_type_id, rule.action, location) not in existing_rules:
+ new_rules_vals.append(rule.copy_data({
+ 'location_src_id': location.id,
+ 'name': rule.name.replace(reference_location_id.name, location.name)
+ })[0])
+ else:
+ existing_rule = existing_rules[(rule.route_id, rule.picking_type_id, rule.action, location)]
+ if not existing_rule.active:
+ rules_to_unarchive += existing_rule
+ existing_rules = {
+ (rule.route_id, rule.picking_type_id, rule.action, rule.location_dest_id): rule
+ for rule in self.env['stock.rule'].with_context(active_test=False).search([('location_dest_id', '=', location.id)])
+ }
+ for rule in reference_rules_to:
+ if (rule.route_id, rule.picking_type_id, rule.action, location) not in existing_rules:
+ new_rules_vals.append(rule.copy_data({
+ 'location_dest_id': location.id,
+ 'name': rule.name.replace(reference_location_id.name, location.name)
+ })[0])
+ else:
+ existing_rule = existing_rules[(rule.route_id, rule.picking_type_id, rule.action, location)]
+ if not existing_rule.active:
+ rules_to_unarchive += existing_rule
+ self.env['stock.rule'].create(new_rules_vals)
+ rules_to_unarchive.action_unarchive()
+
+ def _archive_subcontracting_location_rules(self):
+ """ Archive subcontracting rules for locations that are no longer 'custom' subcontracting locations."""
+ reference_location_ids = self.company_id.subcontracting_location_id
+ reference_rules = self.env['stock.rule'].search(['|', ('location_src_id', 'in', reference_location_ids.ids), ('location_dest_id', 'in', reference_location_ids.ids)])
+ reference_routes = reference_rules.route_id
+ rules_to_archive = self.env['stock.rule'].search(['&', ('route_id', 'in', reference_routes.ids), '|', ('location_src_id', 'in', self.ids), ('location_dest_id', 'in', self.ids)])
+ rules_to_archive.action_archive()
diff --git a/addons/mrp_subcontracting/models/stock_move_line.py b/addons/mrp_subcontracting/models/stock_move_line.py
index 91def171cf0..9042796d0e3 100644
--- a/addons/mrp_subcontracting/models/stock_move_line.py
+++ b/addons/mrp_subcontracting/models/stock_move_line.py
@@ -12,7 +12,7 @@ class StockMoveLine(models.Model):
def _onchange_serial_number(self):
current_location_id = self.location_id
res = super()._onchange_serial_number()
- if res and not self.lot_name and self.company_id.subcontracting_location_id == current_location_id:
+ if res and not self.lot_name and current_location_id.is_subcontracting_location:
# we want to avoid auto-updating source location in this case + change the warning message
self.location_id = current_location_id
res['warning']['message'] = res['warning']['message'].split("\n\n", 1)[0] + "\n\n" + \
diff --git a/addons/mrp_subcontracting/models/stock_quant.py b/addons/mrp_subcontracting/models/stock_quant.py
index c2b48b2c7fa..05ed58c21bf 100644
--- a/addons/mrp_subcontracting/models/stock_quant.py
+++ b/addons/mrp_subcontracting/models/stock_quant.py
@@ -14,7 +14,7 @@ class StockQuant(models.Model):
if operator not in ['=', '!='] or not isinstance(value, bool):
raise UserError(_('Operation not supported'))
- subcontract_locations = self.env['res.company'].search([]).subcontracting_location_id
+ subcontract_locations = self.env['stock.location'].search([('is_subcontracting_location', '=', 'True')])
quant_ids = self.env['stock.quant'].search([
('location_id', 'in', subcontract_locations.ids),
diff --git a/addons/mrp_subcontracting/models/stock_warehouse.py b/addons/mrp_subcontracting/models/stock_warehouse.py
index e7477271257..3611636f8dc 100644
--- a/addons/mrp_subcontracting/models/stock_warehouse.py
+++ b/addons/mrp_subcontracting/models/stock_warehouse.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import fields, models, _
+from odoo import api, fields, models, _
class StockWarehouse(models.Model):
@@ -24,6 +24,12 @@ class StockWarehouse(models.Model):
'stock.picking.type', 'Subcontracting Resupply Operation Type',
domain=[('code', '=', 'outgoing')])
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super().create(vals_list)
+ res._update_subcontracting_locations_rules()
+ return res
+
def get_rules_dict(self):
result = super(StockWarehouse, self).get_rules_dict()
subcontract_location_id = self._get_subcontracting_location()
@@ -90,7 +96,7 @@ class StockWarehouse(models.Model):
'auto': 'manual',
'route_id': self._find_global_route('mrp_subcontracting.route_resupply_subcontractor_mto',
_('Resupply Subcontractor on Order')).id,
- 'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, False),
+ 'name': self._format_rulename(subcontract_location_id, production_location_id, False),
'location_dest_id': production_location_id.id,
'location_src_id': subcontract_location_id.id,
'picking_type_id': self.subcontracting_resupply_type_id.id
@@ -129,16 +135,17 @@ class StockWarehouse(models.Model):
def _get_sequence_values(self):
values = super(StockWarehouse, self)._get_sequence_values()
+ count = self.env['ir.sequence'].search_count([('prefix', 'like', self.code + '/SBC%/%')])
values.update({
'subcontracting_type_id': {
'name': self.name + ' ' + _('Sequence subcontracting'),
- 'prefix': self.code + '/SBC/',
+ 'prefix': self.code + (('/SBC' + str(count) + '/') if count else '/SBC/'),
'padding': 5,
'company_id': self.company_id.id
},
'subcontracting_resupply_type_id': {
'name': self.name + ' ' + _('Sequence Resupply Subcontractor'),
- 'prefix': self.code + '/RES/',
+ 'prefix': self.code + (('/RES' + str(count) + '/') if count else '/RES/'),
'padding': 5,
'company_id': self.company_id.id
},
@@ -165,3 +172,10 @@ class StockWarehouse(models.Model):
def _get_subcontracting_location(self):
return self.company_id.subcontracting_location_id
+
+ def _update_subcontracting_locations_rules(self):
+ subcontracting_locations = self.env['stock.location'].search([
+ ('company_id', 'in', self.company_id.ids),
+ ('is_subcontracting_location', '=', 'True'),
+ ])
+ subcontracting_locations._activate_subcontracting_location_rules()
diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py
index 657223c81e8..7ab0ff6c24a 100644
--- a/addons/mrp_subcontracting/tests/test_subcontracting.py
+++ b/addons/mrp_subcontracting/tests/test_subcontracting.py
@@ -22,6 +22,7 @@ class TestSubcontractingBasic(TransactionCase):
self.assertTrue(self.env.company.subcontracting_location_id != company2.subcontracting_location_id)
+@tagged('post_install', '-at_install')
class TestSubcontractingFlows(TestMrpSubcontractingCommon):
def test_flow_1(self):
""" Don't tick any route on the components and trigger the creation of the subcontracting
@@ -98,22 +99,18 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
# Tick "resupply subconractor on order"
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
(self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
- # Create a different subcontract location
+ # Create a different subcontract location & check rules replication
+ reference_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', self.env.company.subcontracting_location_id.id), ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)])
partner_subcontract_location = self.env['stock.location'].create({
'name': 'Specific partner location',
'location_id': self.env.ref('stock.stock_location_locations_partner').id,
'usage': 'internal',
'company_id': self.env.company.id,
+ 'is_subcontracting_location': True,
})
+ custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', partner_subcontract_location.id), ('location_dest_id', '=', partner_subcontract_location.id)])
+ self.assertEqual(reference_location_rules_count, custom_location_rules_count)
self.subcontractor_partner1.property_stock_subcontractor = partner_subcontract_location.id
- resupply_rule = resupply_sub_on_order_route.rule_ids.filtered(lambda r:
- r.location_dest_id == self.comp1.property_stock_production and
- r.location_src_id == self.env.company.subcontracting_location_id)
- resupply_rule.copy({'location_src_id': partner_subcontract_location.id})
- resupply_warehouse_rule = self.warehouse.route_ids.rule_ids.filtered(lambda r:
- r.location_dest_id == self.env.company.subcontracting_location_id and
- r.location_src_id == self.warehouse.lot_stock_id)
- resupply_warehouse_rule.copy({'location_dest_id': partner_subcontract_location.id})
# Add a manufacturing lead time to check that the resupply delivery is correctly planned 2 days
# before the subcontracting receipt
self.finished.produce_delay = 2
@@ -177,7 +174,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
"""
# Tick "resupply subconractor on order"
resupply_sub_on_order_route = self.env['stock.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
- (self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
+ (self.comp1 + self.comp2).write({'route_ids': [(6, None, [resupply_sub_on_order_route.id])]})
# Tick "manufacture" and MTO on self.comp2
mto_route = self.env.ref('stock.route_warehouse0_mto')
@@ -243,8 +240,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
mto_route = self.env.ref('stock.route_warehouse0_mto')
mto_route.active = True
manufacture_route = self.env['stock.route'].search([('name', '=', 'Manufacture')])
- self.comp2.write({'route_ids': [(4, manufacture_route.id, None)]})
- self.comp2.write({'route_ids': [(4, mto_route.id, None)]})
+ self.comp2.write({'route_ids': [(6, None, [manufacture_route.id, mto_route.id])]})
orderpoint_form = Form(self.env['stock.warehouse.orderpoint'])
orderpoint_form.product_id = self.comp2
@@ -667,7 +663,35 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
self.assertEqual(self.env['mrp.production'].search_count([('bom_id', '=', bom.id)]), 3)
+ def test_subcontracting_rules_replication(self):
+ """ Test activate/archive subcontracting location rules."""
+ reference_location_rules = self.env['stock.rule'].search(['|', ('location_src_id', '=', self.env.company.subcontracting_location_id.id), ('location_dest_id', '=', self.env.company.subcontracting_location_id.id)])
+ warehouse_related_rules = reference_location_rules.filtered(lambda r: r.warehouse_id)
+ company_rules = reference_location_rules - warehouse_related_rules
+ # Create a custom subcontracting location
+ custom_subcontracting_location = self.env['stock.location'].create({
+ 'name': 'Custom Subcontracting Location',
+ 'location_id': self.env.ref('stock.stock_location_locations').id,
+ 'usage': 'internal',
+ 'company_id': self.env.company.id,
+ 'is_subcontracting_location': True,
+ })
+ custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', custom_subcontracting_location.id), ('location_dest_id', '=', custom_subcontracting_location.id)])
+ self.assertEqual(len(reference_location_rules), custom_location_rules_count)
+ # Add a new warehouse
+ warehouse = self.env['stock.warehouse'].create({
+ 'name': 'Additional Warehouse',
+ 'code': 'ADD'
+ })
+ company_subcontracting_locations_rules_count = self.env['stock.rule'].search_count(['&', ('company_id', '=', warehouse.company_id.id), '|', ('location_src_id.is_subcontracting_location', '=', 'True'), ('location_dest_id.is_subcontracting_location', '=', 'True')])
+ self.assertEqual(len(warehouse_related_rules) * 4 + len(company_rules) * 2, company_subcontracting_locations_rules_count)
+ # Custom location no longer a subcontracting one
+ custom_subcontracting_location.is_subcontracting_location = False
+ custom_location_rules_count = self.env['stock.rule'].search_count(['|', ('location_src_id', '=', custom_subcontracting_location.id), ('location_dest_id', '=', custom_subcontracting_location.id)])
+ self.assertEqual(custom_location_rules_count, 0)
+
+@tagged('post_install', '-at_install')
class TestSubcontractingTracking(TransactionCase):
@classmethod
@@ -1056,6 +1080,7 @@ class TestSubcontractingTracking(TransactionCase):
self.assertEqual(picking_receipt.state, 'done')
+@tagged('post_install', '-at_install')
class TestSubcontractingPortal(TransactionCase):
@classmethod
diff --git a/addons/mrp_subcontracting/views/res_partner_views.xml b/addons/mrp_subcontracting/views/res_partner_views.xml
index ee3c6ba6c7d..c218a4db95f 100644
--- a/addons/mrp_subcontracting/views/res_partner_views.xml
+++ b/addons/mrp_subcontracting/views/res_partner_views.xml
@@ -6,7 +6,7 @@
-
+
diff --git a/addons/mrp_subcontracting/views/stock_location_views.xml b/addons/mrp_subcontracting/views/stock_location_views.xml
new file mode 100644
index 00000000000..f04b8c06464
--- /dev/null
+++ b/addons/mrp_subcontracting/views/stock_location_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ stock.location.form
+ stock.location
+
+
+
+
+
+
+
+
diff --git a/addons/mrp_subcontracting_dropshipping/models/stock_rule.py b/addons/mrp_subcontracting_dropshipping/models/stock_rule.py
index 21090a7ee88..906b60f73f8 100644
--- a/addons/mrp_subcontracting_dropshipping/models/stock_rule.py
+++ b/addons/mrp_subcontracting_dropshipping/models/stock_rule.py
@@ -8,6 +8,6 @@ class StockRule(models.Model):
_inherit = 'stock.rule'
def _prepare_purchase_order(self, company_id, origins, values):
- if 'partner_id' not in values[0] and company_id.subcontracting_location_id.parent_path in self.location_dest_id.parent_path:
+ if 'partner_id' not in values[0] and self.location_dest_id.is_subcontracting_location:
values[0]['partner_id'] = values[0]['group_id'].partner_id.id
return super()._prepare_purchase_order(company_id, origins, values)
diff --git a/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py b/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py
index eeb3d9c68c9..23cf0958cff 100644
--- a/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py
+++ b/addons/mrp_subcontracting_dropshipping/tests/test_purchase_subcontracting.py
@@ -147,11 +147,10 @@ class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon):
sub_location = self.env['stock.location'].create({
'name': 'Super Location',
'location_id': subcontract_location.id,
+ 'is_subcontracting_location': True,
})
dropship_subcontractor_route = self.env['stock.route'].search([('name', '=', 'Dropship Subcontractor on Order')])
- dropship_subcontractor_route.rule_ids.filtered(lambda rule: rule.location_dest_id == subcontract_location).copy(default={'location_dest_id': sub_location.id})
- dropship_subcontractor_route.rule_ids.filtered(lambda rule: rule.location_src_id == subcontract_location).copy(default={'location_src_id': sub_location.id})
subcontractor, vendor = self.env['res.partner'].create([
{'name': 'SuperSubcontractor', 'property_stock_subcontractor': sub_location.id},