From 2ba8d857a2474a79b4c89c3a831d8fd022893fbc Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Thu, 8 Jan 2026 17:26:37 +0700 Subject: [PATCH] add propagation method to future stock movement valuation --- ...tock_inventory_revaluation.cpython-312.pyc | Bin 12345 -> 15754 bytes models/stock_inventory_revaluation.py | 172 +++++++++++++++--- 2 files changed, 150 insertions(+), 22 deletions(-) diff --git a/models/__pycache__/stock_inventory_revaluation.cpython-312.pyc b/models/__pycache__/stock_inventory_revaluation.cpython-312.pyc index 5f85518bd6a6aa089c1d6bd8ea74d1bae4a5638f..f539a0ff44bc7f276248bec3bc8435f98ea0e96d 100644 GIT binary patch delta 4359 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 delta 1869 zcmZ8hTW}Lq7(TnX^p@tnA#Kx3dI@Y=+LWeD3B8M@jxBAa;FO_;xY?AX+iW@6R2#SS z#Zh#40MCrdR0kMEJAgAXc>weQe9;kyaaYuIb{Op=Pfh_HAAIqg^oH(izCGtZ-}&}C zfBt{pedG2C^ADw^#QzwU5mkJ^T56755+We)U}qlA_LdhB+hJ5B~QIUS~G^&B`y!E2Z^NEAyv(+2>F zENu{r%ALds5gA0MDjX#x0A#Si1(-iAQwb#W-qZOPtz_Q()5^ zRfp6aWrq4_>4t*naTdjN(T1oUhftNf5#28=aad(5{9PJZR|P<}CdP=tO5dj9=5xU& z+oE~f@Gh}hzhIZ`vMqy+E;wZC0Wf(LkDSusEh%SWoa94iTQ@)_j-qdz4s_1!s@OH1G1>@t(t zfrYTkP2mbMx1GJN2hm+J2sdwfod+z7zOFi7JBTA0+S>)mG2q_(th)<_$ZS7>L6oe1 z$;Q%JL1d1Hkttr&$Ko*&-K}mw|5R5rXjC_(3GpNb%>wzENIV%&PKD3@Irh@vIBIU*ASepnLTv7E6=-YvGg>4bQ?j z$7|+ov#B{Dr8&0er=PL}67~69=$LOne^L)X_KI(bMxU__yNsaHdvWB#NX`>dJ)v9C zJK}9o86D4!POA9so>V=roEcs*Sy75@)*C@pE&7GszqnSQKS-IM0)3U`%%R67V6A%S zW0#wA{vOrebJy~~Kl;EordaD%9KL1SPY(Y|u;b>)jSqL@t(AW9eK>R%7zt08CK>^yBr%p8za+4$^A?yhsnx zPXO=?(JR3*)zi8f#M(!4SRUU&PVuy^89{4%F3_t6dUJsRH87wYJpLdssknEqx+^ah zUnpKO<=lSN?N^$*Zl!ZQ<7&^i^0J`z2ukD3&+g=^yIt9LNOd1l-0dsw#^rRbrC)97 zH>>V`)!n#OM$E5yvS*v$*6UdVF5tMgu94M4QtEJtz()i=#-JJaqy*<#gQkOgEMNX` z0)63i+gLN4CM;5ia1jGy>Kjb?qFvVz&}^F@h3eUz1%zTGkbeW|A=su$;aKvd;%^>g-z4R`IM?{Yg|+>n&OD!w#|s z_15+-vZiOOkuHJ)fo%u*zHK8h(sCNjL9Kv)o#$}9=8uSTVJE9^k=5dSm6C2v!4GgC zg)0%%;0rnk=QM^)>tRXcre8?T;cSWMQbz;*BglT&K~b(5!m(jUk?-`zo*DGpK0o~k zpyH5-Pn-$uY`Q{tB=?$OKoBDm>?eVIYZLQY(P3WTIVmnC;hX4wDA1n&SX>|^lB~Q9 zt#B5KvpkgMA$hG<8V++pB$eWLK?sLg3XWv=>=!5_W$-*1(xFc~uTkHl(Jr4;M>SqD Qer~#K`U`M)3>m!pU(g>A@&Et; diff --git a/models/stock_inventory_revaluation.py b/models/stock_inventory_revaluation.py index 011a9c0..cfe8ba4 100755 --- a/models/stock_inventory_revaluation.py +++ b/models/stock_inventory_revaluation.py @@ -119,40 +119,168 @@ class StockInventoryRevaluation(models.Model): # Create Stock Valuation Layer self._create_valuation_layer(move) - # AVCO/FIFO Logic: Update Standard Price and Distribute Value to Layers - # This fixes the issue where revaluation creates a layer but doesn't update the moving average cost - if self.product_id.categ_id.property_cost_method in ['average', 'fifo'] and self.quantity > 0: - # 1. Update Standard Price - # We use disable_auto_svl to prevent Odoo from creating an extra SVL for this price change - new_std_price = self.product_id.standard_price + (self.extra_cost / self.quantity) - self.product_id.with_context(disable_auto_svl=True).sudo().write({'standard_price': new_std_price}) + # 1. Common Logic: Calculate Unit Adjustment & Update Standard Price + # This applies to Standard Price, AVCO, and FIFO + if self.quantity > 0: + unit_adjust = self.extra_cost / self.quantity + new_std_price = self.product_id.standard_price + unit_adjust + + # Update the price on the product + # For Standard Price: This sets the new fixed cost. + # For AVCO/FIFO: This updates the current running average. + self.product_id.with_context(disable_auto_svl=True).sudo().write({'standard_price': new_std_price}) + + # 2. AVCO/FIFO Specific Logic: Distribute Value to Layers & Propagate to Sales + # Standard Price does not use layers for costing, so we skip this part for it. + if self.product_id.categ_id.property_cost_method in ['average', 'fifo'] and self.quantity > 0: + + # Distribute to Remaining Stock (The "Survivor" Layers) + # Track how much we distributed + total_distributed = 0.0 - # 2. Distribute Value to Remaining Layers (Crucial for correct COGS later) remaining_svls = self.env['stock.valuation.layer'].search([ ('product_id', '=', self.product_id.id), ('remaining_qty', '>', 0), ('company_id', '=', self.company_id.id), + ('create_date', '<=', self.date), ]) + remaining_svls = remaining_svls.filtered(lambda l: l.create_date <= self.date) if remaining_svls: - remaining_qty_total = sum(remaining_svls.mapped('remaining_qty')) - if remaining_qty_total > 0: - remaining_value_to_distribute = self.extra_cost - remaining_value_unit_cost = remaining_value_to_distribute / remaining_qty_total + for layer in remaining_svls: + adjustment_amount = layer.remaining_qty * unit_adjust + adjustment_amount = self.currency_id.round(adjustment_amount) - for layer in remaining_svls: - if float_compare(layer.remaining_qty, remaining_qty_total, precision_rounding=self.product_id.uom_id.rounding) >= 0: - taken_remaining_value = remaining_value_to_distribute - else: - taken_remaining_value = remaining_value_unit_cost * layer.remaining_qty + if not float_is_zero(adjustment_amount, precision_rounding=self.currency_id.rounding): + layer.sudo().write({ + 'remaining_value': layer.remaining_value + adjustment_amount, + # 'value': layer.value + adjustment_amount # Leaving value untouched to preserve historic record + }) + total_distributed += adjustment_amount + + # 3. Distribute to Sold Stock (The "Forward Propagation") + # Only distribute what's left! This ensures we don't "correct" sales of NEW stock. + remaining_value_to_expense = self.extra_cost - total_distributed + remaining_value_to_expense = self.currency_id.round(remaining_value_to_expense) + + if float_compare(remaining_value_to_expense, 0, precision_rounding=self.currency_id.rounding) > 0: + # Find outgoing moves (sales) that happened AFTER revaluation date + outgoing_svls = self.env['stock.valuation.layer'].search([ + ('product_id', '=', self.product_id.id), + ('company_id', '=', self.company_id.id), + ('quantity', '<', 0), # Outgoing + ('create_date', '>', self.date), # After revaluation + ], order='create_date asc, id asc') # Chronological + + if outgoing_svls: + for out_layer in outgoing_svls: + if float_compare(remaining_value_to_expense, 0, precision_rounding=self.currency_id.rounding) <= 0: + break # Limit reached - # Rounding - taken_remaining_value = self.currency_id.round(taken_remaining_value) + # How much correction does this move "deserve"? + qty_sold = abs(out_layer.quantity) + theoretical_correction = qty_sold * unit_adjust + theoretical_correction = self.currency_id.round(theoretical_correction) - layer.sudo().write({'remaining_value': layer.remaining_value + taken_remaining_value}) + # We can only give what we have left + actual_correction = min(theoretical_correction, remaining_value_to_expense) # If positive adjustment + if unit_adjust < 0: # If negative adjustment? + # If unit_adjust is negative, everything is negative. + # extra_cost is neg, total_dist is neg, remaining_to_exp is neg. + # abs(actual) = min(abs(theo), abs(rem)) + # implementation detail: let's handle signs properly? + # Simplify: assume always positive for logic "Cap", but math works. + # Wait, min() with negatives works differently. + # -100 vs -50. min is -100. We want "Closest to zero". + pass - remaining_value_to_distribute -= taken_remaining_value - remaining_qty_total -= layer.remaining_qty + # Handle Sign-Agnostic Capping + # We want to reduce the magnitude of remaining_to_expense towards zero. + # If remaining is +100, we reduce by positive amounts. + # If remaining is -100, we reduce by negative amounts. + + if remaining_value_to_expense > 0: + actual_correction = min(theoretical_correction, remaining_value_to_expense) + else: + # theoretical is likely negative too because unit_adjust is negative + actual_correction = max(theoretical_correction, remaining_value_to_expense) + + if float_is_zero(actual_correction, precision_rounding=self.currency_id.rounding): + continue + + # Create Correction SVL + stock_val_acc = self.product_id.categ_id.property_stock_valuation_account_id.id + cogs_acc = self.product_id.categ_id.property_stock_account_output_categ_id.id + + if not stock_val_acc or not cogs_acc: + continue + + # Accounting Entries + # If Positive Adjustment (Value Added): + # Dr COGS -> Increased Cost + # Cr Stock Asset -> Decreased Asset (Since we put it all in Asset initially) + # Wait, initially we Debited Asset +100. + # Now we say "50 of that was sold". + # So we Credit Asset -50, Debit COGS +50. + # Correct. + + # If Negative Adjustment (Value Removed): + # Initially Credited Asset -100. + # "50 of that removal belongs to sales". + # So we Debit Asset +50, Credit COGS -50 (Reduce Cost). + # The math: actual_correction is -50. + # Using same logic: Debit COGS with -50? (Credit 50). + # Credit Asset with -50? (Debit 50). + # Logic holds purely with signs. + + move_lines = [ + (0, 0, { + 'name': _('Revaluation Correction for %s') % out_layer.stock_move_id.name, + 'account_id': cogs_acc, + 'debit': actual_correction if actual_correction > 0 else 0, + 'credit': -actual_correction if actual_correction < 0 else 0, + 'product_id': self.product_id.id, + }), + (0, 0, { + 'name': _('Revaluation Correction for %s') % out_layer.stock_move_id.name, + 'account_id': stock_val_acc, + 'debit': -actual_correction if actual_correction < 0 else 0, + 'credit': actual_correction if actual_correction > 0 else 0, + 'product_id': self.product_id.id, + }), + ] + + am_vals = { + 'ref': f"{self.name} - Corr - {out_layer.stock_move_id.name}", + 'date': out_layer.create_date.date(), + 'journal_id': self.account_journal_id.id, + 'line_ids': move_lines, + 'move_type': 'entry', + 'company_id': self.company_id.id, + } + am = self.env['account.move'].create(am_vals) + am.action_post() + + # Correction SVL + # Value should be negative of correction to reduce Asset + # If correction is +50 (add to COGS), SVL value is -50 (remove from Asset). + svl_value = -actual_correction + + new_svl = self.env['stock.valuation.layer'].create({ + 'product_id': self.product_id.id, + 'value': svl_value, + 'quantity': 0, + 'unit_cost': 0, + 'remaining_qty': 0, + 'stock_move_id': out_layer.stock_move_id.id, + 'company_id': self.company_id.id, + 'description': _('Revaluation Correction (from %s)') % self.name, + 'account_move_id': am.id, + }) + self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', + (out_layer.create_date, new_svl.id)) + + remaining_value_to_expense -= actual_correction self.state = 'done'