quality_patch/models/quality.py

275 lines
13 KiB
Python

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