commit 16d9d42ad9f97b0b2356eb7a3bc20a2d383108d3 Author: Suherdy Yacob Date: Tue Jan 13 16:32:10 2026 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..947623b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.pyo +.DS_Store +*.swp +*.swo +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..45d9641 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# Stock Readjust Valuation + +This module allows for the retrospective adjustment of product costs and Cost of Goods Sold (COGS) based on a calculated weighted average cost over a specified period. It is designed to correct valuation discrepancies and ensure accurate financial reporting, particularly for Point of Sale (POS) transactions. + +## Features + +* **Period-Based Readjustment**: Define a Start Date and End Date to calculate the Weighted Average Cost of products based on Initial Stock and Purchases within that period. +* **Initial Stock Correction**: Automatically readjusts the value of the stock on hand at the Start Date if the target cost differs from the historical cost. +* **COGS Readjustment**: Propagates the new Average Cost to all Outgoing Stock Moves (Sales, Manufacturing, etc.) within the period. +* **POS Accounting Correction**: Specifically handles POS transactions by creating secondary accounting entries to move the cost difference from the Interim Stock Output account to the Final POS Expense account. +* **Traceability**: + * Generates a unique sequence (`ADJ/XXXXX`) for each readjustment record. + * Smart Button to view all generated Accounting Journal Entries. + * Detailed references in Journal Entries linking back to the Readjustment record, Stocks Moves, and POS Sessions. + +## Accounting Logic + +### 1. Initial Stock Adjustment +* **Trigger**: Difference between `Initial Value` and `Target Initial Value`. +* **Accounting Date**: **Start Date** of the Readjustment Period. +* **Entry**: Adjusts `Stock Valuation` vs `Expense/Gain-Loss`. + +### 2. COGS (Delivery) Adjustment +* **Trigger**: Outgoing Stock Moves within the period. +* **Accounting Date**: **End Date** of the Readjustment Period. +* **Entry**: Adjusts `Cost of Goods Sold` vs `Stock Valuation`. +* **Calculation**: `(New Cost * Qty) - Abs(Current Value)` + +### 3. POS Specific Adjustment (Secondary Correction) +* **Trigger**: Outgoing Moves linked to POS Orders. +* **Context**: POS transactions typically move cost from `Stock Valuation` -> `Interim Output` (Delivery) -> `Expense` (POS Session Closing). +* **Primary Correction**: Adjusts `Stock Valuation` vs `Interim Output`. +* **Secondary Correction**: Adjusts `Interim Output` vs `Final Expense`. +* **Accounting Date**: **End Date** of the Readjustment Period. +* **Reference**: Includes POS Order and POS Session Name for easy reconciliation. + +## Usage + +1. Go to **Inventory > Operations > Stock Readjustment**. +2. Create a new record. +3. Select the **Product Category** (optional filtering) and set the **Date Range**. +4. Click **Load Products** to populate the lines with products that had stock or moves during the period. +5. Review the `Qty at Start`, `Valuation at Start`, and `Purchase Value`. + * You can manually edit `Qty Counted` and `Target Initial Value` if the system computed values need overridings. +6. Click **Calculate** to see the `New Average Cost`. +7. Click **Apply Readjustment** to post the corrections. +8. Use the **Journal Entries** smart button to review the posted moves. 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..873600f --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'Stock Readjust Valuation', + 'version': '17.0.1.0.0', + 'category': 'Inventory/Inventory', + 'summary': 'Retrospective Stock Valuation Adjustment', + 'description': """ + Allows retrospective readjustment of stock valuation by calculating + Weighted Average Cost over a period (Initial + Purchases) and + redistributing the cost to COGS and Inventory. + """, + 'author': 'Suherdy Yacob', + 'depends': ['stock_account', 'account'], + 'data': [ + 'security/ir.model.access.csv', + 'data/ir_sequence_data.xml', + 'views/stock_readjust_valuation_views.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/data/ir_sequence_data.xml b/data/ir_sequence_data.xml new file mode 100644 index 0000000..9f42288 --- /dev/null +++ b/data/ir_sequence_data.xml @@ -0,0 +1,12 @@ + + + + + Stock Readjustment + stock.readjust.valuation + ADJ/ + 5 + + + + diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..a31dc07 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1 @@ +from . import stock_readjust_valuation diff --git a/models/stock_readjust_valuation.py b/models/stock_readjust_valuation.py new file mode 100644 index 0000000..20ce5d4 --- /dev/null +++ b/models/stock_readjust_valuation.py @@ -0,0 +1,725 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from odoo.tools import float_is_zero + +class StockReadjustValuation(models.Model): + _name = 'stock.readjust.valuation' + _description = 'Stock Readjust Valuation' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default=lambda self: _('New')) + date_start = fields.Datetime(string='Start Date', required=True) + date_end = fields.Datetime(string='End Date', required=True) + journal_id = fields.Many2one('account.journal', string='Journal', required=True) + + line_ids = fields.One2many('stock.readjust.valuation.line', 'readjust_id', string='Products') + + state = fields.Selection([ + ('draft', 'Draft'), + ('calculated', 'Calculated'), + ('done', 'Done'), + ('cancel', 'Cancelled') + ], string='Status', default='draft', tracking=True) + + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) + + account_move_ids = fields.Many2many('account.move', string='Journal Entries', readonly=True) + account_move_count = fields.Integer(compute='_compute_account_move_count', string='Journal Entries Count') + + @api.depends('account_move_ids') + def _compute_account_move_count(self): + for record in self: + record.account_move_count = len(record.account_move_ids) + + def action_view_journal_entries(self): + self.ensure_one() + return { + 'name': _('Journal Entries'), + 'type': 'ir.actions.act_window', + 'res_model': 'account.move', + 'view_mode': 'tree,form', + 'domain': [('id', 'in', self.account_move_ids.ids)], + 'context': {'create': False}, + } + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('stock.readjust.valuation') or _('New') + return super().create(vals_list) + + def action_load_products(self): + self.ensure_one() + # Find products that have stock moves in the period or prior + # For simplicity, we can load all storable products, or those with moves. + # Let's find products with valuation layers. + + domain = [('type', '=', 'product')] + products = self.env['product.product'].search(domain) + + existing_products = self.line_ids.mapped('product_id') + products_to_add = products - existing_products + + lines = [] + for product in products_to_add: + lines.append((0, 0, { + 'product_id': product.id, + })) + + self.write({'line_ids': lines}) + + # Auto-calculate initial stock for all lines (new and old) + self._compute_initial_stock() + + # Explicitly set defaults for newly loaded lines + # Only set if they are zero/empty? Or force reset? + # When loading, we assume we want the defaults. + for line in self.line_ids: + if float_is_zero(line.qty_counted, precision_rounding=line.product_id.uom_id.rounding) and float_is_zero(line.target_initial_value, precision_digits=2): + line.qty_counted = line.initial_qty + line.target_initial_value = line.initial_value + + def _compute_initial_stock(self): + for record in self: + for line in record.line_ids: + # 1. Initial Stock (Before Start Date) + layers = self.env['stock.valuation.layer'].search([ + ('product_id', '=', line.product_id.id), + ('create_date', '<', record.date_start), + ('company_id', '=', record.company_id.id) + ]) + line.initial_qty = sum(layers.mapped('quantity')) + line.initial_value = sum(layers.mapped('value')) + + line.initial_qty = sum(layers.mapped('quantity')) + line.initial_value = sum(layers.mapped('value')) + + # Removed automatic reset of counted/target values to prevent overwriting user input + # This is now handled by _onchange_product_id and action_load_products explicitly. + + def action_calculate(self): + self.ensure_one() + # Re-run initial stock just in case dates changed + self._compute_initial_stock() + + for line in self.line_ids: + # 2. Purchases (Start <= Date <= End) + # Find incoming layers + incoming_layers = self.env['stock.valuation.layer'].search([ + ('product_id', '=', line.product_id.id), + ('create_date', '>=', self.date_start), + ('create_date', '<=', self.date_end), + ('quantity', '>', 0), # Incoming + ('stock_move_id.location_id.usage', '=', 'supplier'), # From Vendor + ('company_id', '=', self.company_id.id) + ]) + + line.purchase_qty = sum(incoming_layers.mapped('quantity')) + line.purchase_value = sum(incoming_layers.mapped('value')) + + # 3. Calculate Weighted Average + total_qty = line.qty_counted + line.purchase_qty + total_value = line.target_initial_value + line.purchase_value + + if not float_is_zero(total_qty, precision_rounding=line.product_id.uom_id.rounding): + line.new_average_cost = total_value / total_qty + else: + line.new_average_cost = 0.0 + + self.state = 'calculated' + + def action_reset_to_draft(self): + self.ensure_one() + self.state = 'draft' + + def action_apply(self): + self.ensure_one() + if self.state != 'calculated': + raise UserError(_("Please calculate first.")) + + for line in self.line_ids: + # 1. Readjust Initial Stock Value (if changed) + initial_diff = line.target_initial_value - line.initial_value + if not float_is_zero(initial_diff, precision_digits=2): + # Create a layer to adjust the starting value + # We simply create a value-only layer. + # Value = initial_diff. + self._create_correction_layer(line.product_id, initial_diff, self.date_start, _("Readjustment: Initial Value")) + + new_cost = line.new_average_cost + + # 2. Update Product Cost + # We bypass the costing method check and force the update + line.product_id.sudo().write({'standard_price': new_cost}) + + # 3. Readjust COGS (Outgoing Moves) + # Find outgoing moves in the period + outgoing_layers = self.env['stock.valuation.layer'].search([ + ('product_id', '=', line.product_id.id), + ('create_date', '>=', self.date_start), + ('create_date', '<=', self.date_end), + ('quantity', '<', 0), # Outgoing + ('company_id', '=', self.company_id.id) + ]) + + # Idempotent Logic: Group by Stock Move to strictly fix the net value + moves = outgoing_layers.mapped('stock_move_id') + + for move in moves: + # Sum ALL layers for this move (Original + Landed Costs + Previous Corrections) + # We need to find all layers linked to this move. + all_layers_for_move = self.env['stock.valuation.layer'].search([ + ('stock_move_id', '=', move.id), + ('product_id', '=', line.product_id.id) + ]) + + # Calculate NET state + total_qty = sum(all_layers_for_move.mapped('quantity')) # Should be negative (e.g. -5) + current_value = sum(all_layers_for_move.mapped('value')) # Should be negative (e.g. -5000) + + # Math using positive numbers for clarity + qty_sold = abs(total_qty) + curr_val_abs = abs(current_value) + + target_value = qty_sold * new_cost + + diff = target_value - curr_val_abs + + # If diff > 0: means Cost SHOULD be higher (e.g. 5500 vs 5000). + # Since stored value is negative (-5000), we need to make it MORE negative (-5500). + # So we must ADD -500 (which is -diff). + + # If diff < 0: means Cost SHOULD be lower (e.g. 4500 vs 5000). + # Stored -5000, want -4500. + # Must ADD +500 (which is -diff). + + # So in all cases, adjustment value = -diff. + + # Only create correction if diff implies a change + if not float_is_zero(diff, precision_digits=2): + self._create_correction_from_move(move, diff) + + self.state = 'done' + + def _create_correction_from_move(self, move, amount): + # Wrapper to use move context + # SVL Value = -amount + + svl_vals = { + 'company_id': self.company_id.id, + 'product_id': move.product_id.id, + 'description': _('Readjustment: %s') % self.name, + 'stock_move_id': move.id, + 'quantity': 0, + 'value': -amount, + 'account_move_id': False, + } + + new_svl = self.env['stock.valuation.layer'].create(svl_vals) + + def _create_correction_from_move(self, move, amount): + # Wrapper to use move context + # SVL Value = -amount + + svl_vals = { + 'company_id': self.company_id.id, + 'product_id': move.product_id.id, + 'description': _('Readjustment: %s') % self.name, + 'stock_move_id': move.id, + 'quantity': 0, + 'value': -amount, + 'account_move_id': False, + } + + new_svl = self.env['stock.valuation.layer'].create(svl_vals) + + # Accounting + product = move.product_id + + # Default Accounts + accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False) + expense_acc = accounts['expense'] + stock_out_acc = accounts['stock_output'] or expense_acc + valuation_acc = accounts['stock_valuation'] + + debit_acc = stock_out_acc.id + credit_acc = valuation_acc.id + + # Attempt to find ACTUAL accounts used in the original move + # We look for an SVL linked to this move that has an account move + original_svls = self.env['stock.valuation.layer'].search([ + ('stock_move_id', '=', move.id), + ('account_move_id', '!=', False) + ], limit=1) + + if original_svls: + am = original_svls.account_move_id + # Analyze lines to find Asset vs Expense + # We assume the move involves the Product. + # Typical Sale: Dr COGS, Cr Asset. + # Asset Account should match the Category's Valuation Account. + + asset_acc_id = valuation_acc.id + found_expense_id = False + + for line in am.line_ids: + if line.account_id.id == asset_acc_id: + continue # This is the asset account + + # If it's not the asset account, and it has the product (optional check), it's likely the COGS + # In POS, it might be granular. + if line.account_id.id != asset_acc_id: + found_expense_id = line.account_id.id + break + + if found_expense_id: + debit_acc = found_expense_id + # credit_acc stays as valuation_acc (Asset) + + if not (debit_acc and credit_acc): + return + + interim_account_id = debit_acc + + # Enhanced POS Detection + is_pos = False + pos_order_obj = False + + if move.picking_id: + # Check if linked to any POS Order + pos_orders = self.env['pos.order'].search([('picking_ids', 'in', move.picking_id.id)], limit=1) + if pos_orders: + is_pos = True + pos_order_obj = pos_orders[0] + elif move.picking_id.pos_order_id: # Direct field check if exists + is_pos = True + pos_order_obj = move.picking_id.pos_order_id + + if not is_pos and 'POS' in move.picking_id.name: + is_pos = True + + if not is_pos and move.origin and ('POS' in move.origin or 'Sesi' in move.origin): + is_pos = True + + if not is_pos and move.reference and 'POS' in move.reference: + is_pos = True + + move_amount = amount + if move_amount < 0: + debit_acc, credit_acc = credit_acc, debit_acc + move_amount = abs(move_amount) + + move_vals = { + 'journal_id': self.journal_id.id, + 'date': self.date_end.date(), # Use End Date for Accounting + 'ref': f"{self.name} - Adj for {move.name}", + 'move_type': 'entry', + 'line_ids': [ + (0, 0, { + 'name': _('Readjustment COGS'), + 'account_id': debit_acc, + 'debit': move_amount, + 'credit': 0, + 'product_id': product.id, + }), + (0, 0, { + 'name': _('Readjustment Valuation'), + 'account_id': credit_acc, + 'debit': 0, + 'credit': move_amount, + 'product_id': product.id, + }), + ] + } + + account_move = self.env['account.move'].create(move_vals) + account_move.action_post() + + self.account_move_ids = [(4, account_move.id)] + + new_svl.account_move_id = account_move.id + + # Force Date + # SVL keeps the Move Date (for valuation history) + self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (move.date, new_svl.id)) + # Account Move uses End Date + self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (self.date_end.date(), account_move.id)) + + # SECONDARY CORRECTION FOR POS (Interim -> Final Expense) + # If this was a POS order, the Stock Move only hit the Interim Account (Stock Output). + # The POS Session later moved it to the Final Expense Account. + # We must propagate the correction to the Final Expense. + if is_pos and interim_account_id != expense_acc.id: + interim_acc = interim_account_id + final_acc = expense_acc.id + + # Logic: + # If Amt < 0 (Cost Decreased): + # We want to Credit Expense. + # Primary Correction Check: Dr Asset, Cr Interim. + # Secondary Correction: Dr Interim, Cr Expense. + + sec_debit_acc = interim_acc + sec_credit_acc = final_acc + sec_amount = abs(amount) + + if amount > 0: + # Cost Increased. + # Primary: Dr Interim, Cr Asset. + # Secondary: Dr Expense, Cr Interim. + sec_debit_acc = final_acc + sec_credit_acc = interim_acc + + ref_text = f"{self.name} - POS Adj for {move.name}" + if pos_order_obj: + if pos_order_obj.session_id: + ref_text += f" ({pos_order_obj.session_id.name})" + else: + ref_text += f" ({pos_order_obj.name})" + + move_vals_2 = { + 'journal_id': self.journal_id.id, + 'date': self.date_end.date(), # Use End Date + 'ref': ref_text, + 'move_type': 'entry', + 'line_ids': [ + (0, 0, { + 'name': _('Readjustment POS Expense'), + 'account_id': sec_debit_acc, + 'debit': sec_amount, + 'credit': 0, + 'product_id': product.id, + }), + (0, 0, { + 'name': _('Readjustment POS Expense'), + 'account_id': sec_credit_acc, + 'debit': 0, + 'credit': sec_amount, + 'product_id': product.id, + }), + ] + } + sec_move = self.env['account.move'].create(move_vals_2) + sec_move.action_post() + self.account_move_ids = [(4, sec_move.id)] + self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (self.date_end.date(), sec_move.id)) + + def _create_correction_layer(self, product, amount, date, description): + """ Create a generic correction layer (e.g. for Initial Stock) """ + svl_vals = { + 'company_id': self.company_id.id, + 'product_id': product.id, + 'description': description, + 'stock_move_id': False, + 'quantity': 0, + 'value': amount, + 'account_move_id': False, + } + new_svl = self.env['stock.valuation.layer'].create(svl_vals) + + # Accounting + accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False) + valuation_acc = accounts['stock_valuation'] + expense_acc = accounts['expense'] # Or we can use a specific revaluation account from Journal? + # Let's use the Journal's account if possible, or Fallback to Expense/Stock Output + + if not (valuation_acc and expense_acc): + return + + # Positive Amount = Value Increase = Dr Inventory, Cr Expense/Gain + debit_acc = valuation_acc.id + credit_acc = expense_acc.id + + move_amount = amount + if move_amount < 0: + # Value Decrease = Dr Expense/Loss, Cr Inventory + debit_acc, credit_acc = credit_acc, debit_acc + move_amount = abs(move_amount) + + move_vals = { + 'journal_id': self.journal_id.id, + 'date': date.date(), + 'ref': f"{self.name} - {description}", + 'move_type': 'entry', + 'line_ids': [ + (0, 0, { + 'name': description, + 'account_id': debit_acc, + 'debit': move_amount, + 'credit': 0, + 'product_id': product.id, + }), + (0, 0, { + 'name': description, + 'account_id': credit_acc, + 'debit': 0, + 'credit': move_amount, + 'product_id': product.id, + }), + ] + } + + move = self.env['account.move'].create(move_vals) + move.action_post() + self.account_move_ids = [(4, move.id)] + + new_svl.account_move_id = move.id + + # Force Date + self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (date, new_svl.id)) + self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (date.date(), move.id)) + + # SECONDARY CORRECTION FOR POS (Interim -> Final Expense) + # If this was a POS order, the Stock Move only hit the Interim Account (Stock Output). + # The POS Session later moved it to the Final Expense Account. + # We must propagate the correction to the Final Expense. + if move.picking_id.pos_order_id and stock_out_acc.id != expense_acc.id: + interim_acc = stock_out_acc.id + final_acc = expense_acc.id + + # Logic: + # If Amt < 0 (Cost Decreased): + # We want to Credit Expense. + # Primary Correction Check: Dr Asset, Cr Interim. + # Secondary Correction: Dr Interim, Cr Expense. + + sec_debit_acc = interim_acc + sec_credit_acc = final_acc + sec_amount = abs(amount) + + if amount > 0: + # Cost Increased. + # Primary: Dr Interim, Cr Asset. + # Secondary: Dr Expense, Cr Interim. + sec_debit_acc = final_acc + sec_credit_acc = interim_acc + + move_vals_2 = { + 'journal_id': self.journal_id.id, + 'date': move.date.date(), + 'ref': f"{self.name} - POS Adj for {move.name}", + 'move_type': 'entry', + 'line_ids': [ + (0, 0, { + 'name': _('Readjustment POS Expense'), + 'account_id': sec_debit_acc, + 'debit': sec_amount, + 'credit': 0, + 'product_id': product.id, + }), + (0, 0, { + 'name': _('Readjustment POS Expense'), + 'account_id': sec_credit_acc, + 'debit': 0, + 'credit': sec_amount, + 'product_id': product.id, + }), + ] + } + sec_move = self.env['account.move'].create(move_vals_2) + sec_move.action_post() + self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (move.date.date(), sec_move.id)) + + def _create_correction_layer(self, product, amount, date, description): + """ Create a generic correction layer (e.g. for Initial Stock) """ + svl_vals = { + 'company_id': self.company_id.id, + 'product_id': product.id, + 'description': description, + 'stock_move_id': False, + 'quantity': 0, + 'value': amount, + 'account_move_id': False, + } + new_svl = self.env['stock.valuation.layer'].create(svl_vals) + + # Accounting + accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False) + valuation_acc = accounts['stock_valuation'] + expense_acc = accounts['expense'] # Or we can use a specific revaluation account from Journal? + # Let's use the Journal's account if possible, or Fallback to Expense/Stock Output + + if not (valuation_acc and expense_acc): + return + + # Positive Amount = Value Increase = Dr Inventory, Cr Expense/Gain + debit_acc = valuation_acc.id + credit_acc = expense_acc.id + + move_amount = amount + if move_amount < 0: + # Value Decrease = Dr Expense/Loss, Cr Inventory + debit_acc, credit_acc = credit_acc, debit_acc + move_amount = abs(move_amount) + + move_vals = { + 'journal_id': self.journal_id.id, + 'date': date.date(), + 'ref': f"{self.name} - {description}", + 'move_type': 'entry', + 'line_ids': [ + (0, 0, { + 'name': description, + 'account_id': debit_acc, + 'debit': move_amount, + 'credit': 0, + 'product_id': product.id, + }), + (0, 0, { + 'name': description, + 'account_id': credit_acc, + 'debit': 0, + 'credit': move_amount, + 'product_id': product.id, + }), + ] + } + + move = self.env['account.move'].create(move_vals) + move.action_post() + + new_svl.account_move_id = move.id + + # Force Date + self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (date, new_svl.id)) + self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (date.date(), move.id)) + + def _create_correction(self, layer, amount): + """ Create SVL and AM correction for the given layer """ + # Only adjust if amount is non-zero + + # SVL + svl_vals = { + 'company_id': self.company_id.id, + 'product_id': layer.product_id.id, + 'description': _('Readjustment: %s') % self.name, + 'stock_move_id': layer.stock_move_id.id, + 'quantity': 0, + 'value': -amount, # Adjustment to the ASSET value on the layer? + # Wait. Outgoing layer has negative value (e.g. -100). + # If correct cost is higher (e.g. 110), value should be -110. + # So we need to add -10. + # 'amount' coming in is (Correct - Current). + # (110 - 100) = 10 (Positive magnitude). + # Since layer.value is negative, we want to make it MORE negative. + # So we should add -amount to the asset. + 'account_move_id': False, # Linked below + } + + # If amount is positive (Cost increased): + # We need to Credit Asset, Debit COGS. + # Layer Value change: Negative. + + pass + # Actually let's reuse logic from revaluation. + # If Cost Increased: We under-expensed. Need to expense more. + # Dr COGS, Cr Asset. + # Asset Value Change = -Amount. + + # If Cost Decreased: We over-expensed. Need to return to asset. + # Dr Asset, Cr COGS. + # Asset Value Change = +Amount. + + # My 'diff' calculation: Correct (big) - Current (small) = Positive. + # Means Correct Cost > Current cost. + # So we need to reduce Asset (Cr) and increase COGS (Dr). + # So SVL value should be -diff. + + svl_vals['value'] = -amount + + new_svl = self.env['stock.valuation.layer'].create(svl_vals) + + # Accounting Entry + account_moves = layer.account_move_id + if not account_moves: + return + + # We find the original accounts used. + # Or look up standard accounts. + product = layer.product_id + accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=False) + expense_acc = accounts['expense'] + stock_out_acc = accounts['stock_output'] or expense_acc + valuation_acc = accounts['stock_valuation'] + + if not (stock_out_acc and valuation_acc): + return # Skip if no accounts + + # Dr COGS (stock_out), Cr Valuation + debit_acc = stock_out_acc.id + credit_acc = valuation_acc.id + + move_amount = amount + + if move_amount < 0: + # Cost Decreased. Reverse. Dr Valuation, Cr COGS. + debit_acc, credit_acc = credit_acc, debit_acc + move_amount = abs(move_amount) + + move_vals = { + 'journal_id': self.journal_id.id, + 'date': layer.create_date.date(), # Backdate + 'ref': f"{self.name} - Adj for {layer.stock_move_id.name}", + 'move_type': 'entry', + 'line_ids': [ + (0, 0, { + 'name': _('Readjustment COGS'), + 'account_id': debit_acc, + 'debit': move_amount, + 'credit': 0, + 'product_id': product.id, + }), + (0, 0, { + 'name': _('Readjustment Valuation'), + 'account_id': credit_acc, + 'debit': 0, + 'credit': move_amount, + 'product_id': product.id, + }), + ] + } + + move = self.env['account.move'].create(move_vals) + move.action_post() + + new_svl.account_move_id = move.id + + # Force Date + self.env.cr.execute('UPDATE stock_valuation_layer SET create_date = %s WHERE id = %s', (layer.create_date, new_svl.id)) + self.env.cr.execute('UPDATE account_move SET date = %s WHERE id = %s', (layer.create_date.date(), move.id)) + + +class StockReadjustValuationLine(models.Model): + _name = 'stock.readjust.valuation.line' + _description = 'Line for Readjustment' + + readjust_id = fields.Many2one('stock.readjust.valuation', required=True, ondelete='cascade') + product_id = fields.Many2one('product.product', required=True) + state = fields.Selection(related='readjust_id.state') + + initial_qty = fields.Float(string='Qty at Start', readonly=True) + qty_counted = fields.Float(string='Qty Counted') + + initial_value = fields.Float(string='Valuation at Start', readonly=True) + target_initial_value = fields.Float(string='Target Valuation') + + purchase_qty = fields.Float(string='Purchase Qty', readonly=True) + purchase_value = fields.Float(string='Purchase Value', readonly=True) + + new_average_cost = fields.Float(string='New Average Cost', readonly=True) + + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id and self.readjust_id.date_start: + # Calculate Initial Stock + layers = self.env['stock.valuation.layer'].search([ + ('product_id', '=', self.product_id.id), + ('create_date', '<', self.readjust_id.date_start), + ('company_id', '=', self.readjust_id.company_id.id) + ]) + self.initial_qty = sum(layers.mapped('quantity')) + self.initial_value = sum(layers.mapped('value')) + + self.qty_counted = self.initial_qty + self.target_initial_value = self.initial_value diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..cb50fcf --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_readjust_valuation,stock.readjust.valuation,model_stock_readjust_valuation,,1,1,1,1 +access_stock_readjust_valuation_line,stock.readjust.valuation.line,model_stock_readjust_valuation_line,,1,1,1,1 diff --git a/views/stock_readjust_valuation_views.xml b/views/stock_readjust_valuation_views.xml new file mode 100644 index 0000000..a0ceec3 --- /dev/null +++ b/views/stock_readjust_valuation_views.xml @@ -0,0 +1,97 @@ + + + + + + stock.readjust.valuation.form + stock.readjust.valuation + +
+
+
+ +
+ +
+
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + + stock.readjust.valuation.tree + stock.readjust.valuation + + + + + + + + + + + + + + Stock Readjustment + stock.readjust.valuation + tree,form + + + + + +
+