first commit
This commit is contained in:
commit
4a44ffa1fe
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.bak
|
||||
29
README.md
Normal file
29
README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Quality Patch
|
||||
|
||||
This module addresses specific issues encountered in Odoo 19 Quality Management.
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
1. **MRP Quality Check AttributeError**: Fixes `AttributeError: 'mrp.production' object has no attribute 'lot_producing_id'` by adding a **compatibility layer** to the `mrp.production` model. It provides a computed `lot_producing_id` field (alias to `lot_producing_ids[:1]`), satisfying all Odoo 18-style callers.
|
||||
2. **Partial Failure / Quantity Discrepancy & Reversion**: Fixes issues where partial failure quantities (e.g., 20,000) reverted to full production quantities (e.g., 220,000) after MO completion.
|
||||
- **Persistent Input Storage**: Introduces a new field `qty_failed_manual` that permanently stores your wizard input.
|
||||
- **Recompute Protection**: The system now strictly prioritizes this manual value over any automatic recomputations, ensuring reports remain accurate.
|
||||
- **Popup Capture**: Overrides the wizard's confirmation logic to ensure the specific quantity entered in the failure popup is captured correctly.
|
||||
3. **MO Closing Error (Price Difference)**: Fixes a `ValueError` during MO closing when a product has multiple split moves (common after partial quality failures). Overrides `_cal_price` on `stock.move` to handle multiple finished moves.
|
||||
6. **"Control per Quantity" on MOs**: Removes the standard restriction that prevented selecting "Control per Quantity" for Manufacturing Operations, allowing for the partial failure workflow to be used.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Ensure this module is in your Odoo `addons` path.
|
||||
2. Update the Apps List in Odoo.
|
||||
3. Install or **Upgrade** `quality_patch`.
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Model Compatibility**: Extends `mrp.production` to provide `lot_producing_id`.
|
||||
- **Inheritance**: Extends `quality.check.wizard` (Transient Model) and `quality.check`.
|
||||
- **Views**: Extends `quality_control.quality_check_wizard_form_failure` via XPath.
|
||||
- **Dependencies**: `quality_mrp`, `quality_control`, `mrp`.
|
||||
|
||||
---
|
||||
*Created by Antigravity AI Assistant*
|
||||
2
__init__.py
Normal file
2
__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
20
__manifest__.py
Normal file
20
__manifest__.py
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
'name': 'Quality Patch',
|
||||
'version': '1.0',
|
||||
'category': 'Quality Management',
|
||||
'summary': 'Patch for Odoo 19 Quality Check issues',
|
||||
'description': """
|
||||
Fixes:
|
||||
1. lot_producing_id AttributeError in quality_mrp
|
||||
2. Fail location visibility in quality_control
|
||||
""",
|
||||
'author': 'Antigravity',
|
||||
'depends': ['quality_mrp', 'quality_control', 'mrp'],
|
||||
'data': [
|
||||
'views/quality_check_views.xml',
|
||||
'wizard/quality_check_wizard_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'OEEL-1',
|
||||
}
|
||||
2
models/__init__.py
Normal file
2
models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import mrp_production
|
||||
from . import quality
|
||||
70
models/mrp_production.py
Normal file
70
models/mrp_production.py
Normal file
@ -0,0 +1,70 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
|
||||
lot_producing_id = fields.Many2one(
|
||||
'stock.lot',
|
||||
compute='_compute_lot_producing_id',
|
||||
string='Producing Lot (v18 Compatible)',
|
||||
help="Compatibility field for Odoo 18 modules using lot_producing_id"
|
||||
)
|
||||
|
||||
@api.depends('lot_producing_ids')
|
||||
def _compute_lot_producing_id(self):
|
||||
for production in self:
|
||||
production.lot_producing_id = production.lot_producing_ids[:1]
|
||||
|
||||
def _cal_price(self, consumed_moves):
|
||||
""" Patch to handle multiple finished moves for the same product.
|
||||
Odoo's mrp_account._cal_price uses finished_move.ensure_one(), which crashes
|
||||
if quality checks split the finished move into multiple lines (e.g. Pass/Fail).
|
||||
"""
|
||||
# We only apply this logic if there are multiple finished moves for the MO product
|
||||
finished_moves = self.move_finished_ids.filtered(
|
||||
lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel') and x.quantity > 0)
|
||||
|
||||
if len(finished_moves) > 1:
|
||||
# If we have multiple moves, we need to bypass the strict ensure_one() in the parent
|
||||
# while still ensuring the cost is calculated correctly.
|
||||
# We call super for EACH move separately by temporarily filtering move_finished_ids
|
||||
# but that's complex since _cal_price is usually called on the whole MO.
|
||||
|
||||
# Alternative: Since we can't easily change how the super() code reaches finished_move,
|
||||
# we handle the multi-move case here and return True to bypass the crash in super().
|
||||
# (Note: Odoo's mrp_account._cal_price starts with super()._cal_price(consumed_moves))
|
||||
|
||||
# The super() call in mrp_account._cal_price is:
|
||||
# res = super()._cal_price(consumed_moves)
|
||||
# which usually just calculates the price on the move if it's NOT fifo/avco.
|
||||
|
||||
# Let's try to mimic the logic but for multiple moves.
|
||||
work_center_cost = 0
|
||||
for work_order in self.workorder_ids:
|
||||
work_center_cost += work_order._cal_cost()
|
||||
|
||||
total_quantity = sum(m.product_uom._compute_quantity(m.quantity, m.product_id.uom_id) for m in finished_moves)
|
||||
if total_quantity == 0:
|
||||
return super()._cal_price(consumed_moves)
|
||||
|
||||
total_cost = sum(move.value for move in consumed_moves) + work_center_cost + (self.extra_cost * total_quantity)
|
||||
|
||||
byproduct_moves = self.move_byproduct_ids.filtered(lambda m: m.state not in ('done', 'cancel') and m.quantity > 0)
|
||||
byproduct_cost_share = sum(byproduct_moves.mapped('cost_share'))
|
||||
|
||||
# Update price_unit for each finished move
|
||||
shared_price_unit = (total_cost * (1 - byproduct_cost_share / 100)) / total_quantity
|
||||
for move in finished_moves:
|
||||
move.price_unit = shared_price_unit
|
||||
|
||||
# Also handle byproducts if any
|
||||
for byproduct in byproduct_moves:
|
||||
if byproduct.cost_share > 0 and byproduct.product_id.cost_method in ('fifo', 'average'):
|
||||
byproduct_qty = byproduct.product_uom._compute_quantity(byproduct.quantity, byproduct.product_id.uom_id)
|
||||
if byproduct_qty > 0:
|
||||
byproduct.price_unit = (total_cost * byproduct.cost_share / 100) / byproduct_qty
|
||||
|
||||
return True
|
||||
|
||||
return super()._cal_price(consumed_moves)
|
||||
305
models/quality.py
Normal file
305
models/quality.py
Normal file
@ -0,0 +1,305 @@
|
||||
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
|
||||
13
views/quality_check_views.xml
Normal file
13
views/quality_check_views.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="quality_check_view_form_inherit_debug" model="ir.ui.view">
|
||||
<field name="name">quality.check.view.form.inherit.debug</field>
|
||||
<field name="model">quality.check</field>
|
||||
<field name="inherit_id" ref="quality_control.quality_check_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='failure_location_id']" position="before">
|
||||
<field name="qty_failed_manual" readonly="0" invisible="quality_state != 'fail'"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1
wizard/__init__.py
Normal file
1
wizard/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import quality_check_wizard
|
||||
70
wizard/quality_check_wizard.py
Normal file
70
wizard/quality_check_wizard.py
Normal file
@ -0,0 +1,70 @@
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class QualityCheckWizard(models.TransientModel):
|
||||
_inherit = 'quality.check.wizard'
|
||||
|
||||
# The AttributeError fix is now handled by the compatibility field in models/mrp_production.py
|
||||
|
||||
@api.onchange('qty_failed')
|
||||
def onchange_qty_failed(self):
|
||||
""" Disable enterprise quality_mrp behavior that resets qty_failed to qty_line """
|
||||
pass
|
||||
|
||||
def do_fail(self):
|
||||
""" Ensure move_line_id is populated for MO checks with Quantity control.
|
||||
This is required for quality_control's _move_to_failure_location to split
|
||||
the stock move correctly.
|
||||
"""
|
||||
check = self.current_check_id
|
||||
# We mimic the enterprise/quality_mrp behavior but in our patch
|
||||
if check.production_id and check.point_id.measure_on == 'move_line' and not check.move_line_id:
|
||||
# Try to find a finished move line for this product
|
||||
move_line = check.production_id.finished_move_line_ids.filtered(
|
||||
lambda ml: ml.product_id == (check.product_id or self.product_id)
|
||||
)[:1]
|
||||
if move_line:
|
||||
check.move_line_id = move_line
|
||||
# Also set lot if available
|
||||
if not check.lot_line_id and check.production_id.lot_producing_ids:
|
||||
check.lot_line_id = check.production_id.lot_producing_ids[0]
|
||||
|
||||
return super().do_fail()
|
||||
|
||||
def confirm_fail(self):
|
||||
""" Override to capture the quantity from the failure wizard popup """
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info(f"DEBUG: confirm_fail called. qty_failed from wizard: {self.qty_failed}")
|
||||
|
||||
if self.qty_failed > 0:
|
||||
self.current_check_id.qty_failed_manual = self.qty_failed
|
||||
_logger.info(f"DEBUG: Saved qty_failed_manual: {self.current_check_id.qty_failed_manual} to check {self.current_check_id.id}")
|
||||
|
||||
# Odoo 18 Wizard logic:
|
||||
# self.current_check_id.do_fail()
|
||||
# if self.measure_on == 'move_line':
|
||||
# self.current_check_id._move_line_to_failure_location(self.failure_location_id.id, self.qty_failed)
|
||||
|
||||
# We replicate this but with our relaxed condition:
|
||||
self.current_check_id.do_fail()
|
||||
|
||||
# Check if we should split the move line.
|
||||
# We allow it if strictly defined as move_line check OR if a manual fail qty is provided (relaxed logic)
|
||||
if self.current_check_id.point_id.measure_on == 'move_line' or self.qty_failed > 0:
|
||||
_logger.info(f"DEBUG: Triggering _move_line_to_failure_location for check {self.current_check_id.id}")
|
||||
# Use our ported method
|
||||
self.current_check_id._move_line_to_failure_location(self.failure_location_id.id, self.qty_failed)
|
||||
else:
|
||||
_logger.info(f"DEBUG: Skipping _move_line_to_failure_location. measure_on={self.current_check_id.point_id.measure_on}, qty_failed={self.qty_failed}")
|
||||
|
||||
return self.action_generate_next_window()
|
||||
|
||||
def show_failure_message(self):
|
||||
""" Initialize qty_failed with tested quantity or line quantity """
|
||||
res = super().show_failure_message()
|
||||
# If the user has already tested a specific quantity, that's likely the one they found
|
||||
# failed if it's a pass/fail check on a quantity.
|
||||
if self.qty_tested and self.qty_tested < self.qty_line:
|
||||
self.qty_failed = self.qty_tested
|
||||
return res
|
||||
20
wizard/quality_check_wizard_views.xml
Normal file
20
wizard/quality_check_wizard_views.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="quality_check_wizard_form_failure_inherit" model="ir.ui.view">
|
||||
<field name="name">quality.check.wizard.form.failure.inherit</field>
|
||||
<field name="model">quality.check.wizard</field>
|
||||
<field name="inherit_id" ref="quality_control.quality_check_wizard_form_failure"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[1]/group[1]" position="attributes">
|
||||
<attribute name="invisible">measure_on != 'move_line' and not potential_failure_location_ids and not context.get('from_failure_form')</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='failure_location_id']" position="after">
|
||||
<field name="failure_location_id"
|
||||
invisible="potential_failure_location_ids"
|
||||
domain="[('usage', '=', 'internal')]"
|
||||
groups="stock.group_stock_multi_locations"
|
||||
string="Failure Location (Manual)"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user