Initial commit: Mrp packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 50d736b3bd
739 changed files with 538193 additions and 0 deletions

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
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
from . import stock_quant
from . import stock_rule
from . import stock_warehouse
from . import mrp_production

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.osv.expression import AND
class MrpBom(models.Model):
_inherit = 'mrp.bom'
type = fields.Selection(selection_add=[
('subcontract', 'Subcontracting')
], ondelete={'subcontract': lambda recs: recs.write({'type': 'normal', 'active': False})})
subcontractor_ids = fields.Many2many('res.partner', 'mrp_bom_subcontractor', string='Subcontractors', check_company=True)
def _bom_subcontract_find(self, product, picking_type=None, company_id=False, bom_type='subcontract', subcontractor=False):
domain = self._bom_find_domain(product, picking_type=picking_type, company_id=company_id, bom_type=bom_type)
if subcontractor:
domain = AND([domain, [('subcontractor_ids', 'parent_of', subcontractor.ids)]])
return self.search(domain, order='sequence, product_id, id', limit=1)
else:
return self.env['mrp.bom']
@api.constrains('operation_ids', 'byproduct_ids', 'type')
def _check_subcontracting_no_operation(self):
if self.filtered_domain([('type', '=', 'subcontract'), '|', ('operation_ids', '!=', False), ('byproduct_ids', '!=', False)]):
raise ValidationError(_('You can not set a Bill of Material with operations or by-product line as subcontracting.'))

View file

@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import fields, models, _, api
from odoo.exceptions import UserError, ValidationError, AccessError
from odoo.tools.float_utils import float_compare, float_is_zero
class MrpProduction(models.Model):
_inherit = 'mrp.production'
_rec_names_search = ['name', 'incoming_picking.name']
move_line_raw_ids = fields.One2many(
'stock.move.line', string="Detail Component", readonly=False,
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')
@api.depends('name')
def name_get(self):
return [
(record.id, "%s (%s)" % (record.incoming_picking.name, record.name)) if record.bom_id.type == 'subcontract'
else (record.id, record.name) for record in self
]
@api.depends('move_raw_ids.move_line_ids')
def _compute_move_line_raw_ids(self):
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'])
for line in production.move_line_raw_ids:
line_by_product[line.product_id] |= line
for move in production.move_raw_ids:
move.move_line_ids = line_by_product.pop(move.product_id, self.env['stock.move.line'])
for product_id, lines in line_by_product.items():
qty = sum(line.product_uom_id._compute_quantity(line.qty_done, product_id.uom_id) for line in lines)
move = production._get_move_raw_values(product_id, qty, product_id.uom_id)
move['additional'] = True
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."))
return super().action_merge()
def subcontracting_record_component(self):
self.ensure_one()
if not self._get_subcontract_move():
raise UserError(_("This MO isn't related to a subcontracted move"))
if float_is_zero(self.qty_producing, precision_rounding=self.product_uom_id.rounding):
return {'type': 'ir.actions.act_window_close'}
if self.move_raw_ids and not any(self.move_raw_ids.mapped('quantity_done')):
raise UserError(_("You must indicate a non-zero amount consumed for at least one of your components"))
consumption_issues = self._get_consumption_issues()
if consumption_issues:
return self._action_generate_consumption_wizard(consumption_issues)
self._update_finished_move()
self.subcontracting_has_been_recorded = True
quantity_issues = self._get_quantity_produced_issues()
if quantity_issues:
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
backorder.qty_producing = backorder.product_qty
backorder._set_qty_producing()
self.product_qty = self.qty_producing
action = self._get_subcontract_move().filtered(lambda m: m.state not in ('done', 'cancel'))._action_record_components()
action['res_id'] = backorder.id
return action
return {'type': 'ir.actions.act_window_close'}
def _pre_button_mark_done(self):
if self._get_subcontract_move():
return True
return super()._pre_button_mark_done()
def _should_postpone_date_finished(self, date_planned_finished):
return super()._should_postpone_date_finished(date_planned_finished) and not self._get_subcontract_move()
def _update_finished_move(self):
""" After producing, set the move line on the subcontract picking. """
self.ensure_one()
subcontract_move_id = self._get_subcontract_move().filtered(lambda m: m.state not in ('done', 'cancel'))
if subcontract_move_id:
quantity = self.qty_producing
if self.lot_producing_id:
move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_producing_id or not ml.lot_id)
else:
move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: not ml.lot_id)
# Update reservation and quantity done
for ml in move_lines:
rounding = ml.product_uom_id.rounding
if float_compare(quantity, 0, precision_rounding=rounding) <= 0:
break
quantity_to_process = min(quantity, ml.reserved_uom_qty - ml.qty_done)
quantity -= quantity_to_process
new_quantity_done = (ml.qty_done + quantity_to_process)
# on which lot of finished product
if float_compare(new_quantity_done, ml.reserved_uom_qty, precision_rounding=rounding) >= 0:
ml.write({
'qty_done': new_quantity_done,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
})
else:
new_qty_reserved = ml.reserved_uom_qty - new_quantity_done
default = {
'reserved_uom_qty': new_quantity_done,
'qty_done': new_quantity_done,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
}
ml.copy(default=default)
ml.with_context(bypass_reservation_update=True).write({
'reserved_uom_qty': new_qty_reserved,
'qty_done': 0
})
if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) > 0:
self.env['stock.move.line'].create({
'move_id': subcontract_move_id.id,
'picking_id': subcontract_move_id.picking_id.id,
'product_id': self.product_id.id,
'location_id': subcontract_move_id.location_id.id,
'location_dest_id': subcontract_move_id.location_dest_id.id,
'reserved_uom_qty': 0,
'product_uom_id': self.product_uom_id.id,
'qty_done': quantity,
'lot_id': self.lot_producing_id and self.lot_producing_id.id,
})
if not self._get_quantity_to_backorder():
ml_reserved = subcontract_move_id.move_line_ids.filtered(lambda ml:
float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and
not float_is_zero(ml.reserved_uom_qty, precision_rounding=ml.product_uom_id.rounding))
ml_reserved.unlink()
for ml in subcontract_move_id.move_line_ids:
ml.reserved_uom_qty = ml.qty_done
subcontract_move_id._recompute_state()
def _subcontracting_filter_to_done(self):
""" Filter subcontracting production where composant is already recorded and should be consider to be validate """
def filter_in(mo):
if mo.state in ('done', 'cancel'):
return False
if not mo.subcontracting_has_been_recorded:
return False
return True
return self.filtered(filter_in)
def _has_been_recorded(self):
self.ensure_one()
if self.state in ('cancel', 'done'):
return True
return self.subcontracting_has_been_recorded
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']
def _subcontract_sanity_check(self):
for production in self:
if production.product_tracking != 'none' and not self.lot_producing_id:
raise UserError(_('You must enter a serial number for %s') % production.product_id.name)
for sml in production.move_raw_ids.move_line_ids:
if sml.tracking != 'none' and not sml.lot_id:
raise UserError(_('You must enter a serial number for each line of %s') % sml.product_id.display_name)
return True

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class SupplierInfo(models.Model):
_inherit = 'product.supplierinfo'
is_subcontractor = fields.Boolean('Subcontracted', compute='_compute_is_subcontractor', help="Choose a vendor of type subcontractor if you want to subcontract the product")
@api.depends('partner_id', 'product_id', 'product_tmpl_id')
def _compute_is_subcontractor(self):
for supplier in self:
boms = supplier.product_id.variant_bom_ids
boms |= supplier.product_tmpl_id.bom_ids.filtered(lambda b: not b.product_id or b.product_id in (supplier.product_id or supplier.product_tmpl_id.product_variant_ids))
supplier.is_subcontractor = supplier.partner_id in boms.subcontractor_ids
class ProductProduct(models.Model):
_inherit = 'product.product'
def _prepare_sellers(self, params=False):
if params and params.get('subcontractor_ids'):
return super()._prepare_sellers(params=params).filtered(lambda s: s.partner_id in params.get('subcontractor_ids'))
return super()._prepare_sellers(params=params)

View file

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class ResCompany(models.Model):
_inherit = 'res.company'
subcontracting_location_id = fields.Many2one('stock.location')
@api.model
def _create_missing_subcontracting_location(self):
company_without_subcontracting_loc = self.env['res.company'].with_context(active_test=False).search(
[('subcontracting_location_id', '=', False)])
company_without_subcontracting_loc._create_subcontracting_location()
def _create_per_company_locations(self):
super(ResCompany, self)._create_per_company_locations()
self._create_subcontracting_location()
def _create_subcontracting_location(self):
parent_location = self.env.ref('stock.stock_location_locations', raise_if_not_found=False)
for company in self:
subcontracting_location = self.env['stock.location'].create({
'name': _('Subcontracting Location'),
'usage': 'internal',
'location_id': parent_location.id,
'company_id': company.id,
'is_subcontracting_location': True,
})
self.env['ir.property']._set_default(
"property_stock_subcontractor",
"res.partner",
subcontracting_location,
company,
)
company.subcontracting_location_id = subcontracting_location

View file

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
property_stock_subcontractor = fields.Many2one(
'stock.location', string="Subcontractor Location", company_dependent=True,
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", 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'
subcontractor_ids = self.env['mrp.bom'].search(
[('type', '=', 'subcontract')]).subcontractor_ids.ids
if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
search_operator = 'in'
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),
]))

View file

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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."
)
subcontractor_ids = fields.One2many('res.partner', 'property_stock_subcontractor')
@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':
raise ValidationError(_("In order to manage stock accurately, subcontracting locations must be type Internal, linked to the appropriate company."))
@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()

View file

@ -0,0 +1,360 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import fields, models, api, _
from odoo.exceptions import UserError, AccessError
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
from odoo.tools.misc import OrderedSet
class StockMove(models.Model):
_inherit = 'stock.move'
is_subcontract = fields.Boolean('The move is a subcontract receipt')
show_subcontracting_details_visible = fields.Boolean(
compute='_compute_show_subcontracting_details_visible'
)
def _compute_display_assign_serial(self):
super(StockMove, self)._compute_display_assign_serial()
for move in self:
if not move.is_subcontract:
continue
productions = move._get_subcontract_production()
if not productions or move.has_tracking != 'serial':
continue
if productions._has_tracked_component() or productions[:1].consumption != 'strict':
move.display_assign_serial = False
def _compute_show_subcontracting_details_visible(self):
""" Compute if the action button in order to see moves raw is visible """
self.show_subcontracting_details_visible = False
for move in self:
if not move.is_subcontract:
continue
if float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding):
continue
productions = move._get_subcontract_production()
if not productions or (productions[:1].consumption == 'strict' and not productions[:1]._has_tracked_component()):
continue
move.show_subcontracting_details_visible = True
def _compute_show_details_visible(self):
""" If the move is subcontract and the components are tracked. Then the
show details button is visible.
"""
res = super(StockMove, self)._compute_show_details_visible()
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
productions = move._get_subcontract_production()
if not productions._has_tracked_component() and productions[:1].consumption == 'strict':
continue
move.show_details_visible = True
return res
def _set_quantity_done(self, qty):
to_set_moves = self
for move in self:
if move.is_subcontract and move._subcontracting_possible_record():
# If 'done' quantity is changed through the move, record components as if done through the wizard.
move._auto_record_components(qty)
to_set_moves -= move
if to_set_moves:
super(StockMove, to_set_moves)._set_quantity_done(qty)
def _quantity_done_set(self):
to_set_moves = self
for move in self:
if move.is_subcontract and move._subcontracting_possible_record():
delta_qty = move.quantity_done - move._quantity_done_sml()
if float_compare(delta_qty, 0, precision_rounding=move.product_uom.rounding) > 0:
move._auto_record_components(delta_qty)
to_set_moves -= move
elif float_compare(delta_qty, 0, precision_rounding=move.product_uom.rounding) < 0 and not move.picking_id.immediate_transfer:
move.with_context(transfer_qty=True)._reduce_subcontract_order_qty(abs(delta_qty))
if to_set_moves:
super(StockMove, to_set_moves)._quantity_done_set()
def _auto_record_components(self, qty):
self.ensure_one()
subcontracted_productions = self._get_subcontract_production()
production = subcontracted_productions.filtered(lambda p: not p._has_been_recorded())[-1:]
if not production:
# If new quantity is over the already recorded quantity and we have no open production, then create a new one for the missing quantity.
production = subcontracted_productions[-1:]
production = production.sudo().with_context(allow_more=True)._split_productions({production: [production.qty_producing, qty]})[-1:]
qty = self.product_uom._compute_quantity(qty, production.product_uom_id)
if production.product_tracking == 'serial':
qty = float_round(qty, precision_digits=0, rounding_method='UP') # Makes no sense to have partial quantities for serial number
if float_compare(qty, production.product_qty, precision_rounding=production.product_uom_id.rounding) < 0:
remaining_qty = production.product_qty - qty
productions = production.sudo()._split_productions({production: ([1] * int(qty)) + [remaining_qty]})[:-1]
else:
productions = production.sudo().with_context(allow_more=True)._split_productions({production: ([1] * int(qty))})
for production in productions:
production.qty_producing = 1
if not production.lot_producing_id:
production.action_generate_serial()
production.with_context(cancel_backorder=False).subcontracting_record_component()
else:
production.qty_producing = qty
if float_compare(production.qty_producing, production.product_qty, precision_rounding=production.product_uom_id.rounding) > 0:
self.env['change.production.qty'].with_context(skip_activity=True).create({
'mo_id': production.id,
'product_qty': qty
}).change_prod_qty()
if production.product_tracking == 'lot' and not production.lot_producing_id:
production.action_generate_serial()
production._set_qty_producing()
production.with_context(cancel_backorder=False).subcontracting_record_component()
def copy(self, default=None):
self.ensure_one()
if not self.is_subcontract or 'location_id' in default:
return super(StockMove, self).copy(default=default)
if not default:
default = {}
default['location_id'] = self.picking_id.location_id.id
return super(StockMove, self).copy(default=default)
def write(self, values):
""" 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 and not self._context.get('extra_move_mode'):
self.filtered(
lambda m: m.is_subcontract and m.state not in ['draft', 'cancel', 'done']
and float_compare(m.product_uom_qty, values['product_uom_qty'], precision_rounding=m.product_uom.rounding) != 0
)._update_subcontract_order_qty(values['product_uom_qty'])
res = super().write(values)
if 'date' in values:
for move in self:
if move.state in ('done', 'cancel') or not move.is_subcontract:
continue
move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel')).write({
'date_planned_finished': move.date,
'date_planned_start': move.date,
})
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.
"""
self.ensure_one()
if self.state != 'done' and (self._subcontrating_should_be_record() or self._subcontrating_can_be_record()):
return self._action_record_components()
action = super(StockMove, self).action_show_details()
if self.is_subcontract and all(p._has_been_recorded() for p in self._get_subcontract_production()):
action['views'] = [(self.env.ref('stock.view_stock_move_operations').id, 'form')]
action['context'].update({
'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):
""" Display moves raw for subcontracted product self. """
moves = self._get_subcontract_production().move_raw_ids.filtered(lambda m: m.state != 'cancel')
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',
'res_model': 'stock.move',
'views': [(tree_view.id, 'list'), (form_view.id, 'form')],
'target': 'current',
'domain': [('id', 'in', moves.ids)],
'context': ctx
}
def _set_quantities_to_reservation(self):
move_untouchable = self.filtered(lambda m: m.is_subcontract and m._get_subcontract_production()._has_tracked_component())
return super(StockMove, self - move_untouchable)._set_quantities_to_reservation()
def _action_cancel(self):
productions_to_cancel_ids = OrderedSet()
for move in self:
if move.is_subcontract:
active_productions = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))
moves_todo = self.env.context.get('moves_todo')
not_todo_productions = active_productions.filtered(lambda p: p not in moves_todo.move_orig_ids.production_id) if moves_todo else active_productions
if not_todo_productions:
productions_to_cancel_ids.update(not_todo_productions.ids)
if productions_to_cancel_ids:
productions_to_cancel = self.env['mrp.production'].browse(productions_to_cancel_ids)
productions_to_cancel.with_context(skip_activity=True).action_cancel()
return super()._action_cancel()
def _action_confirm(self, merge=True, merge_into=False):
subcontract_details_per_picking = defaultdict(list)
for move in self:
if move.location_id.usage != 'supplier' or move.location_dest_id.usage == 'supplier':
continue
if move.move_orig_ids.production_id:
continue
bom = move._get_subcontract_bom()
if not bom:
continue
if float_is_zero(move.product_qty, precision_rounding=move.product_uom.rounding) and\
move.picking_id.immediate_transfer is True:
raise UserError(_("To subcontract, use a planned transfer."))
move.write({
'is_subcontract': True,
'location_id': move.picking_id.partner_id.with_company(move.company_id).property_stock_subcontractor.id
})
if float_compare(move.product_qty, 0, precision_rounding=move.product_uom.rounding) <= 0:
# If a subcontracted amount is decreased, don't create a MO that would be for a negative value.
# We don't care if the MO decreases even when done since everything is handled through picking
continue
res = super()._action_confirm(merge=merge, merge_into=merge_into)
for move in res:
if move.is_subcontract:
subcontract_details_per_picking[move.picking_id].append((move, move._get_subcontract_bom()))
for picking, subcontract_details in subcontract_details_per_picking.items():
picking._subcontracted_produce(subcontract_details)
if subcontract_details_per_picking:
self.env['stock.picking'].concat(*list(subcontract_details_per_picking.keys())).action_assign()
return res
def _action_record_components(self):
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')
context = dict(self._context)
context.pop('skip_consumption', False)
return {
'name': _('Subcontract'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mrp.production',
'views': [(view.id, 'form')],
'view_id': view.id,
'target': 'new',
'res_id': production.id,
'context': context,
}
def _get_subcontract_bom(self):
self.ensure_one()
bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
self.product_id,
picking_type=self.picking_type_id,
company_id=self.company_id.id,
bom_type='subcontract',
subcontractor=self.picking_id.partner_id,
)
return bom
def _subcontrating_should_be_record(self):
return self._get_subcontract_production().filtered(lambda p: not p._has_been_recorded() and p._has_tracked_component())
def _subcontrating_can_be_record(self):
return self._get_subcontract_production().filtered(lambda p: not p._has_been_recorded() and p.consumption != 'strict')
def _subcontracting_possible_record(self):
return self._get_subcontract_production().filtered(lambda p: p._has_tracked_component() or p.consumption != 'strict')
def _get_subcontract_production(self):
return self.filtered(lambda m: m.is_subcontract).move_orig_ids.production_id
# TODO: To be deleted, use self._get_subcontract_production()._has_tracked_component() instead
def _has_tracked_subcontract_components(self):
return any(m.has_tracking != 'none' for m in self._get_subcontract_production().move_raw_ids)
def _prepare_extra_move_vals(self, qty):
vals = super(StockMove, self)._prepare_extra_move_vals(qty)
vals['location_id'] = self.location_id.id
return vals
def _prepare_move_split_vals(self, qty):
vals = super(StockMove, self)._prepare_move_split_vals(qty)
vals['location_id'] = self.location_id.id
return vals
def _should_bypass_set_qty_producing(self):
if (self.production_id | self.raw_material_production_id)._get_subcontract_move():
return False
return super()._should_bypass_set_qty_producing()
def _should_bypass_reservation(self, forced_location=False):
""" If the move is subcontracted then ignore the reservation. """
should_bypass_reservation = super()._should_bypass_reservation(forced_location=forced_location)
if not should_bypass_reservation and self.is_subcontract:
return True
return should_bypass_reservation
def _update_subcontract_order_qty(self, new_quantity):
for move in self:
quantity_to_remove = move.product_uom_qty - new_quantity
move._reduce_subcontract_order_qty(quantity_to_remove)
def _reduce_subcontract_order_qty(self, quantity_to_remove):
self.ensure_one()
productions = self.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))[::-1]
wip_production = productions[0] if self._context.get('transfer_qty') and len(productions) > 1 else self.env['mrp.production']
# Transfer removed qty to WIP production
if wip_production:
self.env['change.production.qty'].with_context(skip_activity=True).create({
'mo_id': wip_production.id,
'product_qty': wip_production.product_qty + quantity_to_remove
}).change_prod_qty()
# Cancel productions until reach new_quantity
for production in (productions - wip_production):
if float_compare(quantity_to_remove, production.product_qty, precision_rounding=production.product_uom_id.rounding) >= 0:
quantity_to_remove -= production.product_qty
production.with_context(skip_activity=True).action_cancel()
else:
if float_is_zero(quantity_to_remove, precision_rounding=production.product_uom_id.rounding):
# No need to do change_prod_qty for no change at all.
break
self.env['change.production.qty'].with_context(skip_activity=True).create({
'mo_id': production.id,
'product_qty': production.product_qty - quantity_to_remove
}).change_prod_qty()
break
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'."))
def _is_subcontract_return(self):
self.ensure_one()
subcontracting_location = self.picking_id.partner_id.with_company(self.company_id).property_stock_subcontractor
return (
not self.is_subcontract
and self.origin_returned_move_id.is_subcontract
and self.location_dest_id.id == subcontracting_location.id
)

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, models
class StockMoveLine(models.Model):
_inherit = 'stock.move.line'
@api.onchange('lot_name', 'lot_id')
def _onchange_serial_number(self):
current_location_id = self.location_id
res = super()._onchange_serial_number()
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" + \
_("Make sure you validate or adapt the related resupply picking to your subcontractor in order to avoid inconsistencies in your stock.")
return res
def write(self, vals):
for move_line in self:
if vals.get('lot_id') and move_line.move_id.is_subcontract and move_line.location_id.is_subcontracting_location:
# Update related subcontracted production to keep consistency between production and reception.
subcontracted_production = move_line.move_id._get_subcontract_production().filtered(lambda p: p.state not in ('done', 'cancel') and p.lot_producing_id == move_line.lot_id)
if subcontracted_production:
subcontracted_production.lot_producing_id = vals['lot_id']
return super().write(vals)

View file

@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from datetime import timedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare
from dateutil.relativedelta import relativedelta
class StockPicking(models.Model):
_name = 'stock.picking'
_inherit = 'stock.picking'
# override existing field domains to prevent suboncontracting production lines from showing in Detailed Operations tab
move_line_nosuggest_ids = fields.One2many(
domain=['&', '|', ('location_dest_id.usage', '!=', 'production'), ('move_id.picking_code', '!=', 'outgoing'),
'|', ('reserved_qty', '=', 0.0), '&', ('reserved_qty', '!=', 0.0), ('qty_done', '!=', 0.0)])
move_line_ids_without_package = fields.One2many(
domain=['&', '|', ('location_dest_id.usage', '!=', 'production'), ('move_id.picking_code', '!=', 'outgoing'),
'|', ('package_level_id', '=', False), ('picking_type_entire_packs', '=', False)])
display_action_record_components = fields.Selection(
[('hide', 'Hide'), ('facultative', 'Facultative'), ('mandatory', 'Mandatory')],
compute='_compute_display_action_record_components')
@api.depends('state', 'move_ids')
def _compute_display_action_record_components(self):
self.display_action_record_components = 'hide'
for picking in self:
# Hide if not encoding state or it is not a subcontracting picking
if picking.state in ('draft', 'cancel', 'done') or not picking._is_subcontract():
continue
subcontracted_moves = picking.move_ids.filtered(lambda m: m.is_subcontract)
if subcontracted_moves._subcontrating_should_be_record():
picking.display_action_record_components = 'mandatory'
continue
if subcontracted_moves._subcontrating_can_be_record():
picking.display_action_record_components = 'facultative'
# -------------------------------------------------------------------------
# Action methods
# -------------------------------------------------------------------------
def _action_done(self):
res = super(StockPicking, self)._action_done()
for move in self.move_ids:
if not move.is_subcontract:
continue
# Auto set qty_producing/lot_producing_id of MO wasn't recorded
# manually (if the flexible + record_component or has tracked component)
productions = move._get_subcontract_production()
recorded_productions = productions.filtered(lambda p: p._has_been_recorded())
recorded_qty = sum(recorded_productions.mapped('qty_producing'))
sm_done_qty = sum(productions._get_subcontract_move().mapped('quantity_done'))
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
if float_compare(recorded_qty, sm_done_qty, precision_digits=rounding) >= 0:
continue
production = productions - recorded_productions
if not production:
continue
if len(production) > 1:
raise UserError(_("There shouldn't be multiple productions to record for the same subcontracted move."))
# Manage additional quantities
quantity_done_move = move.product_uom._compute_quantity(move.quantity_done, production.product_uom_id)
if float_compare(production.product_qty, quantity_done_move, precision_rounding=production.product_uom_id.rounding) == -1:
change_qty = self.env['change.production.qty'].create({
'mo_id': production.id,
'product_qty': quantity_done_move
})
change_qty.with_context(skip_activity=True).change_prod_qty()
# Create backorder MO for each move lines
amounts = [move_line.qty_done for move_line in move.move_line_ids]
len_amounts = len(amounts)
# _split_production can set the qty_done, but not split it.
# Remove the qty_done potentially set by a previous split to prevent any issue.
production.move_line_raw_ids.filtered(lambda sml: sml.state not in ['done', 'cancel']).write({'qty_done': 0})
productions = production._split_productions({production: amounts}, set_consumed_qty=True)
for production, move_line in zip(productions, move.move_line_ids):
if move_line.lot_id:
production.lot_producing_id = move_line.lot_id
production.qty_producing = production.product_qty
production._set_qty_producing()
productions[:len_amounts].subcontracting_has_been_recorded = True
for picking in self:
productions_to_done = picking._get_subcontract_production()._subcontracting_filter_to_done()
productions_to_done._subcontract_sanity_check()
if not productions_to_done:
continue
productions_to_done = productions_to_done.sudo()
production_ids_backorder = []
if not self.env.context.get('cancel_backorder'):
production_ids_backorder = productions_to_done.filtered(lambda mo: mo.state == "progress").ids
productions_to_done.with_context(mo_ids_to_backorder=production_ids_backorder).button_mark_done()
# For concistency, set the date on production move before the date
# on picking. (Traceability report + Product Moves menu item)
minimum_date = min(picking.move_line_ids.mapped('date'))
production_moves = productions_to_done.move_raw_ids | productions_to_done.move_finished_ids
production_moves.write({'date': minimum_date - timedelta(seconds=1)})
production_moves.move_line_ids.write({'date': minimum_date - timedelta(seconds=1)})
return res
def action_record_components(self):
self.ensure_one()
move_subcontracted = self.move_ids.filtered(lambda m: m.is_subcontract)
for move in move_subcontracted:
production = move._subcontrating_should_be_record()
if production:
return move._action_record_components()
for move in move_subcontracted:
production = move._subcontrating_can_be_record()
if production:
return move._action_record_components()
raise UserError(_("Nothing to record"))
# -------------------------------------------------------------------------
# Subcontract helpers
# -------------------------------------------------------------------------
def _is_subcontract(self):
self.ensure_one()
return self.picking_type_id.code == 'incoming' and any(m.is_subcontract for m in self.move_ids)
def _get_subcontract_production(self):
return self.move_ids._get_subcontract_production()
def _get_warehouse(self, subcontract_move):
return subcontract_move.warehouse_id or self.picking_type_id.warehouse_id or subcontract_move.move_dest_ids.picking_type_id.warehouse_id
def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
subcontract_move.ensure_one()
group = self.env['procurement.group'].create({
'name': self.name,
'partner_id': self.partner_id.id,
})
product = subcontract_move.product_id
warehouse = self._get_warehouse(subcontract_move)
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,
'location_src_id': subcontract_move.picking_id.partner_id.with_company(subcontract_move.company_id).property_stock_subcontractor.id,
'location_dest_id': subcontract_move.picking_id.partner_id.with_company(subcontract_move.company_id).property_stock_subcontractor.id,
'product_qty': subcontract_move.product_uom_qty,
'picking_type_id': warehouse.subcontracting_type_id.id,
'date_planned_start': subcontract_move.date - relativedelta(days=product.produce_delay)
}
return vals
def _subcontracted_produce(self, subcontract_details):
self.ensure_one()
group_move = defaultdict(list)
group_by_company = defaultdict(list)
for move, bom in subcontract_details:
# do not create extra production for move that have their quantity updated
if move.move_orig_ids.production_id:
continue
if float_compare(move.product_qty, 0, precision_rounding=move.product_uom.rounding) <= 0:
# If a subcontracted amount is decreased, don't create a MO that would be for a negative value.
continue
mo_subcontract = self._prepare_subcontract_mo_vals(move, bom)
# Link the move to the id of the MO's procurement group
group_move[mo_subcontract['procurement_group_id']] = move
# Group the MO by company
group_by_company[move.company_id.id].append(mo_subcontract)
all_mo = set()
for company, group in group_by_company.items():
grouped_mo = self.env['mrp.production'].with_company(company).create(group)
all_mo.update(grouped_mo.ids)
all_mo = self.env['mrp.production'].browse(sorted(all_mo))
all_mo.action_confirm()
for mo in all_mo:
move = group_move[mo.procurement_group_id.id][0]
finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
finished_move.write({'move_dest_ids': [(4, move.id, False)]})
all_mo.action_assign()
@api.onchange('location_id', 'location_dest_id')
def _onchange_locations(self):
moves = self.move_ids | self.move_ids_without_package
moves.filtered(lambda m: m.is_subcontract).update({
"location_dest_id": self.location_dest_id,
})
moves.filtered(lambda m: not m.is_subcontract).update({
"location_id": self.location_id,
"location_dest_id": self.location_dest_id,
})
if any(line.reserved_qty or line.qty_done for line in self.move_ids.move_line_ids):
return {'warning': {
'title': _("Locations to update"),
'message': _("You might want to update the locations of this transfer's operations"),
}
}

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.exceptions import UserError
class StockQuant(models.Model):
_inherit = 'stock.quant'
is_subcontract = fields.Boolean(store=False, search='_search_is_subcontract')
def _search_is_subcontract(self, operator, value):
if operator not in ['=', '!='] or not isinstance(value, bool):
raise UserError(_('Operation not supported'))
return [('location_id.is_subcontracting_location', operator, value)]

View file

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class StockRule(models.Model):
_inherit = "stock.rule"
def _push_prepare_move_copy_values(self, move_to_copy, new_date):
new_move_vals = super(StockRule, self)._push_prepare_move_copy_values(move_to_copy, new_date)
new_move_vals["is_subcontract"] = False
return new_move_vals

View file

@ -0,0 +1,223 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class StockWarehouse(models.Model):
_inherit = 'stock.warehouse'
subcontracting_to_resupply = fields.Boolean(
'Resupply Subcontractors', default=True)
subcontracting_mto_pull_id = fields.Many2one(
'stock.rule', 'Subcontracting MTO Rule')
subcontracting_pull_id = fields.Many2one(
'stock.rule', 'Subcontracting MTS Rule'
)
subcontracting_route_id = fields.Many2one('stock.route', 'Resupply Subcontractor', ondelete='restrict')
subcontracting_type_id = fields.Many2one(
'stock.picking.type', 'Subcontracting Operation Type',
domain=[('code', '=', 'mrp_operation')])
subcontracting_resupply_type_id = fields.Many2one(
'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()
# if new warehouse has resupply enabled, enable global route
if any([vals.get('subcontracting_to_resupply', False) for vals in vals_list]):
res._update_global_route_resupply_subcontractor()
return res
def write(self, vals):
res = super().write(vals)
# if all warehouses have resupply disabled, disable global route, until its enabled on a warehouse
if 'subcontracting_to_resupply' in vals or 'active' in vals:
if 'subcontracting_to_resupply' in vals:
# ignore when warehouse archived since it will auto-archive all of its rules
self._update_resupply_rules()
self._update_global_route_resupply_subcontractor()
return res
def get_rules_dict(self):
result = super(StockWarehouse, self).get_rules_dict()
subcontract_location_id = self._get_subcontracting_location()
for warehouse in self:
result[warehouse.id].update({
'subcontract': [
self.Routing(warehouse.lot_stock_id, subcontract_location_id, warehouse.subcontracting_resupply_type_id, 'pull'),
]
})
return result
def _update_global_route_resupply_subcontractor(self):
route_id = self._find_or_create_global_route('mrp_subcontracting.route_resupply_subcontractor_mto',
_('Resupply Subcontractor on Order'))
if not route_id.sudo().rule_ids.filtered(lambda r: r.active):
route_id.active = False
else:
route_id.active = True
def _get_routes_values(self):
routes = super(StockWarehouse, self)._get_routes_values()
routes.update({
'subcontracting_route_id': {
'routing_key': 'subcontract',
'depends': ['subcontracting_to_resupply'],
'route_create_values': {
'product_categ_selectable': False,
'warehouse_selectable': True,
'product_selectable': False,
'company_id': self.company_id.id,
'sequence': 10,
'name': self._format_routename(name=_('Resupply Subcontractor'))
},
'route_update_values': {
'active': self.subcontracting_to_resupply,
},
'rules_values': {
'active': self.subcontracting_to_resupply,
}
}
})
return routes
def _get_global_route_rules_values(self):
rules = super(StockWarehouse, self)._get_global_route_rules_values()
subcontract_location_id = self._get_subcontracting_location()
production_location_id = self._get_production_location()
rules.update({
'subcontracting_mto_pull_id': {
'depends': ['subcontracting_to_resupply'],
'create_values': {
'procure_method': 'make_to_order',
'company_id': self.company_id.id,
'action': 'pull',
'auto': 'manual',
'route_id': self._find_or_create_global_route('stock.route_warehouse0_mto', _('Make To Order')).id,
'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, 'MTO'),
'location_dest_id': subcontract_location_id.id,
'location_src_id': self.lot_stock_id.id,
'picking_type_id': self.subcontracting_resupply_type_id.id
},
'update_values': {
'active': self.subcontracting_to_resupply
}
},
'subcontracting_pull_id': {
'depends': ['subcontracting_to_resupply'],
'create_values': {
'procure_method': 'make_to_order',
'company_id': self.company_id.id,
'action': 'pull',
'auto': 'manual',
'route_id': self._find_or_create_global_route('mrp_subcontracting.route_resupply_subcontractor_mto',
_('Resupply Subcontractor on Order')).id,
'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
},
'update_values': {
'active': self.subcontracting_to_resupply
}
},
})
return rules
def _get_picking_type_create_values(self, max_sequence):
data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence)
data.update({
'subcontracting_type_id': {
'name': _('Subcontracting'),
'code': 'mrp_operation',
'use_create_components_lots': True,
'sequence': next_sequence + 2,
'sequence_code': 'SBC',
'company_id': self.company_id.id,
},
'subcontracting_resupply_type_id': {
'name': _('Resupply Subcontractor'),
'code': 'outgoing',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_dest_id': self._get_subcontracting_location().id,
'sequence': next_sequence + 3,
'sequence_code': 'RES',
'print_label': True,
'company_id': self.company_id.id,
}
})
return data, max_sequence + 4
def _get_sequence_values(self, name=False, code=False):
values = super(StockWarehouse, self)._get_sequence_values(name=name, code=code)
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 + '/' + (self.subcontracting_type_id.sequence_code or (('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 + '/' + (self.subcontracting_resupply_type_id.sequence_code or (('RES' + str(count)) if count else 'RES')) + '/',
'padding': 5,
'company_id': self.company_id.id
},
})
return values
def _get_picking_type_update_values(self):
data = super(StockWarehouse, self)._get_picking_type_update_values()
subcontract_location_id = self._get_subcontracting_location()
production_location_id = self._get_production_location()
data.update({
'subcontracting_type_id': {
'active': False,
'default_location_src_id': subcontract_location_id.id,
'default_location_dest_id': production_location_id.id,
},
'subcontracting_resupply_type_id': {
'default_location_src_id': self.lot_stock_id.id,
'default_location_dest_id': subcontract_location_id.id,
'barcode': self.code.replace(" ", "").upper() + "-RESUPPLY",
'active': self.subcontracting_to_resupply and self.active
},
})
return data
def _get_subcontracting_location(self):
return self.company_id.subcontracting_location_id
def _get_subcontracting_locations(self):
return self.env['stock.location'].search([
('company_id', 'in', self.company_id.ids),
('is_subcontracting_location', '=', 'True'),
])
def _update_subcontracting_locations_rules(self):
subcontracting_locations = self._get_subcontracting_locations()
subcontracting_locations._activate_subcontracting_location_rules()
def _update_resupply_rules(self):
'''update (archive/unarchive) any warehouse subcontracting location resupply rules'''
subcontracting_locations = self._get_subcontracting_locations()
warehouses_to_resupply = self.filtered(lambda w: w.subcontracting_to_resupply and w.active)
if warehouses_to_resupply:
self.env['stock.rule'].with_context(active_test=False).search([
'&', ('picking_type_id', 'in', warehouses_to_resupply.subcontracting_resupply_type_id.ids),
'|', ('location_src_id', 'in', subcontracting_locations.ids),
('location_dest_id', 'in', subcontracting_locations.ids)]).action_unarchive()
warehouses_not_to_resupply = self - warehouses_to_resupply
if warehouses_not_to_resupply:
self.env['stock.rule'].search([
'&', ('picking_type_id', 'in', warehouses_not_to_resupply.subcontracting_resupply_type_id.ids),
'|', ('location_src_id', 'in', subcontracting_locations.ids),
('location_dest_id', 'in', subcontracting_locations.ids)]).action_archive()