306 lines
15 KiB
Python
306 lines
15 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
|
|
|
|
# Partial Failure Case (Split)
|
|
_logger.info(f"DEBUG: Partial failure split for check {check.id}")
|
|
# 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
|
|
_logger.info(f"DEBUG: Found lot {lot_id} from MO lot_producing_ids")
|
|
elif hasattr(move.production_id, 'lot_producing_id') and move.production_id.lot_producing_id:
|
|
lot_id = move.production_id.lot_producing_id.id
|
|
_logger.info(f"DEBUG: Found lot {lot_id} from MO lot_producing_id")
|
|
|
|
if not lot_id:
|
|
_logger.warning(f"DEBUG: No Lot ID found for Move {move.id} / Line {move_line.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':
|
|
_logger.info(f"DEBUG: Move {move.id} is DONE. Creating new move from Stock -> Reject")
|
|
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)
|
|
_logger.info(f"DEBUG: Manually created line {failed_move_line.id} with lot {lot_id}")
|
|
|
|
new_failed_move.picked = True
|
|
new_failed_move._action_done()
|
|
|
|
else:
|
|
_logger.info(f"DEBUG: Move {move.id} is ASSIGNED/CONFIRMED. Using _split")
|
|
|
|
# _split returns a list of values (dicts)
|
|
vals_list = move._split(failed_qty)
|
|
_logger.info(f"DEBUG: _split returned vals count: {len(vals_list)}")
|
|
|
|
if vals_list:
|
|
new_failed_move = self.env['stock.move'].create(vals_list)
|
|
_logger.info(f"DEBUG: Created split move {new_failed_move.id}. State: {new_failed_move.state}")
|
|
else:
|
|
_logger.warning("DEBUG: _split returned empty! Fallback to copy.")
|
|
new_failed_move = move.copy({
|
|
'location_dest_id': dest_location,
|
|
'product_uom_qty': failed_qty,
|
|
'state': 'draft',
|
|
'move_line_ids': [],
|
|
})
|
|
_logger.info(f"DEBUG: Fallback created move {new_failed_move.id}")
|
|
|
|
# Configure and Confirm
|
|
new_failed_move.location_dest_id = dest_location
|
|
new_failed_move._action_confirm()
|
|
_logger.info(f"DEBUG: Confirmed new move {new_failed_move.id}. State: {new_failed_move.state}")
|
|
|
|
# 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,
|
|
})
|
|
|
|
_logger.info(f"DEBUG: Line Vals: {line_vals}")
|
|
|
|
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
|
|
_logger.info(f"DEBUG: Updated existing line {target_line.id} with Lot {lot_id}")
|
|
else:
|
|
# Create new line
|
|
failed_move_line = self.env['stock.move.line'].create(line_vals)
|
|
_logger.info(f"DEBUG: Manually created line {failed_move_line.id} with Lot {lot_id}")
|
|
|
|
# Reduce original reservation to free up Lot
|
|
# Important: Do this AFTER Confirming new move but BEFORE Done?
|
|
# Or maybe before creating new line?
|
|
# Actually, if we reduce quantity on original line, it releases reservation.
|
|
if move_line.quantity >= failed_qty:
|
|
_logger.info(f"DEBUG: Reducing original line {move_line.id} qty from {move_line.quantity} by {failed_qty}")
|
|
move_line.quantity -= failed_qty
|
|
|
|
new_failed_move.picked = True
|
|
try:
|
|
new_failed_move._action_done()
|
|
_logger.info(f"DEBUG: Forced new move {new_failed_move.id} to DONE state.")
|
|
except Exception as e:
|
|
_logger.error(f"DEBUG: Failed to force Done on {new_failed_move.id}. Error: {e}")
|
|
# Log line details for debugging
|
|
for ml in new_failed_move.move_line_ids:
|
|
_logger.info(f"DEBUG: Line {ml.id}: Lot={ml.lot_id.name}, Qty={ml.quantity}, Loc={ml.location_id.name}->{ml.location_dest_id.name}")
|
|
raise e
|
|
|
|
_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
|