from odoo import api, fields, models class QualityCheck(models.Model): _inherit = 'quality.check' qty_failed_manual = fields.Float('Manual Failed Quantity', copy=False) @api.depends("production_id.qty_producing", "move_line_id", "move_line_id.quantity", "qty_tested", "qty_failed_manual", "quality_state") def _compute_qty_line(self): """ Patch to prevent 0 qty_line and respect partial quantities for MOs. If we are on an MO and have a tested quantity, that quantity represents the check better than the full MO or full move line quantity. """ # We process the checks; for those that don't match our specific criteria, we'll call super (or handle defaults) # However, calling super() with a partial set is tricky if the logic differs significantly. # quality_mrp's _compute_qty_line sets qty_line = production_id.qty_producing. # Let's iterate and set values. for qc in self: if qc.quality_state == 'fail' and qc.qty_failed_manual > 0: qc.qty_line = qc.qty_failed_manual elif qc.move_line_id: # If we have a move line, it usually defines the quantity qc.qty_line = qc.move_line_id.quantity elif qc.qty_tested > 0: # If we don't have a move line yet (before split), qty_tested is our best guess qc.qty_line = qc.qty_tested elif qc.production_id and qc.production_id.qty_producing > 0: qc.qty_line = qc.production_id.qty_producing else: # Fallback to standard logic if we can't determine better # But since we can't easily call super() for a single record without potential recursion or complexity if mixed # We replicate the base defaults: 1.0 or whatever super does. # The safest way is to let super calculate first, then override. pass # Call super first to populate defaults super()._compute_qty_line() # Now override with our specific logic for qc in self: if qc.quality_state == 'fail' and qc.qty_failed_manual > 0: qc.qty_line = qc.qty_failed_manual # We preserve the other logical improvements we wanted (like using move_line_id.quantity if available) # But primarily we care about the failed manual quantity for now. elif qc.move_line_id: qc.qty_line = qc.move_line_id.quantity @api.depends('qty_line', 'quality_state') def _compute_qty_passed(self): for qc in self: if qc.quality_state == 'pass': qc.qty_passed = qc.qty_line else: qc.qty_passed = 0 @api.depends('qty_line', 'quality_state', 'qty_failed_manual') def _compute_qty_failed(self): """ Patch to allow manual override of failed quantity from wizard context """ import logging _logger = logging.getLogger(__name__) for qc in self: if qc.qty_failed_manual > 0: _logger.info(f"DEBUG: Using qty_failed_manual {qc.qty_failed_manual} for check {qc.id}") qc.qty_failed = qc.qty_failed_manual elif qc.quality_state == 'fail': # Priority 2: Context override (legacy/fallback) context_qty = self.env.context.get('quality_check_qty_failed') if context_qty is not None: _logger.info(f"DEBUG: Using context qty {context_qty} for check {qc.id}") qc.qty_failed = context_qty # Priority 3: MO checks likely use the full qty_line of the split move line else: _logger.info(f"DEBUG: Using qty_line {qc.qty_line} for check {qc.id}") qc.qty_failed = qc.qty_line else: qc.qty_failed = 0 def _can_move_line_to_failure_location(self): self.ensure_one() # Ported from Odoo 18 quality_mrp/models/quality.py # We also allow if qty_failed_manual is set, assuming the user implies a quantity check if self.production_id and self.quality_state == 'fail' and (self.point_id.measure_on == 'move_line' or self.qty_failed_manual > 0): mo = self.production_id move = mo.move_finished_ids.filtered(lambda m: m.product_id == mo.product_id) # Ensure we have a move line if not already set (rare but possible in some flows) if not self.move_line_id and move: self.move_line_id = move.move_line_ids[:1] return True return False def _move_line_to_failure_location(self, failure_location_id, failed_qty=None): """ Ported from Odoo 18 quality_control/models/quality.py This handles the splitting of the stock.move.line and stock.move """ import logging _logger = logging.getLogger(__name__) for check in self: _logger.info(f"DEBUG: Processing check {check.id} for failure split") if not check._can_move_line_to_failure_location(): _logger.info(f"DEBUG: Check {check.id} cannot move to failure location") continue failed_qty = failed_qty or check.qty_failed_manual or check.move_line_id.quantity move_line = check.move_line_id move = move_line.move_id mo = check.production_id dest_location = failure_location_id or move_line.location_dest_id.id _logger.info(f"DEBUG: Split stats - Check: {check.id}, Failed Qty: {failed_qty}, Original Qty: {move_line.quantity}, Dest Loc: {dest_location}") # Switch to mts if the failure location is not the same as the final destination if move_line.move_id.move_dest_ids and \ move_line.move_id.move_dest_ids.location_id.id != dest_location: move_line.move_id.move_dest_ids._break_mto_link(move_line.move_id) if failed_qty == move_line.quantity: _logger.info(f"DEBUG: Full failure for check {check.id}") move_line.location_dest_id = dest_location if move_line.quantity == move.quantity: move.location_dest_id = dest_location else: move.with_context(do_not_unreserve=True).product_uom_qty -= failed_qty move.copy({ 'location_dest_id': dest_location, 'move_orig_ids': move.move_orig_ids.ids, 'product_uom_qty': failed_qty, 'state': 'assigned', 'move_line_ids': [(4, move_line.id)], }) check.failure_location_id = dest_location return # Determine Lot ID with fallback to MO lot_id = move_line.lot_id.id if not lot_id and move.production_id: # Check for lot_producing_ids (M2M) or lot_producing_id (M2O) if hasattr(move.production_id, 'lot_producing_ids') and move.production_id.lot_producing_ids: lot_id = move.production_id.lot_producing_ids[0].id elif hasattr(move.production_id, 'lot_producing_id') and move.production_id.lot_producing_id: lot_id = move.production_id.lot_producing_id.id # Common line values preparation (template) line_vals_template = { 'product_id': move.product_id.id, 'product_uom_id': move.product_uom.id, 'location_dest_id': dest_location, 'quantity': failed_qty, 'lot_id': lot_id, 'company_id': move.company_id.id, } if move.state == 'done': source_location_id = move.location_dest_id.id new_failed_move = move.copy({ 'location_id': source_location_id, 'location_dest_id': dest_location, 'move_orig_ids': move.move_orig_ids.ids, 'product_uom_qty': failed_qty, 'state': 'draft', 'move_line_ids': [], }) new_failed_move._action_confirm() # Manual line creation for Done case line_vals = line_vals_template.copy() line_vals.update({ 'move_id': new_failed_move.id, 'location_id': source_location_id, }) failed_move_line = self.env['stock.move.line'].create(line_vals) new_failed_move.picked = True new_failed_move._action_done() else: # _split returns a list of values (dicts) vals_list = move._split(failed_qty) if vals_list: new_failed_move = self.env['stock.move'].create(vals_list) else: new_failed_move = move.copy({ 'location_dest_id': dest_location, 'product_uom_qty': failed_qty, 'state': 'draft', 'move_line_ids': [], }) # Configure and Confirm new_failed_move.location_dest_id = dest_location new_failed_move._action_confirm() # Manual Line Creation/Update (Force Lot) line_vals = line_vals_template.copy() line_vals.update({ 'move_id': new_failed_move.id, 'location_id': new_failed_move.location_id.id, }) if new_failed_move.move_line_ids: # Update existing auto-created line target_line = new_failed_move.move_line_ids[0] target_line.write(line_vals) failed_move_line = target_line else: # Create new line failed_move_line = self.env['stock.move.line'].create(line_vals) # Reduce original reservation to free up Lot # Use write to ensure persistence if move_line.quantity >= failed_qty: move_line.write({'quantity': move_line.quantity - failed_qty}) new_failed_move.picked = True new_failed_move._action_done() _logger.info(f"DEBUG: Forced new move {new_failed_move.id} to DONE state.") # Create new check for the PASSED quantity (original move line) new_check_vals = check.copy_data()[0] new_check_vals.update({ 'move_line_id': move_line.id, 'qty_tested': 0, 'quality_state': 'pass', }) new_check = self.create(new_check_vals) _logger.info(f"DEBUG: Created new_check (Passed) {new_check.id} linked to original move_line {move_line.id}") new_check.do_pass() # Update CURRENT check to point to the FAILED move line check.move_line_id = failed_move_line check.failure_location_id = dest_location check.qty_failed = failed_qty _logger.info(f"DEBUG: Updated check {check.id} (Failed) to failed_move_line {failed_move_line.id} and loc {dest_location}") # Additional Odoo 18 quality_mrp logic if check.production_id and check.move_line_id: check.move_line_id.move_id.picked = False def _move_to_failure_location(self, failure_location_id, failed_qty=None): """ Override/Patch to direct to our new logic """ if any(c._can_move_line_to_failure_location() for c in self): self._move_line_to_failure_location(failure_location_id, failed_qty) else: super()._move_to_failure_location(failure_location_id, failed_qty) def write(self, vals): import logging _logger = logging.getLogger(__name__) if 'quality_state' in vals: _logger.info(f"DEBUG: Quality Check {self.ids} state changed to {vals['quality_state']}") if 'qty_failed_manual' in vals: _logger.info(f"DEBUG: Quality Check {self.ids} qty_failed_manual updated to {vals['qty_failed_manual']}") return super().write(vals) def unlink(self): import logging _logger = logging.getLogger(__name__) for qc in self: _logger.info(f"DEBUG: Unlinking Quality Check {qc.id} - State: {qc.quality_state}, MO: {qc.production_id.id}, MoveLine: {qc.move_line_id.id}") return super().unlink() class QualityPoint(models.Model): _inherit = "quality.point" def _check_measure_on(self): # Override to allow "Item" (Control per Quantity) for Manufacturing Operations # The original constraint in quality_mrp raises UserError if measure_on == 'move_line' # and operation is manufacturing. We want to allow this. return