From 66770bc471fa4cc8c00b3cc6bf94f399d178eb3e Mon Sep 17 00:00:00 2001 From: "admin.suherdy" Date: Sat, 6 Dec 2025 19:03:29 +0700 Subject: [PATCH] first commit --- __init__.py | 1 + __manifest__.py | 14 +++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 230 bytes models/__init__.py | 2 + models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 276 bytes models/__pycache__/stock_move.cpython-312.pyc | Bin 0 -> 3972 bytes .../__pycache__/stock_quant.cpython-312.pyc | Bin 0 -> 2766 bytes models/stock_move.py | 81 ++++++++++++++++++ models/stock_quant.py | 53 ++++++++++++ security/ir.model.access.csv | 2 + tests/__init__.py | 1 + tests/test_stock_backdate.py | 59 +++++++++++++ views/stock_quant_views.xml | 14 +++ 13 files changed, 227 insertions(+) create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/stock_move.cpython-312.pyc create mode 100644 models/__pycache__/stock_quant.cpython-312.pyc create mode 100644 models/stock_move.py create mode 100644 models/stock_quant.py create mode 100644 security/ir.model.access.csv create mode 100644 tests/__init__.py create mode 100644 tests/test_stock_backdate.py create mode 100644 views/stock_quant_views.xml diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..98e732a --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,14 @@ +{ + "name": "Stock Inventory Backdate", + "summary": "Allow backdating of physical stock adjustments and valuations.", + "version": "17.0.1.0.0", + "category": "Warehouse", + "author": "Antigravity", + "license": "AGPL-3", + "depends": ["stock_account"], + "data": [ + "security/ir.model.access.csv", + "views/stock_quant_views.xml", + ], + "installable": True, +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00e3e452ba1763ac7335b7373ae17a63c4fb727e GIT binary patch literal 230 zcmX@j%ge<81c${VNJ$c zY`OUNP&QEg4O)N;v(@W0R z%S_P^s4U6I&r1es*UwMM&o?yJPcAJk$$LV1Uab~#Q4C>$jEq)L8OQc$N>Nau|7Ni literal 0 HcmV?d00001 diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..c1e37b5 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_quant +from . import stock_move diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58b324f227b258d28896b4eed4d203d38197c021 GIT binary patch literal 276 zcmXv|F-`+95Zt|s1SCogNPOU$vk*u$2=M|$oyKx(OJ{QS2HO|OFX0V*goYnPf`*C? zDP1b|6qsUmX0^Ms@ALT-7;mT7?uzTzX#P?CiQNT@7cgM32agNHQCF<1MTJo6X7Y_Z z(%^QMGU=6k9#qI|rw8k4V;`D&3$f<0IAMB)9W(5oL`U9=G;}ta(iWv@dGFO)g;rDR zjCd@$qoB!HL5Aq^M(BaBdSwiSBn|;CY;4FBi(D$bGAdi)d81E4N*7#~vR#+`@hvxp Z-rmI%jvhZwGp3&!5kKJk6VBoh`(LVkOcDS9 literal 0 HcmV?d00001 diff --git a/models/__pycache__/stock_move.cpython-312.pyc b/models/__pycache__/stock_move.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67217ab0864312b9b6015e690314eda8d0ac8df0 GIT binary patch literal 3972 zcmcha-EZ606~HgQWa?X~71#Oj!X{1^^~1_$w?o!$br!oxo5YQsq{9RZ1VhpdT}o6g zDc3S+zz@cz3@Mrd8IoapaG#1Gd)S}Q_q|Ya3u{D&W!N6JH(^PJqE9=Aq$OE#+yEVR z1stCH!OMH^`JHo*{+LcD2$ad;3FBs#kiTPPs|bOvv*FRTHKxRAVuQ>x8oL%R@qxyzi6tS> zc+g^6JkSN`ABglA=%O~TMoQ4P#FP>jNg?6KFFKa`{&{OrUryMlj+H@I*`Af)>EL1> z#27+;&DZW`} zQQxY)dRndJ;uPpil zs#S6HCCBI9(;Z*dsZGq5`i)OS7!+ z{h+AUfRM7H!IJG_087sY@epvI#gOo*9~UrIty8qb@=%AhvEJPeK%e~!5MQm5FLQfu z8SD9GZrq!`?B&Kca_=^T`_kyA;wR#!bi64Y-;^er(uDWQ*$wHOCyx9%DYb@2{!Iqb zv&>DtmCN5we4M!RrZ;-RJMhELbFZ}%BtN`4cx+?vSPPVVp_wmi=8MgI@ef1Zi5c(s zJ7454w6pkn`x%nk+ekftl@PTT(l|5(4)7%?=l}pV4(cll9cSovENq3AZW9ET4%p(i zoT=*~aW}ri*eqZ#t|Z(9W#Yu?IG@wm4Y=vE=%y~~axU)*Zp2jj3iQGd=Y6tDbr=J7?ZMSFDtaMB^DxZc0|PX}=?pM5Kd0$e zwznh}tQxH-CV=j9*POba)O1^=Mim#~$Lz(RhgU7zp+h+97Wms&>L(qAf)fSqM~7jc zjbS<)F0i&^ zC5Hgc^qzlY^S6Gs&Ns7fc$2fEIg*zxAAd~y}$yRxmm2lXIZ)* zR}^Bh7Up?D6(VH8JDjDqm_;C1AW z9!2pyihwfwa1UitgxN7@ULNhi?1_td2}at#0s)+Lz?k$-&Uu$KZ@%i$8d!gcIfW2R zvuE*fo}CTAm}49KZjo{~Fk3?}_F&mM@O)$H=Fs(X_p^hIWC&VsY{OU>#KSpIz6rFx zgdmDF@M5R99b-fEg3I+TiW+mI9f53fzkEMe;Y^3)e_2e=i;8KdaJVqQsS zU53s)VfLLOBc=v@1@chd>EyF6v*SO6C`X=W??sLU=rap=z4jH2f3Fi&8Bz~^1tGkq zgP6BGdHKTB$xG9cjoG{0y$DUEu2bpa^d%|68XZLXk#y9SetKqlZdx)l>8%;4`pKRF z1+gzK`m9R*0ewkVYmkl#POWM}z6yz`pNBM8F^$0QsIEc=Z0k-z2p#V@>?63MyCKrX zTquZ+;pK~DF7NLlu+N95q2Im%VwLO~yLP#7x5u^LjNeb?K00&#%tvRhpWRFyYNigg za&xUc2Y;uppZfIlC#Qc~@p3Oe6u5MzEs}I@Gd0>wjjos10ix-$T z3(QO*>5H;lQOde3`w1B`bggEho|0vVT4vNELXr$Qw&4UxaFB9PybvgcABt`m_^6+b z(scMoVajrW2_qJQG8vv1CUHTwg^p~07sOX& wT6{g+=8x$qsN(8u2}GM?80N2Jv`I$)LJmLV&oRuaH)FR`AEzD>)PouR3!v0%CjbBd literal 0 HcmV?d00001 diff --git a/models/__pycache__/stock_quant.cpython-312.pyc b/models/__pycache__/stock_quant.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..179e6b28bb2f2db77df7de94bea5ff90999b1f18 GIT binary patch literal 2766 zcmcImOKclO7@paeAKBPxlD?y|C?bi|)+Pd4)OVY-G>s^aNL6crmhJ91UT5t!vzx@W z94SbV>O(7~Qc=RGpSE7w6J^oKFjJ0a89^iD@Y= zLBN5xfWa>ULqL2g)G*X~uky8Z+@BXC(jSzF2p+FxO;1Xg$TA#HO4|mq91p4y=!LIn zmT73NX=l!3Y-7D;3IL%_Y<@!)H;9J_#GwJ=oB`uJ2au3rcWYMx#8-p=Qds3`+_>~i zInj{XfUOJQYDaphHufoTHlHXpCjjQlj~rbfkl5@E`c13cTR#x&ae#R0^{G?HZBwkBs@9_MfcFFkSqYEF z$ngTIs#}`lsH#J6tSAdl(y^qSMlok5iLjB6<~7~EekQGDwM36;>fwa{3JulhU6T?6HzQ|0jKivKiWgu6?@O(G5cr|=&A&*7cJ zCMhaR+Zhg%DZ=9}x@K|Pzx~yj4rW)c#|ZDDI(x}f7KIQ{Ad-W6ixG|M$y->cx$lLNg%j+ zRRDP*A{Gw4GfPU^G!ShVSyH968|$Q>(sFVrlXhm^&bTP&V)}tMxx>PNjweqb(js$QIIl>D~VVm?CLuDoGcp`gy{4~n9 zoha3JdEL$7okY)}h%5E6P|IM2unPOzGx(`9setnxnMl2F$Y5VzDOBCL_aP7hk<$5U z&z|+3!>ga}9x9Fg81An|&n@xg-r-8u!L8Kq@{T*hD~a_3Bb5UqU-hpYI8u=> z{vcoaGfZu>!it1Ezp7@mG*VSBsH&tn&RBFUs}xwPzT+qOlRj*^n3jkqjFHHjr9ctMrkm>OvsD}F%F8;cIVCBf& zVCVvTBno{}S=h4?1bu^B_9j|;GLQl{u6=TClM}?AO&&<$4Z0!#Z_EE% z?iDBC2B0&8ggJ9L67U52Sz&i$x5o765!2@5h3QTd<#d##|8WkcW$%e^)89O%{s7cW y(FXa%f1-x!DLg_*dUZ!3(*_SA`~?iIfx(}_!AHWYZ~`vvTJF2u_ZOgRX5k;LjJ|yU literal 0 HcmV?d00001 diff --git a/models/stock_move.py b/models/stock_move.py new file mode 100644 index 0000000..76820bf --- /dev/null +++ b/models/stock_move.py @@ -0,0 +1,81 @@ +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. + if move.account_move_ids: + move.account_move_ids.write({'date': forced_inventory_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: + vals['date'] = target_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 new file mode 100644 index 0000000..cb5d0b6 --- /dev/null +++ b/models/stock_quant.py @@ -0,0 +1,53 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + +class StockQuant(models.Model): + _inherit = 'stock.quant' + + force_inventory_date = fields.Date( + string="Force Inventory Date", + help="Choose a specific date for the inventory adjustment. " + "If set, the stock move will be created with this date." + ) + force_valuation_date = fields.Date( + string="Force Valuation Date", + help="Choose a specific date for the stock valuation. " + "If set, the valuation layer will be created with this date." + ) + + @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 new file mode 100644 index 0000000..715a26f --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,2 @@ +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 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..eee6fc9 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_backdate diff --git a/tests/test_stock_backdate.py b/tests/test_stock_backdate.py new file mode 100644 index 0000000..6805a2a --- /dev/null +++ b/tests/test_stock_backdate.py @@ -0,0 +1,59 @@ +from odoo.tests.common import TransactionCase +from odoo import fields +from datetime import timedelta + +class TestStockBackdate(TransactionCase): + + def setUp(self): + super(TestStockBackdate, self).setUp() + self.product = self.env['product.product'].create({ + 'name': 'Test Product Backdate', + '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. + 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""" + backdate = fields.Date.today() - timedelta(days=10) + + quant = self.env['stock.quant'].create({ + 'product_id': self.product.id, + 'location_id': self.stock_location.id, + 'inventory_quantity': 100, + }) + + # Set forced dates + quant.force_inventory_date = backdate + quant.force_valuation_date = backdate + + # Apply inventory + quant.action_apply_inventory() + + # Check stock move date + move = self.env['stock.move'].search([ + ('product_id', '=', self.product.id), + ('is_inventory', '=', True) + ], limit=1) + + self.assertTrue(move, "Stock move should be created") + self.assertEqual(move.date.date(), backdate, "Stock move date should be backdated") + + # Check account move date if exists + if move.account_move_ids: + self.assertEqual(move.account_move_ids[0].date, backdate, "Account move date should be backdated") + + # Check valuation layer date (create_date) + svl = self.env['stock.valuation.layer'].search([ + ('stock_move_id', '=', move.id) + ], limit=1) + + if svl: + # create_date is datetime, backdate is date. + # We check if the date part matches. + self.assertEqual(svl.create_date.date(), backdate, "SVL create_date should be backdated") diff --git a/views/stock_quant_views.xml b/views/stock_quant_views.xml new file mode 100644 index 0000000..419c753 --- /dev/null +++ b/views/stock_quant_views.xml @@ -0,0 +1,14 @@ + + + + stock.quant.inventory.tree.editable.inherit.backdate + stock.quant + + + + + + + + +