diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mrp_subcontracting/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mrp_subcontracting/models')
| -rw-r--r-- | addons/mrp_subcontracting/models/__init__.py | 12 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/mrp_bom.py | 27 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/mrp_production.py | 140 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/product.py | 17 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/res_company.py | 37 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/res_partner.py | 25 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/stock_move.py | 217 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/stock_move_line.py | 15 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/stock_picking.py | 132 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/stock_rule.py | 13 | ||||
| -rw-r--r-- | addons/mrp_subcontracting/models/stock_warehouse.py | 139 |
11 files changed, 774 insertions, 0 deletions
diff --git a/addons/mrp_subcontracting/models/__init__.py b/addons/mrp_subcontracting/models/__init__.py new file mode 100644 index 00000000..7b4d6067 --- /dev/null +++ b/addons/mrp_subcontracting/models/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +from . import mrp_bom +from . import product +from . import res_company +from . import res_partner +from . import stock_move +from . import stock_move_line +from . import stock_picking +from . import stock_rule +from . import stock_warehouse +from . import mrp_production diff --git a/addons/mrp_subcontracting/models/mrp_bom.py b/addons/mrp_subcontracting/models/mrp_bom.py new file mode 100644 index 00000000..e3b9c0a7 --- /dev/null +++ b/addons/mrp_subcontracting/models/mrp_bom.py @@ -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_tmpl=None, product=None, picking_type=None, company_id=False, bom_type='subcontract', subcontractor=False): + domain = self._bom_find_domain(product_tmpl=product_tmpl, product=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', limit=1) + else: + return self.env['mrp.bom'] + + @api.constrains('operation_ids', 'type') + def _check_subcontracting_no_operation(self): + if self.filtered_domain([('type', '=', 'subcontract'), ('operation_ids', '!=', False)]): + raise ValidationError(_('You can not set a Bill of Material with operations as subcontracting.')) diff --git a/addons/mrp_subcontracting/models/mrp_production.py b/addons/mrp_subcontracting/models/mrp_production.py new file mode 100644 index 00000000..a0917e15 --- /dev/null +++ b/addons/mrp_subcontracting/models/mrp_production.py @@ -0,0 +1,140 @@ +# -*- 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 +from odoo.tools.float_utils import float_compare, float_is_zero + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + 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' + ) + + @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 _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 subcontracting_record_component(self): + self.ensure_one() + assert self.env.context.get('subcontract_move_id') + if float_is_zero(self.qty_producing, precision_rounding=self.product_uom_id.rounding): + return {'type': 'ir.actions.act_window_close'} + for sml in self.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.name) + self._update_finished_move() + quantity_issues = self._get_quantity_produced_issues() + if quantity_issues: + backorder = self._generate_backorder_productions(close_mo=False) + # 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 + subcontract_move_id = self.env['stock.move'].browse(self.env.context.get('subcontract_move_id')) + action = subcontract_move_id._action_record_components() + action.update({'res_id': backorder.id}) + return action + return {'type': 'ir.actions.act_window_close'} + + def _pre_button_mark_done(self): + if self.env.context.get('subcontract_move_id'): + return True + return super()._pre_button_mark_done() + + def _update_finished_move(self): + """ After producing, set the move line on the subcontract picking. """ + self.ensure_one() + subcontract_move_id = self.env.context.get('subcontract_move_id') + if subcontract_move_id: + subcontract_move_id = self.env['stock.move'].browse(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.product_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.product_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.product_uom_qty - new_quantity_done + default = { + 'product_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({ + 'product_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, + 'product_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.product_uom_qty, precision_rounding=ml.product_uom_id.rounding)) + ml_reserved.unlink() + for ml in subcontract_move_id.move_line_ids: + ml.product_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 float_is_zero(mo.qty_producing, precision_rounding=mo.product_uom_id.rounding): + return False + if not all(line.lot_id for line in mo.move_raw_ids.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids): + return False + return True + + return self.filtered(filter_in) diff --git a/addons/mrp_subcontracting/models/product.py b/addons/mrp_subcontracting/models/product.py new file mode 100644 index 00000000..88e969cc --- /dev/null +++ b/addons/mrp_subcontracting/models/product.py @@ -0,0 +1,17 @@ +# -*- 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('name', '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) + supplier.is_subcontractor = supplier.name in boms.subcontractor_ids diff --git a/addons/mrp_subcontracting/models/res_company.py b/addons/mrp_subcontracting/models/res_company.py new file mode 100644 index 00000000..583d2413 --- /dev/null +++ b/addons/mrp_subcontracting/models/res_company.py @@ -0,0 +1,37 @@ +# -*- 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'].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, + }) + self.env['ir.property']._set_default( + "property_stock_subcontractor", + "res.partner", + subcontracting_location, + company, + ) + company.subcontracting_location_id = subcontracting_location diff --git a/addons/mrp_subcontracting/models/res_partner.py b/addons/mrp_subcontracting/models/res_partner.py new file mode 100644 index 00000000..67756a80 --- /dev/null +++ b/addons/mrp_subcontracting/models/res_partner.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import 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") + + 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)] diff --git a/addons/mrp_subcontracting/models/stock_move.py b/addons/mrp_subcontracting/models/stock_move.py new file mode 100644 index 00000000..0d4a9ed5 --- /dev/null +++ b/addons/mrp_subcontracting/models/stock_move.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from collections import defaultdict + +from odoo import fields, models, _ +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare, float_is_zero + + +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_show_subcontracting_details_visible(self): + """ Compute if the action button in order to see moves raw is visible """ + for move in self: + if move.is_subcontract and move._has_tracked_subcontract_components() and\ + not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding): + move.show_subcontracting_details_visible = True + else: + move.show_subcontracting_details_visible = False + + 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 not move._has_tracked_subcontract_components(): + continue + move.show_details_visible = True + return res + + 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. + """ + 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) + 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 + + 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._has_components_to_record(): + return self._action_record_components() + action = super(StockMove, self).action_show_details() + if self.is_subcontract and self._has_tracked_subcontract_components(): + 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, + }) + return action + + def action_show_subcontract_details(self): + """ Display moves raw for subcontracted product self. """ + moves = self.move_orig_ids.production_id.move_raw_ids + 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, subcontract_move_id=self.id) + 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 _action_cancel(self): + for move in self: + if move.is_subcontract: + active_production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel')) + moves = self.env.context.get('moves_todo') + if not moves or active_production not in moves.move_orig_ids.production_id: + active_production.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) + move_to_not_merge = self.env['stock.move'] + 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.")) + subcontract_details_per_picking[move.picking_id].append((move, bom)) + move.write({ + 'is_subcontract': True, + 'location_id': move.picking_id.partner_id.with_company(move.company_id).property_stock_subcontractor.id + }) + move_to_not_merge |= move + for picking, subcontract_details in subcontract_details_per_picking.items(): + picking._subcontracted_produce(subcontract_details) + + # We avoid merging move due to complication with stock.rule. + res = super(StockMove, move_to_not_merge)._action_confirm(merge=False) + res |= super(StockMove, self - move_to_not_merge)._action_confirm(merge=merge, merge_into=merge_into) + 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.move_orig_ids.production_id[-1:] + view = self.env.ref('mrp_subcontracting.mrp_production_subcontracting_form_view') + 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': dict(self.env.context, subcontract_move_id=self.id), + } + + def _get_subcontract_bom(self): + self.ensure_one() + bom = self.env['mrp.bom'].sudo()._bom_subcontract_find( + product=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 _has_components_to_record(self): + """ Returns true if the move has still some tracked components to record. """ + self.ensure_one() + if not self.is_subcontract: + return False + rounding = self.product_uom.rounding + production = self.move_orig_ids.production_id[-1:] + return self._has_tracked_subcontract_components() and\ + float_compare(production.qty_produced, production.product_uom_qty, precision_rounding=rounding) < 0 and\ + float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) < 0 + + def _has_tracked_subcontract_components(self): + self.ensure_one() + return any(m.has_tracking != 'none' for m in self.move_orig_ids.production_id.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.env.context.get('subcontract_move_id'): + return False + return super()._should_bypass_set_qty_producing() + + def _should_bypass_reservation(self): + """ If the move is subcontracted then ignore the reservation. """ + should_bypass_reservation = super(StockMove, self)._should_bypass_reservation() + 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 + productions = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))[::-1] + # Cancel productions until reach new_quantity + for production in productions: + if quantity_to_remove <= 0.0: + break + if quantity_to_remove >= production.product_qty: + quantity_to_remove -= production.product_qty + production.with_context(skip_activity=True).action_cancel() + else: + self.env['change.production.qty'].with_context(skip_activity=True).create({ + 'mo_id': production.id, + 'product_qty': production.product_uom_qty - quantity_to_remove + }).change_prod_qty() diff --git a/addons/mrp_subcontracting/models/stock_move_line.py b/addons/mrp_subcontracting/models/stock_move_line.py new file mode 100644 index 00000000..6dfb789b --- /dev/null +++ b/addons/mrp_subcontracting/models/stock_move_line.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + + +class StockMoveLine(models.Model): + _inherit = 'stock.move.line' + + def _should_bypass_reservation(self, location): + """ If the move line is subcontracted then ignore the reservation. """ + should_bypass_reservation = super(StockMoveLine, self)._should_bypass_reservation(location) + if not should_bypass_reservation and self.move_id.is_subcontract: + return True + return should_bypass_reservation diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py new file mode 100644 index 00000000..ecef0479 --- /dev/null +++ b/addons/mrp_subcontracting/models/stock_picking.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta + +from odoo import api, fields, models +from odoo.tools.float_utils import float_compare +from dateutil.relativedelta import relativedelta + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + display_action_record_components = fields.Boolean(compute='_compute_display_action_record_components') + + @api.depends('state') + def _compute_display_action_record_components(self): + for picking in self: + # Hide if not encoding state + if picking.state in ('draft', 'cancel', 'done'): + picking.display_action_record_components = False + continue + if not picking._is_subcontract(): + picking.display_action_record_components = False + continue + # Hide if all tracked product move lines are already recorded. + picking.display_action_record_components = any( + move._has_components_to_record() for move in picking.move_lines) + + # ------------------------------------------------------------------------- + # Action methods + # ------------------------------------------------------------------------- + def _action_done(self): + res = super(StockPicking, self)._action_done() + + for move in self.move_lines.filtered(lambda move: move.is_subcontract): + # Auto set qty_producing/lot_producing_id of MO if there isn't tracked component + # If there is tracked component, the flow use subcontracting_record_component instead + if move._has_tracked_subcontract_components(): + continue + production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel'))[-1:] + if not production: + continue + # 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 + for move_line in move.move_line_ids: + if move_line.lot_id: + production.lot_producing_id = move_line.lot_id + production.qty_producing = move_line.product_uom_id._compute_quantity(move_line.qty_done, production.product_uom_id) + production._set_qty_producing() + if move_line != move.move_line_ids[-1]: + backorder = production._generate_backorder_productions(close_mo=False) + # The move_dest_ids won't be set because the _split filter out done move + backorder.move_finished_ids.filtered(lambda mo: mo.product_id == move.product_id).move_dest_ids = production.move_finished_ids.filtered(lambda mo: mo.product_id == move.product_id).move_dest_ids + production.product_qty = production.qty_producing + production = backorder + + for picking in self: + productions_to_done = picking._get_subcontracted_productions()._subcontracting_filter_to_done() + 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(subcontract_move_id=True, 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() + for move in self.move_lines: + if move._has_components_to_record(): + return move._action_record_components() + + # ------------------------------------------------------------------------- + # 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_lines) + + def _get_subcontracted_productions(self): + return self.move_lines.filtered(lambda move: move.is_subcontract).move_orig_ids.production_id + + def _get_warehouse(self, subcontract_move): + return subcontract_move.warehouse_id or self.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, + '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() + for move, bom in subcontract_details: + mo = self.env['mrp.production'].with_company(move.company_id).create(self._prepare_subcontract_mo_vals(move, bom)) + self.env['stock.move'].create(mo._get_moves_raw_values()) + self.env['stock.move'].create(mo._get_moves_finished_values()) + mo.date_planned_finished = move.date # Avoid to have the picking late depending of the MO + mo.action_confirm() + + # Link the finished to the receipt move. + 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)]}) + mo.action_assign() diff --git a/addons/mrp_subcontracting/models/stock_rule.py b/addons/mrp_subcontracting/models/stock_rule.py new file mode 100644 index 00000000..45154e0b --- /dev/null +++ b/addons/mrp_subcontracting/models/stock_rule.py @@ -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 diff --git a/addons/mrp_subcontracting/models/stock_warehouse.py b/addons/mrp_subcontracting/models/stock_warehouse.py new file mode 100644 index 00000000..ef1a6056 --- /dev/null +++ b/addons/mrp_subcontracting/models/stock_warehouse.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, _ + + +class StockWarehouse(models.Model): + _inherit = 'stock.warehouse' + + subcontracting_to_resupply = fields.Boolean( + 'Resupply Subcontractors', default=True, + help="Resupply subcontractors with components") + + 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.location.route', 'Resupply Subcontractor', ondelete='restrict') + + subcontracting_type_id = fields.Many2one( + 'stock.picking.type', 'Subcontracting Operation Type', + domain=[('code', '=', 'mrp_operation')]) + + 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.out_type_id, 'pull'), + ] + }) + return result + + 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_global_route('stock.route_warehouse0_mto', _('Make To Order')).id, + 'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, 'MTO'), + 'location_id': subcontract_location_id.id, + 'location_src_id': self.lot_stock_id.id, + 'picking_type_id': self.out_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_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), + 'location_id': production_location_id.id, + 'location_src_id': subcontract_location_id.id, + 'picking_type_id': self.out_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, + }, + }) + return data, max_sequence + 4 + + def _get_sequence_values(self): + values = super(StockWarehouse, self)._get_sequence_values() + values.update({ + 'subcontracting_type_id': {'name': self.name + ' ' + _('Sequence subcontracting'), 'prefix': self.code + '/SBC/', '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, + }, + }) + return data + + def _get_subcontracting_location(self): + return self.company_id.subcontracting_location_id |
