refactor: implement batch processing for inventory validation and optimize account move operations
This commit is contained in:
parent
e705b821c6
commit
5062762237
@ -63,6 +63,11 @@ This module allows you to create backdated inventory adjustments with a specific
|
||||
|
||||
## Version History
|
||||
|
||||
### Version 17.0.2.1.0
|
||||
- Refactored validation logic to use batch processing for improved performance and reliability.
|
||||
- Fixed uniqueness constraint conflicts on account moves by optimizing the sequence of operations.
|
||||
- Enhanced SQL error handling for products without automated valuation.
|
||||
|
||||
### Version 17.0.2.0.0
|
||||
- Complete redesign with dedicated backdated adjustment form
|
||||
- Proper backdating of all related records
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Stock Inventory Backdate",
|
||||
"summary": "Create backdated inventory adjustments with historical position view",
|
||||
"version": "17.0.2.0.0",
|
||||
"version": "17.0.2.1.0",
|
||||
"category": "Warehouse",
|
||||
"author": "Suherdy Yacob",
|
||||
"license": "AGPL-3",
|
||||
|
||||
@ -153,7 +153,7 @@ class StockInventoryBackdate(models.Model):
|
||||
|
||||
|
||||
def action_validate(self):
|
||||
"""Validate and create backdated stock moves"""
|
||||
"""Validate and create backdated stock moves in batch"""
|
||||
self.ensure_one()
|
||||
if self.state != 'draft':
|
||||
raise UserError(_('Only draft adjustments can be validated.'))
|
||||
@ -161,14 +161,170 @@ class StockInventoryBackdate(models.Model):
|
||||
if not self.line_ids:
|
||||
raise UserError(_('Please add at least one inventory line.'))
|
||||
|
||||
# Create stock moves for each line with differences
|
||||
# Find inventory adjustment location
|
||||
inventory_location = self.env['stock.location'].search([
|
||||
('usage', '=', 'inventory'),
|
||||
('company_id', 'in', [self.company_id.id, False])
|
||||
], limit=1)
|
||||
|
||||
if not inventory_location:
|
||||
raise UserError(_('Inventory adjustment location not found. Please check your stock configuration.'))
|
||||
|
||||
moves_to_process = self.env['stock.move']
|
||||
move_line_data = [] # To store move line info linked to moves
|
||||
|
||||
# Step 1: Create all stock moves
|
||||
for line in self.line_ids:
|
||||
if line.difference_qty != 0:
|
||||
line._create_stock_move()
|
||||
if line.difference_qty == 0:
|
||||
continue
|
||||
|
||||
# Determine source and destination based on difference
|
||||
if line.difference_qty > 0:
|
||||
location_id = inventory_location.id
|
||||
location_dest_id = self.location_id.id
|
||||
qty = line.difference_qty
|
||||
else:
|
||||
location_id = self.location_id.id
|
||||
location_dest_id = inventory_location.id
|
||||
qty = abs(line.difference_qty)
|
||||
|
||||
backdate = self.backdate_datetime
|
||||
move_vals = {
|
||||
'name': _('Backdated Inventory Adjustment: %s') % self.name,
|
||||
'product_id': line.product_id.id,
|
||||
'product_uom': line.product_uom_id.id,
|
||||
'product_uom_qty': qty,
|
||||
'location_id': location_id,
|
||||
'location_dest_id': location_dest_id,
|
||||
'company_id': self.company_id.id,
|
||||
'is_inventory': True,
|
||||
'origin': self.name,
|
||||
'date': backdate,
|
||||
'picked': True, # Mark as picked for Odoo 17
|
||||
}
|
||||
|
||||
move = self.env['stock.move'].create(move_vals)
|
||||
moves_to_process |= move
|
||||
|
||||
# Store data for move line creation
|
||||
move_line_data.append({
|
||||
'move': move,
|
||||
'line': line,
|
||||
'qty': qty,
|
||||
'location_id': location_id,
|
||||
'location_dest_id': location_dest_id,
|
||||
'backdate': backdate
|
||||
})
|
||||
|
||||
if not moves_to_process:
|
||||
self.write({'state': 'done'})
|
||||
return True
|
||||
|
||||
# Step 2: Confirm moves and handle move lines
|
||||
moves_to_process._action_confirm()
|
||||
|
||||
for data in move_line_data:
|
||||
move = data['move']
|
||||
line = data['line']
|
||||
|
||||
# Check if move line was already created by _action_confirm (e.g. reservation)
|
||||
existing_ml = move.move_line_ids.filtered(lambda ml: ml.product_id.id == line.product_id.id)
|
||||
|
||||
ml_vals = {
|
||||
'product_id': line.product_id.id,
|
||||
'product_uom_id': line.product_uom_id.id,
|
||||
'quantity': data['qty'],
|
||||
'location_id': data['location_id'],
|
||||
'location_dest_id': data['location_dest_id'],
|
||||
'lot_id': line.lot_id.id if line.lot_id else False,
|
||||
'package_id': line.package_id.id if line.package_id else False,
|
||||
'owner_id': line.owner_id.id if line.owner_id else False,
|
||||
'date': data['backdate'],
|
||||
'picked': True,
|
||||
}
|
||||
|
||||
if existing_ml:
|
||||
existing_ml[0].write(ml_vals)
|
||||
else:
|
||||
ml_vals['move_id'] = move.id
|
||||
self.env['stock.move.line'].create(ml_vals)
|
||||
|
||||
# Step 3: Action Done on all moves at once
|
||||
_logger.info(f"Validating {len(moves_to_process)} moves for {self.name}")
|
||||
moves_to_process._action_done()
|
||||
|
||||
# Step 4: Post-process all moves (backdating and accounting)
|
||||
self._post_process_validated_moves(moves_to_process)
|
||||
|
||||
self.write({'state': 'done'})
|
||||
return True
|
||||
|
||||
def _post_process_validated_moves(self, moves):
|
||||
"""Handle backdating and accounting adjustments for a batch of moves"""
|
||||
self.ensure_one()
|
||||
backdate = self.backdate_datetime
|
||||
account_date = backdate.date()
|
||||
|
||||
# Flush all pending ORM operations to DB before running raw SQL
|
||||
self.env.flush_all()
|
||||
|
||||
# 1. Update stock move dates
|
||||
self.env.cr.execute(
|
||||
"UPDATE stock_move SET date = %s WHERE id IN %s",
|
||||
(backdate, tuple(moves.ids))
|
||||
)
|
||||
|
||||
# 2. Update stock move line dates
|
||||
self.env.cr.execute(
|
||||
"UPDATE stock_move_line SET date = %s WHERE move_id IN %s",
|
||||
(backdate, tuple(moves.ids))
|
||||
)
|
||||
|
||||
# 3. Update stock valuation layers
|
||||
svls = self.env['stock.valuation.layer'].search([('stock_move_id', 'in', moves.ids)])
|
||||
if svls:
|
||||
self.env.cr.execute(
|
||||
"UPDATE stock_valuation_layer SET create_date = %s WHERE id IN %s",
|
||||
(backdate, tuple(svls.ids))
|
||||
)
|
||||
|
||||
# 4. Update account moves
|
||||
account_moves = moves.account_move_ids
|
||||
if account_moves:
|
||||
# Update account move dates
|
||||
self.env.cr.execute(
|
||||
"UPDATE account_move SET date = %s WHERE id IN %s",
|
||||
(account_date, tuple(account_moves.ids))
|
||||
)
|
||||
|
||||
# Update account move line dates
|
||||
self.env.cr.execute(
|
||||
"UPDATE account_move_line SET date = %s WHERE move_id IN %s",
|
||||
(account_date, tuple(account_moves.ids))
|
||||
)
|
||||
|
||||
# Update account for non-valuation lines if target account 521301 exists
|
||||
target_account = self.env['account.account'].search([
|
||||
('code', '=', '521301'),
|
||||
('company_id', '=', self.company_id.id)
|
||||
], limit=1)
|
||||
|
||||
if target_account:
|
||||
for move in moves:
|
||||
product = move.product_id
|
||||
valuation_account = product.categ_id.property_stock_valuation_account_id
|
||||
if valuation_account and move.account_move_ids:
|
||||
# Update the line that is NOT the stock valuation account
|
||||
self.env.cr.execute(
|
||||
"""UPDATE account_move_line
|
||||
SET account_id = %s
|
||||
WHERE move_id IN %s AND account_id != %s""",
|
||||
(target_account.id, tuple(move.account_move_ids.ids), valuation_account.id)
|
||||
)
|
||||
|
||||
# Clear cache to reflect changes
|
||||
self.env.invalidate_all()
|
||||
|
||||
def action_cancel(self):
|
||||
"""Cancel the adjustment"""
|
||||
self.ensure_one()
|
||||
@ -268,195 +424,3 @@ class StockInventoryBackdateLine(models.Model):
|
||||
if not self.counted_qty and not self.difference_qty:
|
||||
self.counted_qty = self.theoretical_qty
|
||||
self.difference_qty = 0.0
|
||||
|
||||
|
||||
def _create_stock_move(self):
|
||||
"""Create backdated stock move for this line"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.difference_qty == 0:
|
||||
return
|
||||
|
||||
# Find inventory adjustment location
|
||||
inventory_location = self.env['stock.location'].search([
|
||||
('usage', '=', 'inventory'),
|
||||
('company_id', 'in', [self.inventory_id.company_id.id, False])
|
||||
], limit=1)
|
||||
|
||||
if not inventory_location:
|
||||
raise UserError(_('Inventory adjustment location not found. Please check your stock configuration.'))
|
||||
|
||||
# Determine source and destination based on difference
|
||||
if self.difference_qty > 0:
|
||||
# Increase inventory
|
||||
location_id = inventory_location.id
|
||||
location_dest_id = self.inventory_id.location_id.id
|
||||
qty = self.difference_qty
|
||||
else:
|
||||
# Decrease inventory
|
||||
location_id = self.inventory_id.location_id.id
|
||||
location_dest_id = inventory_location.id
|
||||
qty = abs(self.difference_qty)
|
||||
|
||||
# Create stock move with backdated datetime
|
||||
backdate = self.inventory_id.backdate_datetime
|
||||
move_vals = {
|
||||
'name': _('Backdated Inventory Adjustment: %s') % self.inventory_id.name,
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom': self.product_uom_id.id,
|
||||
'product_uom_qty': qty,
|
||||
'location_id': location_id,
|
||||
'location_dest_id': location_dest_id,
|
||||
'company_id': self.inventory_id.company_id.id,
|
||||
'is_inventory': True,
|
||||
'origin': self.inventory_id.name,
|
||||
'date': backdate,
|
||||
}
|
||||
|
||||
move = self.env['stock.move'].create(move_vals)
|
||||
move._action_confirm()
|
||||
|
||||
# Check if move line was already created by _action_confirm (e.g. reservation)
|
||||
move_line = move.move_line_ids.filtered(lambda ml: ml.product_id.id == self.product_id.id)
|
||||
|
||||
move_line_vals = {
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_id': self.product_uom_id.id,
|
||||
'quantity': qty,
|
||||
'location_id': location_id,
|
||||
'location_dest_id': location_dest_id,
|
||||
'lot_id': self.lot_id.id if self.lot_id else False,
|
||||
'package_id': self.package_id.id if self.package_id else False,
|
||||
'owner_id': self.owner_id.id if self.owner_id else False,
|
||||
'date': backdate,
|
||||
}
|
||||
|
||||
if move_line:
|
||||
# Update existing line
|
||||
move_line = move_line[0]
|
||||
move_line.write(move_line_vals)
|
||||
_logger.info(f"Updated existing move line {move_line.id} with quantity={qty}")
|
||||
else:
|
||||
# Create new line if none exists
|
||||
move_line_vals['move_id'] = move.id
|
||||
move_line = self.env['stock.move.line'].create(move_line_vals)
|
||||
_logger.info(f"Created new move line {move_line.id} with quantity={qty}")
|
||||
_logger.info(f"Created move line {move_line.id} with quantity_done={qty}")
|
||||
|
||||
# Log product valuation settings
|
||||
product = self.product_id
|
||||
_logger.info(f"Product: {product.name}, Category: {product.categ_id.name}")
|
||||
_logger.info(f"Valuation: {product.categ_id.property_valuation}, Cost Method: {product.categ_id.property_cost_method}")
|
||||
_logger.info(f"Product Cost: {product.standard_price}")
|
||||
|
||||
|
||||
# Mark as picked (required for Odoo 17 _action_done)
|
||||
move.picked = True
|
||||
for ml in move.move_line_ids:
|
||||
ml.picked = True
|
||||
|
||||
# Mark as done
|
||||
_logger.info(f"Move state before _action_done: {move.state}")
|
||||
result = move._action_done()
|
||||
_logger.info(f"Move state after _action_done: {move.state}")
|
||||
_logger.info(f"_action_done returned: {result}")
|
||||
|
||||
# Refresh move to get latest data
|
||||
move = self.env['stock.move'].browse(move.id)
|
||||
_logger.info(f"Move state after refresh: {move.state}")
|
||||
|
||||
# CRITICAL: Update dates via direct SQL after _action_done
|
||||
# The _action_done method overwrites dates, so we must update after
|
||||
|
||||
_logger.info(f"Backdating move {move.id} to {backdate}")
|
||||
|
||||
# Flush all pending ORM operations to DB before running raw SQL
|
||||
self.env.flush_all()
|
||||
|
||||
# Update stock move
|
||||
self.env.cr.execute(
|
||||
"UPDATE stock_move SET date = %s WHERE id = %s",
|
||||
(backdate, move.id)
|
||||
)
|
||||
_logger.info(f"Updated stock_move {move.id}, rows affected: {self.env.cr.rowcount}")
|
||||
|
||||
# Update stock move lines
|
||||
self.env.cr.execute(
|
||||
"UPDATE stock_move_line SET date = %s WHERE move_id = %s",
|
||||
(backdate, move.id)
|
||||
)
|
||||
_logger.info(f"Updated stock_move_line for move {move.id}, rows affected: {self.env.cr.rowcount}")
|
||||
|
||||
# Update stock valuation layer
|
||||
# Check if valuation layer exists
|
||||
svl = self.env['stock.valuation.layer'].search([('stock_move_id', '=', move.id)], limit=1)
|
||||
# svl_count = self.env['stock.valuation.layer'].search_count([('stock_move_id', '=', move.id)])
|
||||
_logger.info(f"Found stock valuation layer for move {move.id}: {svl}")
|
||||
|
||||
if svl:
|
||||
# just update date
|
||||
self.env.cr.execute(
|
||||
"UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s",
|
||||
(backdate, svl.id)
|
||||
)
|
||||
|
||||
_logger.info(f"Updated stock_valuation_layer for move {move.id}")
|
||||
else:
|
||||
_logger.warning(f"No stock valuation layer found for move {move.id}. Product may not use real-time valuation or cost is zero.")
|
||||
|
||||
# Update account moves dates if they exist (Run unconditionally as date fix is always needed)
|
||||
# Refresh move to get account_move_ids
|
||||
move = self.env['stock.move'].browse(move.id)
|
||||
if move.account_move_ids:
|
||||
# Find target account 521301 (Selisih Persediaan)
|
||||
target_account = self.env['account.account'].search([
|
||||
('code', '=', '521301'),
|
||||
('company_id', '=', self.inventory_id.company_id.id)
|
||||
], limit=1)
|
||||
|
||||
account_date = backdate.date()
|
||||
for account_move in move.account_move_ids:
|
||||
# Update move date
|
||||
self.env.cr.execute(
|
||||
"UPDATE account_move SET date = %s WHERE id = %s",
|
||||
(account_date, account_move.id)
|
||||
)
|
||||
|
||||
# Update lines date and account if target_account is found
|
||||
if target_account:
|
||||
# Get valuation account for this product's category to identify which line to update
|
||||
# For inventory adjustments, we want to update the non-valuation line
|
||||
valuation_account = product.categ_id.property_stock_valuation_account_id
|
||||
|
||||
if valuation_account:
|
||||
# Update the line that is NOT the stock valuation account
|
||||
self.env.cr.execute(
|
||||
"""UPDATE account_move_line
|
||||
SET date = %s, account_id = %s
|
||||
WHERE move_id = %s AND account_id != %s""",
|
||||
(account_date, target_account.id, account_move.id, valuation_account.id)
|
||||
)
|
||||
_logger.info(f"Updated account_move {account_move.id} lines with account 521301")
|
||||
else:
|
||||
# Fallback: just update dates if valuation account is not found (though it should be)
|
||||
_logger.warning(f"Valuation account not found for product {product.name} (categ_id: {product.categ_id.id}). Only updating dates.")
|
||||
self.env.cr.execute(
|
||||
"UPDATE account_move_line SET date = %s WHERE move_id = %s",
|
||||
(account_date, account_move.id)
|
||||
)
|
||||
else:
|
||||
# No target account found, just update dates
|
||||
_logger.warning("Target account 521301 not found. Only updating dates for account move lines.")
|
||||
self.env.cr.execute(
|
||||
"UPDATE account_move_line SET date = %s WHERE move_id = %s",
|
||||
(account_date, account_move.id)
|
||||
)
|
||||
_logger.info(f"Updated account_move {account_move.id} and lines to {account_date}")
|
||||
|
||||
# Invalidate cache to ensure ORM reloads data from DB
|
||||
self.env.invalidate_all()
|
||||
|
||||
# Invalidate cache
|
||||
self.env.cache.invalidate()
|
||||
|
||||
return move
|
||||
|
||||
Loading…
Reference in New Issue
Block a user