first commit
This commit is contained in:
commit
16d9d42ad9
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.vscode/
|
||||||
47
README.md
Normal file
47
README.md
Normal file
@ -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.
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
21
__manifest__.py
Normal file
21
__manifest__.py
Normal file
@ -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',
|
||||||
|
}
|
||||||
12
data/ir_sequence_data.xml
Normal file
12
data/ir_sequence_data.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="seq_stock_readjust_valuation" model="ir.sequence">
|
||||||
|
<field name="name">Stock Readjustment</field>
|
||||||
|
<field name="code">stock.readjust.valuation</field>
|
||||||
|
<field name="prefix">ADJ/</field>
|
||||||
|
<field name="padding">5</field>
|
||||||
|
<field name="company_id" eval="False"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
1
models/__init__.py
Normal file
1
models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import stock_readjust_valuation
|
||||||
725
models/stock_readjust_valuation.py
Normal file
725
models/stock_readjust_valuation.py
Normal file
@ -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
|
||||||
3
security/ir.model.access.csv
Normal file
3
security/ir.model.access.csv
Normal file
@ -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
|
||||||
|
97
views/stock_readjust_valuation_views.xml
Normal file
97
views/stock_readjust_valuation_views.xml
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_stock_readjust_valuation_form" model="ir.ui.view">
|
||||||
|
<field name="name">stock.readjust.valuation.form</field>
|
||||||
|
<field name="model">stock.readjust.valuation</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Stock Readjustment">
|
||||||
|
<header>
|
||||||
|
<button name="action_load_products" string="Load Products" type="object" class="oe_highlight" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_calculate" string="Calculate" type="object" class="oe_highlight" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_reset_to_draft" string="Reset to Draft" type="object" invisible="state != 'calculated'"/>
|
||||||
|
<button name="action_apply" string="Apply Readjustment" type="object" class="oe_highlight" invisible="state != 'calculated'"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,calculated,done"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_view_journal_entries" type="object" class="oe_stat_button" icon="fa-bars" invisible="account_move_count == 0">
|
||||||
|
<field name="account_move_count" widget="statinfo" string="Journal Entries"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1><field name="name"/></h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="date_start" readonly="state != 'draft'"/>
|
||||||
|
<field name="date_end" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="journal_id" readonly="state != 'draft'"/>
|
||||||
|
<field name="company_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<page string="Products">
|
||||||
|
<field name="line_ids" readonly="state == 'done'">
|
||||||
|
<tree editable="bottom" create="1" delete="1">
|
||||||
|
<field name="product_id" readonly="parent.state != 'draft'"/>
|
||||||
|
|
||||||
|
<field name="initial_qty"/>
|
||||||
|
<field name="qty_counted" decoration-info="qty_counted != initial_qty"/>
|
||||||
|
|
||||||
|
<field name="initial_value"/>
|
||||||
|
<field name="target_initial_value" decoration-info="target_initial_value != initial_value"/>
|
||||||
|
|
||||||
|
<field name="purchase_qty" optional="show"/>
|
||||||
|
<field name="purchase_value" optional="show"/>
|
||||||
|
|
||||||
|
<field name="new_average_cost" decoration-bf="1"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<div class="oe_chatter">
|
||||||
|
<field name="message_follower_ids"/>
|
||||||
|
<field name="activity_ids"/>
|
||||||
|
<field name="message_ids"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Tree View -->
|
||||||
|
<record id="view_stock_readjust_valuation_tree" model="ir.ui.view">
|
||||||
|
<field name="name">stock.readjust.valuation.tree</field>
|
||||||
|
<field name="model">stock.readjust.valuation</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="date_start"/>
|
||||||
|
<field name="date_end"/>
|
||||||
|
<field name="journal_id"/>
|
||||||
|
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-success="state == 'done'"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_stock_readjust_valuation" model="ir.actions.act_window">
|
||||||
|
<field name="name">Stock Readjustment</field>
|
||||||
|
<field name="res_model">stock.readjust.valuation</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
|
<menuitem id="menu_stock_readjust_valuation"
|
||||||
|
name="Stock Readjustment"
|
||||||
|
parent="stock.menu_stock_warehouse_mgmt"
|
||||||
|
action="action_stock_readjust_valuation"
|
||||||
|
sequence="110"/>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user