diff --git a/README.md b/README.md new file mode 100644 index 0000000..55844bf --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Stock Inventory Backdate + +## Overview + +This module allows you to create backdated inventory adjustments with a specific date and time. Unlike the standard Odoo inventory adjustment, this module creates stock moves with the exact backdated datetime you specify. + +## Features + +- **Dedicated Backdated Adjustment Form**: Separate interface for creating backdated inventory adjustments +- **Historical Inventory Position**: View the theoretical inventory quantity at the backdated time +- **Full Datetime Support**: Specify both date and time for the adjustment +- **Proper Backdating**: All related records (stock moves, move lines, valuation layers, and account moves) are backdated correctly +- **Multi-line Support**: Adjust multiple products in a single backdated adjustment +- **Lot/Serial Number Support**: Handle products with lot/serial number tracking +- **Package & Owner Support**: Support for advanced tracking features + +## Usage + +### Creating a Backdated Inventory Adjustment + +1. Go to **Inventory > Operations > Backdated Adjustments** +2. Click **Create** +3. Set the **Adjustment Date & Time** to the desired backdate +4. Select the **Location** where you want to adjust inventory + +**Option A: Load All Products** +5. Click **Load Products** to automatically load all products with inventory at that location + - The system will calculate the theoretical quantity at the backdated time for each product +6. Adjust the **Counted Quantity** for each product as needed +7. The **Difference** column shows what will be adjusted +8. Click **Validate** to create the backdated stock moves + +**Option B: Add Individual Products** +5. In the **Inventory Lines** tab, click **Add a line** +6. Select the **Product** you want to adjust + - The system will automatically calculate the **Theoretical Quantity** at the backdated time +7. Enter the **Counted Quantity** + - The **Difference** is calculated automatically +8. Add more products as needed +9. Click **Validate** to create the backdated stock moves + +## Technical Details + +### How It Works + +1. When you validate a backdated adjustment, the module creates standard stock moves +2. After the moves are processed, it updates the dates via SQL to ensure proper backdating: + - Stock move `date` field + - Stock move line `date` field + - Stock valuation layer `create_date` field + - Account move `date` field (if real-time valuation is enabled) + +### Models + +- **stock.inventory.backdate**: Main model for backdated adjustments +- **stock.inventory.backdate.line**: Individual product lines in the adjustment + +### Security + +- Stock Users can create, read, update, and delete backdated adjustments +- Stock Managers have full access + +## Version History + +### Version 17.0.2.0.0 +- Complete redesign with dedicated backdated adjustment form +- Proper backdating of all related records +- Historical inventory position calculation +- Support for lot/serial numbers, packages, and owners + +### Version 17.0.1.1.0 +- Changed fields from Date to Datetime +- Added time support to inventory adjustments + +### Version 17.0.1.0.0 +- Initial release +- Basic backdating functionality + +## Author + +Suherdy Yacob + +## License + +AGPL-3 diff --git a/__manifest__.py b/__manifest__.py index 86f658d..84bf004 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,14 +1,15 @@ { "name": "Stock Inventory Backdate", - "summary": "Allow backdating of physical stock adjustments and valuations with date and time.", - "version": "17.0.1.1.0", + "summary": "Create backdated inventory adjustments with historical position view", + "version": "17.0.2.0.0", "category": "Warehouse", "author": "Suherdy Yacob", "license": "AGPL-3", - "depends": ["stock_account"], + "depends": ["stock_account", "mail"], "data": [ "security/ir.model.access.csv", - "views/stock_quant_views.xml", + "data/sequence_data.xml", + "views/stock_inventory_backdate_views.xml", ], "installable": True, } diff --git a/data/sequence_data.xml b/data/sequence_data.xml new file mode 100644 index 0000000..d477db4 --- /dev/null +++ b/data/sequence_data.xml @@ -0,0 +1,13 @@ + + + + + + Backdated Inventory Adjustment + stock.inventory.backdate + BIA/ + 5 + + + + diff --git a/models/__init__.py b/models/__init__.py index c1e37b5..4b33b79 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,2 +1 @@ -from . import stock_quant -from . import stock_move +from . import stock_inventory_backdate diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc index 58b324f..8649ec7 100644 Binary files a/models/__pycache__/__init__.cpython-312.pyc and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/stock_move.cpython-312.pyc b/models/__pycache__/stock_move.cpython-312.pyc deleted file mode 100644 index 12a09d5..0000000 Binary files a/models/__pycache__/stock_move.cpython-312.pyc and /dev/null differ diff --git a/models/__pycache__/stock_quant.cpython-312.pyc b/models/__pycache__/stock_quant.cpython-312.pyc deleted file mode 100644 index a1745bf..0000000 Binary files a/models/__pycache__/stock_quant.cpython-312.pyc and /dev/null differ diff --git a/models/stock_inventory_backdate.py b/models/stock_inventory_backdate.py new file mode 100644 index 0000000..297c78e --- /dev/null +++ b/models/stock_inventory_backdate.py @@ -0,0 +1,421 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +import logging + +_logger = logging.getLogger(__name__) + + +class StockInventoryBackdate(models.Model): + _name = 'stock.inventory.backdate' + _description = 'Backdated Inventory Adjustment' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'backdate_datetime desc, id desc' + + name = fields.Char(string='Reference', required=True, default='New', readonly=True, tracking=True) + backdate_datetime = fields.Datetime( + string='Adjustment Date & Time', + required=True, + default=fields.Datetime.now, + help="The date and time for the backdated inventory adjustment", + tracking=True + ) + location_id = fields.Many2one( + 'stock.location', + string='Location', + required=True, + domain="[('usage', '=', 'internal')]", + tracking=True + ) + company_id = fields.Many2one( + 'res.company', + string='Company', + required=True, + default=lambda self: self.env.company + ) + state = fields.Selection([ + ('draft', 'Draft'), + ('done', 'Done'), + ('cancel', 'Cancelled') + ], string='Status', default='draft', readonly=True, tracking=True) + line_ids = fields.One2many( + 'stock.inventory.backdate.line', + 'inventory_id', + string='Inventory Lines' + ) + notes = fields.Text(string='Notes') + + @api.model + def create(self, vals): + if vals.get('name', 'New') == 'New': + vals['name'] = self.env['ir.sequence'].next_by_code('stock.inventory.backdate') or 'New' + return super(StockInventoryBackdate, self).create(vals) + + def action_load_products(self): + """Load products with current inventory at the location""" + self.ensure_one() + if self.state != 'draft': + raise UserError(_('You can only load products in draft state.')) + + if not self.location_id: + raise UserError(_('Please select a location first.')) + + # Get all quants at this location + quants = self.env['stock.quant'].search([ + ('location_id', '=', self.location_id.id), + ('company_id', '=', self.company_id.id), + ('quantity', '!=', 0) + ]) + + if not quants: + raise UserError(_('No products found at this location. You can add products manually.')) + + # Get existing product IDs to avoid duplicates + existing_product_ids = self.line_ids.mapped('product_id').ids + + lines = [] + for quant in quants: + # Skip if already in lines + if quant.product_id.id in existing_product_ids: + continue + + # Get inventory position at the backdate + historical_qty = self._get_historical_quantity( + quant.product_id, + quant.location_id, + quant.lot_id, + quant.package_id, + quant.owner_id, + self.backdate_datetime + ) + + lines.append((0, 0, { + 'product_id': quant.product_id.id, + 'lot_id': quant.lot_id.id, + 'package_id': quant.package_id.id, + 'owner_id': quant.owner_id.id, + 'theoretical_qty': historical_qty, + 'counted_qty': historical_qty, + 'difference_qty': 0.0, + })) + + if lines: + self.line_ids = [(0, 0, line[2]) for line in lines] + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Products Loaded'), + 'message': _('%s product(s) loaded successfully.') % len(lines), + 'type': 'success', + 'sticky': False, + } + } + else: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('No New Products'), + 'message': _('All products are already in the list.'), + 'type': 'info', + 'sticky': False, + } + } + + def _get_historical_quantity(self, product, location, lot, package, owner, date): + """Calculate inventory quantity at a specific date""" + base_domain = [ + ('product_id', '=', product.id), + ('state', '=', 'done'), + ('date', '<=', date), + ] + if lot: + base_domain.append(('lot_id', '=', lot.id)) + if package: + base_domain.append(('package_id', '=', package.id)) + if owner: + base_domain.append(('owner_id', '=', owner.id)) + + # Get all incoming moves (destination = our location) + domain_in = base_domain + [('location_dest_id', '=', location.id)] + moves_in = self.env['stock.move.line'].search(domain_in) + + # Get all outgoing moves (source = our location) + domain_out = base_domain + [('location_id', '=', location.id)] + moves_out = self.env['stock.move.line'].search(domain_out) + + qty_in = sum(moves_in.mapped('quantity')) + qty_out = sum(moves_out.mapped('quantity')) + + return qty_in - qty_out + + def action_validate(self): + """Validate and create backdated stock moves""" + self.ensure_one() + if self.state != 'draft': + raise UserError(_('Only draft adjustments can be validated.')) + + 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() + + self.write({'state': 'done'}) + return True + + def action_cancel(self): + """Cancel the adjustment""" + self.ensure_one() + if self.state == 'done': + raise UserError(_('Cannot cancel a validated adjustment.')) + self.write({'state': 'cancel'}) + return True + + def action_draft(self): + """Reset to draft""" + self.ensure_one() + self.write({'state': 'draft'}) + return True + + +class StockInventoryBackdateLine(models.Model): + _name = 'stock.inventory.backdate.line' + _description = 'Backdated Inventory Adjustment Line' + _sql_constraints = [ + ('unique_product_per_inventory', + 'unique(inventory_id, product_id, lot_id, package_id, owner_id)', + 'You cannot have duplicate products with the same lot/package/owner in the same adjustment!') + ] + + inventory_id = fields.Many2one( + 'stock.inventory.backdate', + string='Inventory Adjustment', + required=True, + ondelete='cascade' + ) + product_id = fields.Many2one( + 'product.product', + string='Product', + required=True, + domain="[('type', '=', 'product')]" + ) + lot_id = fields.Many2one('stock.lot', string='Lot/Serial Number') + package_id = fields.Many2one('stock.quant.package', string='Package') + owner_id = fields.Many2one('res.partner', string='Owner') + theoretical_qty = fields.Float( + string='Theoretical Quantity', + readonly=True, + help="Quantity at the backdated time (can be negative if there was negative stock)" + ) + counted_qty = fields.Float( + string='Counted Quantity', + required=True, + default=0.0 + ) + difference_qty = fields.Float( + string='Adjustment Qty (+/-)', + default=0.0, + help="Positive value adds stock, negative value removes stock." + ) + product_uom_id = fields.Many2one( + 'uom.uom', + string='Unit of Measure', + related='product_id.uom_id', + readonly=True + ) + state = fields.Selection(related='inventory_id.state', string='Status') + has_negative_theoretical = fields.Boolean( + string='Has Negative Theoretical', + compute='_compute_has_negative_theoretical', + help="Indicates if theoretical quantity is negative" + ) + + @api.onchange('counted_qty') + def _onchange_counted_qty(self): + for line in self: + line.difference_qty = line.counted_qty - line.theoretical_qty + + @api.onchange('difference_qty') + def _onchange_difference_qty(self): + for line in self: + line.counted_qty = line.theoretical_qty + line.difference_qty + + @api.depends('theoretical_qty') + def _compute_has_negative_theoretical(self): + for line in self: + line.has_negative_theoretical = line.theoretical_qty < 0 + + @api.onchange('product_id', 'lot_id', 'package_id', 'owner_id') + def _onchange_product_id(self): + """Auto-calculate theoretical quantity when product is selected""" + if self.product_id and self.inventory_id.location_id and self.inventory_id.backdate_datetime: + self.theoretical_qty = self.inventory_id._get_historical_quantity( + self.product_id, + self.inventory_id.location_id, + self.lot_id, + self.package_id, + self.owner_id, + self.inventory_id.backdate_datetime + ) + # Set counted_qty to theoretical_qty by default (Adjustment 0) + 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_count = self.env['stock.valuation.layer'].search_count([('stock_move_id', '=', move.id)]) + _logger.info(f"Found {svl_count} stock valuation layers for move {move.id}") + + if svl_count > 0: + self.env.cr.execute( + "UPDATE stock_valuation_layer SET create_date = %s WHERE stock_move_id = %s", + (backdate, move.id) + ) + _logger.info(f"Updated stock_valuation_layer for move {move.id}, rows affected: {self.env.cr.rowcount}") + 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 if they exist + # Refresh move to get account_move_ids + move = self.env['stock.move'].browse(move.id) + if move.account_move_ids: + account_date = backdate.date() + for account_move in move.account_move_ids: + self.env.cr.execute( + "UPDATE account_move SET date = %s WHERE id = %s", + (account_date, account_move.id) + ) + 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 diff --git a/models/stock_move.py b/models/stock_move.py deleted file mode 100644 index 4d8562c..0000000 --- a/models/stock_move.py +++ /dev/null @@ -1,85 +0,0 @@ -from odoo import api, models, fields -from odoo.tools import float_round - -class StockMove(models.Model): - _inherit = 'stock.move' - - def _action_done(self, cancel_backorder=False): - moves = super(StockMove, self)._action_done(cancel_backorder=cancel_backorder) - - forced_inventory_date = self.env.context.get('force_inventory_date') - if forced_inventory_date: - for move in moves: - move.write({'date': forced_inventory_date}) - # If valuation is real-time, we might need to adjust the account move date too. - # But account move creation usually happens in _action_done -> _create_account_move_line - # which might use the move date. - # Let's check if we need to update account moves. - # Account move date field is Date type, so convert datetime to date - if move.account_move_ids: - account_date = forced_inventory_date.date() if hasattr(forced_inventory_date, 'date') else forced_inventory_date - move.account_move_ids.write({'date': account_date}) - - return moves - - def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id, qty, description, svl_id, cost): - # Override to force date on account move creation if needed. - # However, if we update the move date in _action_done, it might be too late for this method - # if it's called during super()._action_done(). - # So we might need to rely on context here too. - - forced_inventory_date = self.env.context.get('force_inventory_date') - forced_valuation_date = self.env.context.get('force_valuation_date') - - # Use valuation date if present, otherwise inventory date - target_date = forced_valuation_date or forced_inventory_date - - if target_date: - # We can't easily change the arguments passed to this method without signature change, - # but we can patch the context or check if we can modify the created move later. - # Actually, this method creates 'account.move'. - # Let's see if we can intercept the creation. - pass - - return super(StockMove, self)._create_account_move_line(credit_account_id, debit_account_id, journal_id, qty, description, svl_id, cost) - - def _prepare_account_move_vals(self, credit_account_id, debit_account_id, journal_id, qty, description, svl_id, cost): - # This method prepares the values for account.move.create. - vals = super(StockMove, self)._prepare_account_move_vals(credit_account_id, debit_account_id, journal_id, qty, description, svl_id, cost) - - forced_inventory_date = self.env.context.get('force_inventory_date') - forced_valuation_date = self.env.context.get('force_valuation_date') - target_date = forced_valuation_date or forced_inventory_date - - if target_date: - # Account move date field is Date type, so convert datetime to date if needed - account_date = target_date.date() if hasattr(target_date, 'date') else target_date - vals['date'] = account_date - - return vals - - def _create_in_svl(self, forced_quantity=None): - # Override to force date on stock valuation layer - svl = super(StockMove, self)._create_in_svl(forced_quantity=forced_quantity) - self._update_svl_date(svl) - return svl - - def _create_out_svl(self, forced_quantity=None): - # Override to force date on stock valuation layer - svl = super(StockMove, self)._create_out_svl(forced_quantity=forced_quantity) - self._update_svl_date(svl) - return svl - - def _update_svl_date(self, svl): - forced_inventory_date = self.env.context.get('force_inventory_date') - forced_valuation_date = self.env.context.get('force_valuation_date') - target_date = forced_valuation_date or forced_inventory_date - - if target_date and svl: - # create_date is a magic field, we need to update it via SQL - self.env.cr.execute( - "UPDATE stock_valuation_layer SET create_date = %s WHERE id IN %s", - (target_date, tuple(svl.ids)) - ) - svl.invalidate_recordset(['create_date']) - diff --git a/models/stock_quant.py b/models/stock_quant.py deleted file mode 100644 index e008a78..0000000 --- a/models/stock_quant.py +++ /dev/null @@ -1,53 +0,0 @@ -from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError - -class StockQuant(models.Model): - _inherit = 'stock.quant' - - force_inventory_date = fields.Datetime( - string="Force Inventory Date", - help="Choose a specific date and time for the inventory adjustment. " - "If set, the stock move will be created with this date and time." - ) - force_valuation_date = fields.Datetime( - string="Force Valuation Date", - help="Choose a specific date and time for the stock valuation. " - "If set, the valuation layer will be created with this date and time." - ) - - @api.model - def _get_inventory_fields_create(self): - """ Allow the new fields to be set during inventory creation """ - res = super(StockQuant, self)._get_inventory_fields_create() - res += ['force_inventory_date', 'force_valuation_date'] - return res - - @api.model - def _get_inventory_fields_write(self): - """ Allow the new fields to be set during inventory write """ - res = super(StockQuant, self)._get_inventory_fields_write() - res += ['force_inventory_date', 'force_valuation_date'] - return res - - def _apply_inventory(self): - """Override to pass forced dates to the context""" - # We need to handle quants with different forced dates separately - # Group by (force_inventory_date, force_valuation_date) - # If no forced date, key is (False, False) - - grouped_quants = {} - for quant in self: - key = (quant.force_inventory_date, quant.force_valuation_date) - if key not in grouped_quants: - grouped_quants[key] = self.env['stock.quant'] - grouped_quants[key] |= quant - - for (force_inventory_date, force_valuation_date), quants in grouped_quants.items(): - ctx = dict(self.env.context) - if force_inventory_date: - ctx['force_inventory_date'] = force_inventory_date - if force_valuation_date: - ctx['force_valuation_date'] = force_valuation_date - - super(StockQuant, quants.with_context(ctx))._apply_inventory() - diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index 715a26f..e81ae50 100644 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -1,2 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_stock_quant_backdate,stock.quant.backdate,stock.model_stock_quant,stock.group_stock_user,1,1,1,1 +access_stock_inventory_backdate_user,stock.inventory.backdate.user,model_stock_inventory_backdate,stock.group_stock_user,1,1,1,1 +access_stock_inventory_backdate_line_user,stock.inventory.backdate.line.user,model_stock_inventory_backdate_line,stock.group_stock_user,1,1,1,1 +access_stock_inventory_backdate_manager,stock.inventory.backdate.manager,model_stock_inventory_backdate,stock.group_stock_manager,1,1,1,1 +access_stock_inventory_backdate_line_manager,stock.inventory.backdate.line.manager,model_stock_inventory_backdate_line,stock.group_stock_manager,1,1,1,1 diff --git a/tests/test_stock_backdate.py b/tests/test_stock_backdate.py index abd6e18..e08a7bb 100644 --- a/tests/test_stock_backdate.py +++ b/tests/test_stock_backdate.py @@ -11,34 +11,38 @@ class TestStockBackdate(TransactionCase): 'type': 'product', 'categ_id': self.env.ref('product.product_category_all').id, }) - # Enable automated valuation for the category if needed, - # but for simplicity we test the move date primarily. + # Enable automated valuation for the category self.product.categ_id.property_valuation = 'real_time' self.product.categ_id.property_cost_method = 'average' self.stock_location = self.env.ref('stock.stock_location_stock') def test_inventory_backdate(self): - """Test that inventory adjustment backdating works""" + """Test that backdated inventory adjustment works""" backdate = fields.Datetime.now() - timedelta(days=10) - quant = self.env['stock.quant'].create({ - 'product_id': self.product.id, + # Create backdated inventory adjustment + inventory = self.env['stock.inventory.backdate'].create({ + 'backdate_datetime': backdate, 'location_id': self.stock_location.id, - 'inventory_quantity': 100, }) - # Set forced dates - quant.force_inventory_date = backdate - quant.force_valuation_date = backdate + # Add inventory line + line = self.env['stock.inventory.backdate.line'].create({ + 'inventory_id': inventory.id, + 'product_id': self.product.id, + 'theoretical_qty': 0, + 'counted_qty': 100, + }) - # Apply inventory - quant.action_apply_inventory() + # Validate the adjustment + inventory.action_validate() # Check stock move date move = self.env['stock.move'].search([ ('product_id', '=', self.product.id), - ('is_inventory', '=', True) + ('is_inventory', '=', True), + ('origin', '=', inventory.name) ], limit=1) self.assertTrue(move, "Stock move should be created") diff --git a/views/stock_inventory_backdate_views.xml b/views/stock_inventory_backdate_views.xml new file mode 100644 index 0000000..3708ae5 --- /dev/null +++ b/views/stock_inventory_backdate_views.xml @@ -0,0 +1,136 @@ + + + + + stock.inventory.backdate.tree + stock.inventory.backdate + + + + + + + + + + + + + + stock.inventory.backdate.form + stock.inventory.backdate + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + + stock.inventory.backdate.search + stock.inventory.backdate + + + + + + + + + + + + + + + + + + + + Backdated Inventory Adjustments + stock.inventory.backdate + tree,form + {'search_default_draft': 1} + +

+ Create a backdated inventory adjustment +

+

+ This allows you to adjust inventory with a specific past date and time. + The system will automatically calculate the inventory position at that date. +

+

+ Two ways to add products:
+ 1. Click "Load Products" to load all products from a location
+ 2. Manually add individual products by clicking "Add a line" +

+
+
+ + + +
diff --git a/views/stock_quant_views.xml b/views/stock_quant_views.xml deleted file mode 100644 index 419c753..0000000 --- a/views/stock_quant_views.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - stock.quant.inventory.tree.editable.inherit.backdate - stock.quant - - - - - - - - -