# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). # Copyright 2019 Odoo # Copyright 2020 Tecnativa - Alexandre Díaz # Copyright 2020 Tecnativa - Pedro M. Baeza from collections import defaultdict from odoo import api, 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 raw moves is visible""" for move in self: move.show_subcontracting_details_visible = ( move.is_subcontract and move._has_tracked_subcontract_components() and not float_is_zero( move.quantity_done, precision_rounding=move.product_uom.rounding ) ) 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()._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 @api.multi def copy(self, default=None): self.ensure_one() if not self.is_subcontract or 'location_id' in default: return super().copy(default=default) if not default: default = {} default['location_id'] = self.picking_id.location_id.id return super().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: if self.env.context.get('cancel_backorder') is False: return super().write(values) self.filtered(lambda m: ( m.is_subcontract and m.state not in ['draft', 'cancel', 'done'] ))._update_subcontract_order_qty(values['product_uom_qty']) return super().write(values) 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.is_subcontract: rounding = self.product_uom.rounding production = self.move_orig_ids.mapped("production_id") if 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: return self._action_record_components() action = super().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.mapped("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') return { 'name': _('Raw Materials for %s') % (self.product_id.display_name), 'type': 'ir.actions.act_window', 'res_model': 'stock.move', 'views': [(tree_view.id, 'tree'), (form_view.id, 'form')], 'target': 'current', 'domain': [('id', 'in', moves.ids)], } def _action_cancel(self): for move in self: if move.is_subcontract: move.move_orig_ids.mapped("production_id").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.mapped("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_context( force_company=move.company_id.id) .property_stock_subcontractor.id }) for picking, subcontract_details in\ subcontract_details_per_picking.items(): picking._subcontracted_produce(subcontract_details) res = super(StockMove, self)._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_assign(self): """As we don't have the bypass reservation method in v12 at stock.move level, we have to trick this method for splitting the assign in 2 steps, classifying previously the subcontract moves and then faking location_id.should_bypass_reservation method through context. """ subcontract_moves = self.filtered('is_subcontract') res = super(StockMove, self - subcontract_moves)._action_assign() super(StockMove, subcontract_moves.with_context( mrp_subcontracting_bypass_reservation=True))._action_assign() return res def _action_record_components(self): action = self.env.ref('mrp.act_mrp_product_produce').read()[0] action['context'] = dict( active_id=self.move_orig_ids.mapped("production_id").id, default_subcontract_move_id=self.id ) return action def _check_overprocessed_subcontract_qty(self): """ If a subcontracted move use tracked components. Do not allow to add quantity without the produce wizard. Instead update the initial demand and use the register component button. Split or correct a lot/sn is possible. """ overprocessed_moves = self.env['stock.move'] for move in self: if not move.is_subcontract: continue # Extra quantity is allowed when components do not need to be # register if not move._has_tracked_subcontract_components(): continue rounding = move.product_uom.rounding if float_compare( move.quantity_done, sum(move.move_orig_ids.mapped('production_id.qty_produced')), precision_rounding=rounding ) > 0: overprocessed_moves |= move if overprocessed_moves: raise UserError(_(""" You have to use 'Records Components' button in order to register quantity for a subcontracted product(s) with tracked component(s): %s. If you want to process more than initially planned, you can use the edit + unlock buttons in order to adapt the initial demand on the operations.""") % ('\n'.join(overprocessed_moves.mapped( 'product_id.display_name')))) def _get_subcontract_bom(self): self.ensure_one() return 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, ) def _has_tracked_subcontract_components(self): self.ensure_one() return any(m.has_tracking != 'none' for m in self.move_orig_ids.mapped('production_id.move_raw_ids')) def _prepare_extra_move_vals(self, qty): vals = super()._prepare_extra_move_vals(qty) vals['location_id'] = self.location_id.id return vals def _prepare_move_split_vals(self, qty): vals = super()._prepare_move_split_vals(qty) vals['location_id'] = self.location_id.id return vals def _update_subcontract_order_qty(self, quantity): for move in self: quantity_change = quantity - move.product_uom_qty production = move.move_orig_ids.mapped("production_id") if production: self.env['change.production.qty'].create({ 'mo_id': production.id, 'product_qty': production.product_uom_qty + quantity_change }).change_prod_qty()