feat: Improve lot generator quantity calculation and UOM conversion, and add related unit tests.
This commit is contained in:
parent
816bd3bd71
commit
deef4b357e
@ -168,6 +168,48 @@ class StockMove(models.Model):
|
||||
fake_first = 'SEQDUMMY-1'
|
||||
vals_list = super().action_generate_lot_line_vals(context, mode, fake_first, count, lot_text)
|
||||
|
||||
# Fix UOM issue: if Move UOM is different from Product UOM,
|
||||
# the default generation might use Base UOM but with Move's quantity value
|
||||
move = False
|
||||
|
||||
# 1. Try finding move via active_id
|
||||
if context.get('active_id'):
|
||||
candidate = self.env['stock.move'].browse(context.get('active_id'))
|
||||
if candidate.exists() and candidate.product_id.id == product_id:
|
||||
move = candidate
|
||||
|
||||
# 2. If not found, try finding via context parameters (passed by JS widget)
|
||||
if not move and product_id:
|
||||
domain = [
|
||||
('product_id', '=', product_id),
|
||||
('state', 'not in', ['done', 'cancel']),
|
||||
]
|
||||
if context.get('default_location_id'):
|
||||
domain.append(('location_id', '=', context.get('default_location_id')))
|
||||
if context.get('default_location_dest_id'):
|
||||
domain.append(('location_dest_id', '=', context.get('default_location_dest_id')))
|
||||
if context.get('default_picking_id'):
|
||||
domain.append(('picking_id', '=', context.get('default_picking_id')))
|
||||
elif context.get('active_model') == 'stock.picking' and context.get('active_id'):
|
||||
domain.append(('picking_id', '=', context.get('active_id')))
|
||||
|
||||
moves = self.env['stock.move'].search(domain, limit=1)
|
||||
if moves:
|
||||
move = moves[0]
|
||||
|
||||
try:
|
||||
if move:
|
||||
move_uom = move.product_uom
|
||||
base_uom = move.product_id.uom_id
|
||||
if move_uom and base_uom and move_uom != base_uom:
|
||||
# Update all generate lines to use the move's UOM
|
||||
# assuming reasonable consistency
|
||||
for vals in vals_list:
|
||||
# Pass as tuple (id, name) so frontend displays it correctly immediately
|
||||
vals['product_uom_id'] = (move_uom.id, move_uom.display_name)
|
||||
except Exception as e:
|
||||
_logger.error(f"Error fixing UOM in product_lot_sequence_per_product: {e}")
|
||||
|
||||
# Overwrite the lot_name with sequence-based names
|
||||
for vals, name in zip(vals_list, generated_names):
|
||||
vals['lot_name'] = name
|
||||
|
||||
@ -54,12 +54,51 @@ class StockMoveLine(models.Model):
|
||||
# Use safe field access for quantity fields (field names may vary in different Odoo versions)
|
||||
MoveLine = self.env['stock.move.line']
|
||||
quantity_value = 1.0 # default fallback
|
||||
src_uom = self.product_uom_id
|
||||
|
||||
# Try different quantity field names
|
||||
for field_name in ['reserved_uom_qty', 'product_qty', 'product_uom_qty']:
|
||||
if field_name in MoveLine._fields and hasattr(self, field_name):
|
||||
quantity_value = getattr(self, field_name, 1.0) or 1.0
|
||||
break
|
||||
# Try to get quantity from the line first
|
||||
if 'quantity' in MoveLine._fields and self.quantity:
|
||||
quantity_value = self.quantity
|
||||
src_uom = self.product_uom_id
|
||||
elif 'qty_done' in MoveLine._fields and self.qty_done:
|
||||
quantity_value = self.qty_done
|
||||
src_uom = self.product_uom_id
|
||||
elif 'reserved_uom_qty' in MoveLine._fields and getattr(self, 'reserved_uom_qty', 0):
|
||||
quantity_value = self.reserved_uom_qty
|
||||
src_uom = self.product_uom_id
|
||||
elif 'quantity_product_uom' in MoveLine._fields and getattr(self, 'quantity_product_uom', 0):
|
||||
quantity_value = self.quantity_product_uom
|
||||
src_uom = self.product_uom_id
|
||||
elif 'product_qty' in MoveLine._fields and getattr(self, 'product_qty', 0):
|
||||
quantity_value = self.product_qty
|
||||
src_uom = self.product_id.uom_id
|
||||
elif 'product_uom_qty' in MoveLine._fields and getattr(self, 'product_uom_qty', 0):
|
||||
quantity_value = self.product_uom_qty
|
||||
src_uom = self.product_uom_id
|
||||
else:
|
||||
quantity_value = 0.0
|
||||
src_uom = self.product_uom_id
|
||||
|
||||
# If a quantity was found on the line, assume it is in src_uom
|
||||
if quantity_value > 0:
|
||||
# Convert to Move UOM if needed
|
||||
if self.move_id and self.move_id.product_uom and src_uom and src_uom != self.move_id.product_uom:
|
||||
quantity_value = src_uom._compute_quantity(quantity_value, self.move_id.product_uom)
|
||||
else:
|
||||
# Fallback: Line has no quantity (e.g. new line)
|
||||
# Use Move's remaining demand or default to 1.0 (Move UOM)
|
||||
if self.move_id:
|
||||
# Try to fill with remaining demand
|
||||
# If quantity_done is available on move
|
||||
done = getattr(self.move_id, 'quantity_done', 0.0) or getattr(self.move_id, 'quantity', 0.0)
|
||||
demand = self.move_id.product_uom_qty
|
||||
remaining = max(demand - done, 0.0)
|
||||
quantity_value = remaining if remaining > 0 else 1.0
|
||||
else:
|
||||
quantity_value = 1.0
|
||||
|
||||
# Since we pulled from Move (or assumed 1 unit of Move), the value is already in Move UOM
|
||||
pass
|
||||
|
||||
return {
|
||||
'name': 'Generate Lots for Move Line',
|
||||
|
||||
105
tests/test_uom_conversion.py
Normal file
105
tests/test_uom_conversion.py
Normal file
@ -0,0 +1,105 @@
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
class TestUOMConversion(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.uom_kg = self.env.ref('uom.product_uom_kgm')
|
||||
self.uom_gram = self.env.ref('uom.product_uom_gram')
|
||||
|
||||
self.product = self.env['product.product'].create({
|
||||
'name': 'Test Product Grams',
|
||||
'type': 'product',
|
||||
'uom_id': self.uom_gram.id,
|
||||
'uom_po_id': self.uom_gram.id,
|
||||
'tracking': 'lot',
|
||||
})
|
||||
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.customer_location = self.env.ref('stock.stock_location_customers')
|
||||
|
||||
self.picking_type = self.env.ref('stock.picking_type_out')
|
||||
|
||||
def test_uom_conversion_kg_to_gram(self):
|
||||
# Move in Grams
|
||||
move = self.env['stock.move'].create({
|
||||
'name': 'Test Move',
|
||||
'product_id': self.product.id,
|
||||
'product_uom': self.uom_gram.id,
|
||||
'product_uom_qty': 1000,
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
})
|
||||
move._action_confirm()
|
||||
|
||||
# Line in KG (Detailed Operation)
|
||||
# 1 KG = 1000 Grams
|
||||
# Note: Using qty_done/quantity depending on version.
|
||||
# API usually accepts qty_done in older and quantity in newer.
|
||||
# We try to set both or flexible.
|
||||
line_vals = {
|
||||
'move_id': move.id,
|
||||
'product_id': self.product.id,
|
||||
'product_uom_id': self.uom_kg.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
}
|
||||
|
||||
# Odoo 17+ uses quantity. Older uses qty_done.
|
||||
# We can try to assume the environment supports 'quantity' if we are in 18.
|
||||
# Or just pass 'qty_done' as it is often aliased.
|
||||
# However, checking the module under test, it checks both.
|
||||
# Let's set 'qty_done' for creation as it's safer for 'create' in many versions
|
||||
# unless 'quantity' is strictly required.
|
||||
line_vals['qty_done'] = 1
|
||||
|
||||
line = self.env['stock.move.line'].create(line_vals)
|
||||
|
||||
# Ensure we have quantity set effectively
|
||||
if hasattr(line, 'quantity') and line.quantity == 0:
|
||||
line.quantity = 1
|
||||
|
||||
# Action
|
||||
action = line.action_open_lot_generator()
|
||||
ctx = action.get('context', {})
|
||||
default_qty = ctx.get('default_quantity')
|
||||
|
||||
# Expect 1000 Grams (because Move is in Grams)
|
||||
self.assertEqual(default_qty, 1000.0, "Should convert 1 KG to 1000 Grams")
|
||||
|
||||
def test_uom_conversion_gram_to_kg(self):
|
||||
# Move in KG
|
||||
move = self.env['stock.move'].create({
|
||||
'name': 'Test Move KG',
|
||||
'product_id': self.product.id,
|
||||
'product_uom': self.uom_kg.id,
|
||||
'product_uom_qty': 1,
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
})
|
||||
move._action_confirm()
|
||||
|
||||
# Line in Grams (Detailed Operation)
|
||||
# 1000 Grams = 1 KG
|
||||
line_vals = {
|
||||
'move_id': move.id,
|
||||
'product_id': self.product.id,
|
||||
'product_uom_id': self.uom_gram.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
}
|
||||
line_vals['qty_done'] = 1000
|
||||
|
||||
line = self.env['stock.move.line'].create(line_vals)
|
||||
|
||||
if hasattr(line, 'quantity') and line.quantity == 0:
|
||||
line.quantity = 1000
|
||||
|
||||
# Action
|
||||
action = line.action_open_lot_generator()
|
||||
ctx = action.get('context', {})
|
||||
default_qty = ctx.get('default_quantity')
|
||||
|
||||
# Expect 1 KG (because Move is in KG)
|
||||
# 1000 Grams -> 1 KG
|
||||
self.assertEqual(default_qty, 1.0, "Should convert 1000 Grams to 1 KG")
|
||||
@ -78,26 +78,35 @@ class SubcontractLotGenerator(models.TransientModel):
|
||||
|
||||
lots = Lot.create(lot_vals_list)
|
||||
|
||||
# Create move lines
|
||||
# Calculate quantity in Product's Base UOM
|
||||
# self.quantity is in self.move_id.product_uom (or whatever was passed)
|
||||
base_uom = self.product_id.uom_id
|
||||
move_uom = self.move_id.product_uom
|
||||
|
||||
total_base_qty = self.quantity
|
||||
if move_uom and base_uom and move_uom != base_uom:
|
||||
total_base_qty = move_uom._compute_quantity(self.quantity, base_uom)
|
||||
|
||||
if self.tracking == 'serial':
|
||||
# One move line per lot for serial tracking
|
||||
# One move line per lot for serial tracking, usually 1 unit of base UOM per serial
|
||||
qty_per_lot = 1.0
|
||||
else:
|
||||
# Distribute quantity across lots for lot tracking
|
||||
qty_per_lot = self.quantity / self.lot_count
|
||||
|
||||
qty_per_lot = total_base_qty / self.lot_count
|
||||
|
||||
for lot in lots:
|
||||
self.env['stock.move.line'].create({
|
||||
vals = {
|
||||
'move_id': self.move_id.id,
|
||||
'product_id': self.product_id.id,
|
||||
'lot_id': lot.id,
|
||||
'product_uom_qty': qty_per_lot,
|
||||
'qty_done': 0.0,
|
||||
'product_uom_id': self.move_id.product_uom.id,
|
||||
'product_uom_id': base_uom.id,
|
||||
'location_id': self.move_id.location_id.id,
|
||||
'location_dest_id': self.move_id.location_dest_id.id,
|
||||
'picking_id': self.picking_id.id,
|
||||
})
|
||||
}
|
||||
self.env['stock.move.line'].create(vals)
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user