diff --git a/README.md b/README.md index 646eed0..773cae7 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/__manifest__.py b/__manifest__.py index 84bf004..3e3165c 100755 --- a/__manifest__.py +++ b/__manifest__.py @@ -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", diff --git a/models/stock_inventory_backdate.py b/models/stock_inventory_backdate.py index c2800df..a862d45 100755 --- a/models/stock_inventory_backdate.py +++ b/models/stock_inventory_backdate.py @@ -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 - for line in self.line_ids: - if line.difference_qty != 0: - line._create_stock_move() + # 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: + 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