from odoo import api, fields, models import logging _logger = logging.getLogger(__name__) class StockPicking(models.Model): _inherit = 'stock.picking' subcontract_lot_count = fields.Integer( 'Subcontract Lots Count', compute='_compute_subcontract_lot_count' ) has_subcontract_moves = fields.Boolean( 'Has Subcontract Moves', compute='_compute_has_subcontract_moves' ) @api.depends('move_ids') def _compute_has_subcontract_moves(self): """Compute if this picking has any subcontract moves.""" for picking in self: # Check for 'is_subcontract' field safely using getattr/mapped in case other modules affect it subcontract_moves = picking.move_ids.filtered(lambda m: getattr(m, 'is_subcontract', False)) picking.has_subcontract_moves = bool(subcontract_moves) @api.depends('move_ids.move_line_ids.lot_id') def _compute_subcontract_lot_count(self): """Compute the number of lots generated for subcontract moves.""" for picking in self: if picking.picking_type_code == 'incoming': subcontract_moves = picking.move_ids.filtered(lambda m: getattr(m, 'is_subcontract', False)) lot_ids = subcontract_moves.move_line_ids.mapped('lot_id') picking.subcontract_lot_count = len(lot_ids) else: picking.subcontract_lot_count = 0 def action_view_generated_lots(self): """Action to view all lots generated for subcontract moves in this picking.""" self.ensure_one() subcontract_moves = self.move_ids.filtered(lambda m: getattr(m, 'is_subcontract', False)) lot_ids = subcontract_moves.move_line_ids.mapped('lot_id').ids if not lot_ids: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No Lots Found', 'message': 'No lots have been generated for subcontract moves yet.', 'type': 'info', 'sticky': False, } } return { 'name': f'Generated Lots ({len(lot_ids)})', 'type': 'ir.actions.act_window', 'res_model': 'stock.lot', 'view_mode': 'list,form', 'views': [(False, 'list'), (False, 'form')], 'domain': [('id', 'in', lot_ids)], 'context': { 'create': False, 'edit': False, }, 'target': 'current', } def action_auto_generate_lots_subcontract(self): """ Action to auto-generate lot numbers for subcontracting moves. Similar to the "+" icon functionality in MO forms. """ generated_count = 0 generated_lots = [] for picking in self: if picking.picking_type_code == 'incoming': subcontract_moves = picking.move_ids.filtered(lambda m: getattr(m, 'is_subcontract', False)) if not subcontract_moves: continue for move in subcontract_moves: if move.product_id.tracking in ['lot', 'serial']: try: lots = move._auto_generate_lots_for_subcontract() if lots: generated_count += len(lots) generated_lots.extend([lot.name for lot in lots]) except Exception as e: _logger.error(f"Error generating lots for move {move.id}: {e}") return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Generation Error', 'message': f'Error generating lots: {str(e)}', 'type': 'danger', 'sticky': True, } } if generated_count > 0: message = f'Generated {generated_count} lots: {", ".join(generated_lots[:5])}' if len(generated_lots) > 5: message += f' and {len(generated_lots) - 5} more...' return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Success', 'message': message, 'type': 'success', 'sticky': False, } } else: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No Lots Generated', 'message': 'No lots were generated. Check that products have lot/serial tracking and custom sequences configured.', 'type': 'info', 'sticky': False, } } def _auto_assign_lots_on_subcontract_receipt(self): """ Automatically assign lot numbers when validating subcontract receipts. This is called during the validation process. """ for picking in self: if picking.picking_type_code == 'incoming': subcontract_moves = picking.move_ids.filtered(lambda m: getattr(m, 'is_subcontract', False)) for move in subcontract_moves: if move.product_id.tracking in ['lot', 'serial'] and move.state not in ['done', 'cancel']: move._auto_generate_lots_for_subcontract() def action_open_subcontract_lot_wizard(self): """Open the subcontract lot generator wizard.""" self.ensure_one() # Find the first subcontract move for default values subcontract_move = self.move_ids.filtered(lambda m: getattr(m, 'is_subcontract', False))[:1] if not subcontract_move: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'No Subcontract Moves', 'message': 'This picking does not contain any subcontract moves.', 'type': 'warning', 'sticky': False, } } context = { 'default_picking_id': self.id, } if subcontract_move: context.update({ 'default_move_id': subcontract_move.id, 'default_product_id': subcontract_move.product_id.id, 'default_quantity': subcontract_move.product_uom_qty, }) return { 'name': 'Generate Subcontract Lots', 'type': 'ir.actions.act_window', 'res_model': 'subcontract.lot.generator', 'view_mode': 'form', 'target': 'new', 'context': context, } def button_validate(self): """Override to auto-generate lots for subcontract moves before validation.""" # Auto-generate lots for subcontract moves if needed self._auto_assign_lots_on_subcontract_receipt() return super().button_validate()