From 31911354f09b1e08dc5df944f824a76370536c32 Mon Sep 17 00:00:00 2001 From: "admin.suherdy" Date: Tue, 9 Dec 2025 18:10:48 +0700 Subject: [PATCH] feat: Introduce a dedicated model and UI for backdated inventory adjustments, replacing previous scattered logic. --- README.md | 85 ++++ __manifest__.py | 9 +- data/sequence_data.xml | 13 + models/__init__.py | 3 +- models/__pycache__/__init__.cpython-312.pyc | Bin 276 -> 255 bytes models/__pycache__/stock_move.cpython-312.pyc | Bin 4195 -> 0 bytes .../__pycache__/stock_quant.cpython-312.pyc | Bin 2806 -> 0 bytes models/stock_inventory_backdate.py | 421 ++++++++++++++++++ models/stock_move.py | 85 ---- models/stock_quant.py | 53 --- security/ir.model.access.csv | 5 +- tests/test_stock_backdate.py | 28 +- views/stock_inventory_backdate_views.xml | 136 ++++++ views/stock_quant_views.xml | 14 - 14 files changed, 681 insertions(+), 171 deletions(-) create mode 100644 README.md create mode 100644 data/sequence_data.xml delete mode 100644 models/__pycache__/stock_move.cpython-312.pyc delete mode 100644 models/__pycache__/stock_quant.cpython-312.pyc create mode 100644 models/stock_inventory_backdate.py delete mode 100644 models/stock_move.py delete mode 100644 models/stock_quant.py create mode 100644 views/stock_inventory_backdate_views.xml delete mode 100644 views/stock_quant_views.xml 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 58b324f227b258d28896b4eed4d203d38197c021..8649ec73e429da513d4f4f61e1755eb1163df239 100644 GIT binary patch delta 94 zcmbQj^q-OUG%qg~0}$L>VV7(25q|qeE&n9Cq7>V*W5%KrGpMV^U0xO|dyH z#W6U>X8D|u5*WggOGM>Xh{|hhCbVs&@m%9PJ=Inc625<8C&4+KTu3{(9Ow$$-qs1u#lsiH~g{JfSoby|{C z+0+~!CB#WEiUo~QJUb^O$%>iQ^OCCPHHXvk^G=(h=S^+Fbhr;R(-A(RX?QmxD+<8M zlrW@HS~Z-uIoXg+lTt)&qRrtAEjvq*7>SJ0KNB}&XDYEXo3VjPY`}hFY&|w^i%0(wj8zXF{TK0vN0~c( zH4?oS_%v|;q8|gbt0-ZvvVYn(Tg-MXMrUSchf96K~*ypnl7Ld zj?b8PXLv<7ObYuTb)M|>)Crn01qQm@rH5gmflTwL;qim^zyLi4VzIXgwtMa35Zbr} z0x%4ad*#HQfUWKW<}Dw2>AQdK9=4Oi<>23g-CF??KJZmX^zKhr`AWyz_TZ#Ft=O7w z-!uWVA2IK;@EB&V;A5P<#sV}Wli2Z3vh)aevpY=VFK|KZ27o1mV;wYLzSds&s%QY) z|1B*wfDK@-0o%V6w1TwPtY11-aL2U~%pg2lTGO==TnbqsOH{eHV2vfZSAYw5`Z5f7 zxmaw%^kJz$wSr7rKn+kcT8R*?8)$dYPGdOm3X0<>dQrqtoIvp^2#0qKA&Tj1C{Ch4 zoOyu7jM@a)SZvvY?CUVT7;l2?o=*0`O5=AR0JC+F4chNa+1FKjwqVmDAZdXaLO_kM zWB9nrPP!nQV$1w~S$RK@SG!L&LHw(;Z+_A9aCYPD)%)i+drnn)PHps@wx@qq9I12D9B==sgp-W$RNyO z-uoY3&R95u>DKnb*L)ptOHdNmau|zcGo7>;IlU-?2T<)3 zV3yr3t^xYYZdp?0Kd9$!hLqf2LhyPeCbVD-}f=9gX!Kt`~KqG}3SaG?D)618L%*De^2yEf-WtcZ^gIFf}X0?5; z^_>~-*Zq$}kxxc%jeatAYiu)gtP(m_jZ9S!9Qm!bI`sLa&o2EYZ%0l)6}WKwmPo>p z%}~4&im&EYr7uHoY=y~@W99QM#S_fM1T&EcI-(@y<(wu-PC$Z>2(_3+JtRqxwX(Gt z5lVHa^wXwWio1mkMPgA*KNM-`a?}aez82u4Co4$_#>-d;ltJ&juvBzwIwZ1zH`CWE zq}tK5Jo=1}2whL(ox&vh^!2b1efBahbUq7#_;)BMoO6A4-<(K@4v*gy6lqD(5XGoC z0UYczbscU{#JM9}#qV(Ur3b__fOXk7;#=M}e?&*1i{2SA5L+C>Fn=TQ3W@)f9DmAR TW0-SyeD^}1hMp1BT?_vK_(_`w diff --git a/models/__pycache__/stock_quant.cpython-312.pyc b/models/__pycache__/stock_quant.cpython-312.pyc deleted file mode 100644 index a1745bf3cd70f4e619b801ca5468427ff5cede2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2806 zcmcH*OKclObY_44WMijE`j5(@h$K#1n+VaOs%_dPg{BoP5LC4mXxZ+L<8{|wGrLY~ z%a(!^sX4S#DitN1C=wE_!hs`au5hpwP|OMh5?r{2riiL1X2#!CX`u&3^7#Gd&HK%M z+ua=mWXVHE%+LJ*{6>-H5*xtgI|Nt;1~8xn(l8FwT%1euaXu}?1xV?fC8njg1OW%$ z00zGR3<2?}P}5NRf0eJpaeqOKNPkcyB6z%(H9aX|BFk_*DQz3bay+O;pclTZS*D@6 zrky#9v5ob%DFB29viS`GZV(R`L?$)USh_43!rLfA?sd4G4a-t!1 z$oY^z)w|Ua#TG#V8omYpYJ(LIK10smA=kw;0}KARya@6z-aQQ>K`%J%+WPgkb6UoI z+~rU>`XNP%{jZa|BV{Txi!!c_3(BPCqGEX>Y16Dv_=Dxo;YJ;Wr~VM^MbMarCMS;{q}=ok@W2EpAVkujaE#-kC= zlN=YDnHf(=A}iZ;lB6uhGWMKK)?JuwYH#(gMOG}Wfbf5IQimN$qs2#mo5#rZF{A zz;pzjMU#}V;`NiP=tynlM5Q@vQd{?A)Clj1PA-cu4iHbh0d)qs9g6i+)q0}t^Pb=! zE8+1NIbJ|jbxU&`Rdwi%6=nW$I+nE4DCXpn2pffHLDTJPr_x$h%S3fMY8tV(3U1QQ z=%oE(wqe_2M`Jpv6+5jNhMjR@EI%EmU(@vK)b&`sVH*m|eY*BfN|1>?K=q=wBnp)T!(?fjhkxns{2^ z@NS~BC2$fUu)g?!bPMeZlU6#I1n-|9HP4l}6cKoG5o)Vv+Pnl;=1Y7D&O+Q<0;%SR zZVQk{g!){9CH{EhKhNbkM&pY8#4}#!DCg$5K%`Q>4%G(O>`mg|WdPnIUeI4zD2rvu zr2%Ja`_s335>^{CqUx&00ki;77tyTB`s|lhz^V_=~6nVbuwycJ2{j{hqi8KT$Fb)eZ<;@bukibBOZ@3 zvz|ywFYS1OVd}2uYpS)&Po@)GZP2|QRm*0rLc5V80o+H9@Io`#CbPy+SqnR!$o@V4 zIx2LWC^fje?&k4MqUX?vD~&Z@uV96ciu*e&_=z*Afb$*MNWX8$VBf%EsJ3(O10Vz< zi)U+nd)E67SNjg%%dN+zspUa3xAS(}VM0KUKlSd~vz<<~0RFIEPRem(S4 z@2(|xIe$C9zI(X3dw8vPWO3rh@L(-^W{I!#k5+pQZpD6=cib6WNvt0js~#BpYH;nq zk*a*|2l@P;VQQN-RwU&4RW+lfk*az@RVBkQXHi&IX<)4e$4}x<=CJ8v+9IBRl4PDf z+3iSU+L4~fhASJUY%XAhQo9?|T}PIslJBH?B4`<(swaTvgv}mm-6@jo(E_N22j 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 - - - - - - - - -