# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, Command, fields, models from odoo.tools.misc import groupby MAP_REPAIR_LINE_TYPE_TO_MOVE_LOCATIONS_FROM_REPAIR = { 'add': {'location_id': 'location_id', 'location_dest_id': 'location_dest_id'}, 'remove': {'location_id': 'location_dest_id', 'location_dest_id': 'parts_location_id'}, 'recycle': {'location_id': 'location_dest_id', 'location_dest_id': 'recycle_location_id'}, } class StockMove(models.Model): _inherit = 'stock.move' repair_id = fields.Many2one('repair.order', check_company=True) repair_line_type = fields.Selection([ ('add', 'Add'), ('remove', 'Remove'), ('recycle', 'Recycle') ], 'Type', store=True, index=True) @api.depends('repair_line_type') def _compute_forecast_information(self): moves_to_compute = self.filtered(lambda move: not move.repair_line_type or move.repair_line_type == 'add') for move in (self - moves_to_compute): move.forecast_availability = move.product_qty move.forecast_expected_date = False return super(StockMove, moves_to_compute)._compute_forecast_information() @api.depends('repair_id.picking_type_id') def _compute_picking_type_id(self): remaining_moves = self for move in self: if move.repair_id: move.picking_type_id = move.repair_id.picking_type_id remaining_moves -= move return super(StockMove, remaining_moves)._compute_picking_type_id() def copy_data(self, default=None): default = dict(default or {}) if 'repair_id' in default or self.repair_id: default['sale_line_id'] = False return super().copy_data(default) @api.ondelete(at_uninstall=False) def _unlink_if_draft_or_cancel(self): self.filtered('repair_id')._action_cancel() return super()._unlink_if_draft_or_cancel() def unlink(self): self._clean_repair_sale_order_line() return super().unlink() @api.model_create_multi def create(self, vals_list): for vals in vals_list: if not vals.get('repair_id') or 'repair_line_type' not in vals: continue repair_id = self.env['repair.order'].browse([vals['repair_id']]) vals['name'] = repair_id.name src_location, dest_location = self._get_repair_locations(vals['repair_line_type'], repair_id) if not vals.get('location_id'): vals['location_id'] = src_location.id if not vals.get('location_dest_id'): vals['location_dest_id'] = dest_location.id moves = super().create(vals_list) repair_moves = self.env['stock.move'] for move in moves: if not move.repair_id: continue move.group_id = move.repair_id.procurement_group_id.id move.origin = move.name move.picking_type_id = move.repair_id.picking_type_id.id repair_moves |= move if move.state == 'draft' and move.repair_id.state in ('confirmed', 'under_repair'): move._check_company() move._adjust_procure_method() move._action_confirm() move._trigger_scheduler() repair_moves._create_repair_sale_order_line() return moves def write(self, vals): res = super().write(vals) repair_moves = self.env['stock.move'] moves_to_create_so_line = self.env['stock.move'] for move in self: if not move.repair_id: continue # checks vals update if 'repair_line_type' in vals or 'picking_type_id' in vals and move.product_id != move.repair_id.product_id: move.location_id, move.location_dest_id = move._get_repair_locations(move.repair_line_type) if not move.sale_line_id and 'sale_line_id' not in vals and move.repair_line_type == 'add': moves_to_create_so_line |= move if move.sale_line_id and ('repair_line_type' in vals or 'product_uom_qty' in vals): repair_moves |= move repair_moves._update_repair_sale_order_line() moves_to_create_so_line._create_repair_sale_order_line() return res # Needed to also cancel the lastly added part def _action_cancel(self): self._clean_repair_sale_order_line() return super()._action_cancel() def _create_repair_sale_order_line(self): if not self: return so_line_vals = [] for move in self: if move.sale_line_id or move.repair_line_type != 'add' or not move.repair_id.sale_order_id: continue product_qty = move.product_uom_qty if move.repair_id.state != 'done' else move.quantity so_line_vals.append({ 'order_id': move.repair_id.sale_order_id.id, 'product_id': move.product_id.id, 'product_uom_qty': product_qty, # When relying only on so_line compute method, the sol quantity is only updated on next sol creation 'move_ids': [Command.link(move.id)], }) if move.repair_id.under_warranty: so_line_vals[-1]['price_unit'] = 0.0 elif move.price_unit: so_line_vals[-1]['price_unit'] = move.price_unit self.env['sale.order.line'].create(so_line_vals) def _clean_repair_sale_order_line(self): self.filtered( lambda m: m.repair_id and m.sale_line_id ).mapped('sale_line_id').write({'product_uom_qty': 0.0}) def _update_repair_sale_order_line(self): if not self: return moves_to_clean = self.env['stock.move'] moves_to_update = self.env['stock.move'] for move in self: if not move.repair_id: continue if move.sale_line_id and move.repair_line_type != 'add': moves_to_clean |= move if move.sale_line_id and move.repair_line_type == 'add': moves_to_update |= move moves_to_clean._clean_repair_sale_order_line() for sale_line, _ in groupby(moves_to_update, lambda m: m.sale_line_id): sale_line.product_uom_qty = sum(sale_line.move_ids.mapped('product_uom_qty')) def _is_consuming(self): return super()._is_consuming() or (self.repair_id and self.repair_line_type == 'add') def _get_repair_locations(self, repair_line_type, repair_id=False): location_map = MAP_REPAIR_LINE_TYPE_TO_MOVE_LOCATIONS_FROM_REPAIR.get(repair_line_type) if location_map: if not repair_id: self.repair_id.ensure_one() repair_id = self.repair_id location_id, location_dest_id = [repair_id[field] for field in location_map.values()] else: location_id, location_dest_id = False, False return location_id, location_dest_id def _get_source_document(self): return self.repair_id or super()._get_source_document() def _set_repair_locations(self): moves_per_repair = self.filtered(lambda m: (m.repair_id and m.repair_line_type) is not False).grouped('repair_id') if not moves_per_repair: return for moves in moves_per_repair.values(): grouped_moves = moves.grouped('repair_line_type') for line_type, m in grouped_moves.items(): m.location_id, m.location_dest_id = m._get_repair_locations(line_type) def _should_be_assigned(self): res = super(StockMove, self)._should_be_assigned() return bool(res and not (self.repair_id and self.repair_line_type)) def _create_extra_move(self): if self.repair_id: return self return super(StockMove, self)._create_extra_move() def _split(self, qty, restrict_partner_id=False): # When setting the Repair Order as done with partially done moves, do not split these moves if self.repair_id: return [] return super(StockMove, self)._split(qty, restrict_partner_id)