[REF] stock_account: remove old valuation code

The new code was sitting next to the old one, now that everything is
adapted (minus the cost method changes), we can remove the old one.

We also adapt the valuation wizard to use the stock valuation layer
action, now that the others are removed.

task-1875873
This commit is contained in:
Simon Lejeune
2019-06-10 20:42:59 +00:00
parent e93ede220d
commit 6fd05759e7
10 changed files with 39 additions and 696 deletions
-1
View File
@@ -5,7 +5,6 @@ from . import account_chart_template
from . import account_invoice
from . import account_move
from . import account_reconciliation_widget
from . import procurement_group
from . import product
from . import stock_move
from . import stock_inventory
@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class ProcurementGroup(models.Model):
_inherit = 'procurement.group'
@api.model
def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False):
super(ProcurementGroup, self)._run_scheduler_tasks(use_new_cursor=use_new_cursor, company_id=company_id)
self.env['stock.move']._run_fifo_vacuum()
+1 -226
View File
@@ -32,11 +32,6 @@ class ProductTemplate(models.Model):
})
return accounts
@api.multi
def action_open_product_moves(self):
# TODO: remove me in master
pass
@api.multi
def get_product_accounts(self, fiscal_pos=None):
""" Add the stock journal related to product to the result of super()
@@ -50,16 +45,6 @@ class ProductTemplate(models.Model):
class ProductProduct(models.Model):
_inherit = 'product.product'
stock_value_currency_id = fields.Many2one('res.currency', compute='_compute_stock_value_currency')
stock_value = fields.Float(
'Value', compute='_compute_stock_value')
qty_at_date = fields.Float(
'Quantity', compute='_compute_stock_value')
stock_fifo_real_time_aml_ids = fields.Many2many(
'account.move.line', compute='_compute_stock_value')
stock_fifo_manual_move_ids = fields.Many2many(
'stock.move', compute='_compute_stock_value')
value_svl = fields.Float(compute='_compute_value_svl')
quantity_svl = fields.Float(compute='_compute_value_svl')
stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'product_id')
@@ -331,202 +316,8 @@ class ProductProduct(models.Model):
vacuum_svl.stock_move_id.with_context(svl_id=vacuum_svl.id, force_valuation_amount=vacuum_svl.value, forced_quantity=vacuum_svl.quantity, forced_ref=vacuum_svl.description)._account_entry_move()
# -------------------------------------------------------------------------
# Miscellaneous
# Anglo saxon helpers
# -------------------------------------------------------------------------
@api.multi
def do_change_standard_price(self, new_price, account_id):
""" Changes the Standard Price of Product and creates an account move accordingly."""
AccountMove = self.env['account.move']
quant_locs = self.env['stock.quant'].sudo().read_group([('product_id', 'in', self.ids)], ['location_id'], ['location_id'])
quant_loc_ids = [loc['location_id'][0] for loc in quant_locs]
locations = self.env['stock.location'].search([('usage', '=', 'internal'), ('company_id', '=', self.env.company.id), ('id', 'in', quant_loc_ids)])
product_accounts = {product.id: product.product_tmpl_id.get_product_accounts() for product in self}
for location in locations:
for product in self.with_context(location=location.id, compute_child=False).filtered(lambda r: r.valuation == 'real_time'):
diff = product.standard_price - new_price
if float_is_zero(diff, precision_rounding=product.currency_id.rounding):
raise UserError(_("No difference between the standard price and the new price."))
if not product_accounts[product.id].get('stock_valuation', False):
raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'))
qty_available = product.qty_available
if qty_available:
# Accounting Entries
if diff * qty_available > 0:
debit_account_id = account_id
credit_account_id = product_accounts[product.id]['stock_valuation'].id
else:
debit_account_id = product_accounts[product.id]['stock_valuation'].id
credit_account_id = account_id
move_vals = {
'journal_id': product_accounts[product.id]['stock_journal'].id,
'company_id': location.company_id.id,
'ref': product.default_code,
'line_ids': [(0, 0, {
'name': _('%s changed cost from %s to %s - %s') % (self.env.user.name, product.standard_price, new_price, product.display_name),
'account_id': debit_account_id,
'debit': abs(diff * qty_available),
'credit': 0,
'product_id': product.id,
}), (0, 0, {
'name': _('%s changed cost from %s to %s - %s') % (self.env.user.name, product.standard_price, new_price, product.display_name),
'account_id': credit_account_id,
'debit': 0,
'credit': abs(diff * qty_available),
'product_id': product.id,
})],
}
move = AccountMove.create(move_vals)
move.post()
self.write({'standard_price': new_price})
return True
def _get_fifo_candidates_in_move(self):
""" Find IN moves that can be used to value OUT moves.
"""
return self._get_fifo_candidates_in_move_with_company()
def _get_fifo_candidates_in_move_with_company(self, move_company_id=False):
self.ensure_one()
domain = [('product_id', '=', self.id), ('remaining_qty', '>', 0.0)] + self.env['stock.move']._get_in_base_domain(move_company_id)
candidates = self.env['stock.move'].search(domain, order='date, id')
return candidates
def _sum_remaining_values(self):
StockMove = self.env['stock.move']
domain = [('product_id', '=', self.id)] + StockMove._get_all_base_domain()
moves = StockMove.search(domain)
return sum(moves.mapped('remaining_value')), moves
@api.multi
def _compute_stock_value_currency(self):
currency_id = self.env.company.currency_id
for product in self:
product.stock_value_currency_id = currency_id
@api.multi
@api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'stock_move_ids.remaining_value', 'product_tmpl_id.cost_method', 'product_tmpl_id.standard_price', 'product_tmpl_id.categ_id.property_valuation')
def _compute_stock_value(self):
StockMove = self.env['stock.move']
to_date = self.env.context.get('to_date')
real_time_product_ids = [product.id for product in self if product.product_tmpl_id.valuation == 'real_time']
if real_time_product_ids:
self.env['account.move.line'].check_access_rights('read')
fifo_automated_values = {}
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
FROM account_move_line AS aml
WHERE aml.product_id IN %%s AND aml.company_id=%%s %s
GROUP BY aml.product_id, aml.account_id"""
params = (tuple(real_time_product_ids), self.env.company.id)
if to_date:
query = query % ('AND aml.date <= %s',)
params = params + (to_date,)
else:
query = query % ('',)
self.env.cr.execute(query, params=params)
res = self.env.cr.fetchall()
for row in res:
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
product_values = {product.id: 0 for product in self}
product_move_ids = {product.id: [] for product in self}
if to_date:
domain = [('product_id', 'in', self.ids), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
value_field_name = 'value'
else:
domain = [('product_id', 'in', self.ids)] + StockMove._get_all_base_domain()
value_field_name = 'remaining_value'
StockMove.check_access_rights('read')
query = StockMove._where_calc(domain)
StockMove._apply_ir_rules(query, 'read')
from_clause, where_clause, params = query.get_sql()
query_str = """
SELECT stock_move.product_id, SUM(COALESCE(stock_move.{}, 0.0)), ARRAY_AGG(stock_move.id)
FROM {}
WHERE {}
GROUP BY stock_move.product_id
""".format(value_field_name, from_clause, where_clause)
self.env.cr.execute(query_str, params)
for product_id, value, move_ids in self.env.cr.fetchall():
product_values[product_id] = value
product_move_ids[product_id] = move_ids
for product in self:
if product.cost_method in ['standard', 'average']:
qty_available = product.with_context(company_owned=True, owner_id=False).qty_available
price_used = product.standard_price
if to_date:
price_used = product.get_history_price(
self.env.company.id,
date=to_date,
)
product.stock_value = price_used * qty_available
product.qty_at_date = qty_available
elif product.cost_method == 'fifo':
if to_date:
if product.product_tmpl_id.valuation == 'manual_periodic':
product.stock_value = product_values[product.id]
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product.id])
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
product.stock_value = value
product.qty_at_date = quantity
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
else:
product.stock_value = product_values[product.id]
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
if product.product_tmpl_id.valuation == 'manual_periodic':
product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product.id])
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
def action_valuation_at_date_details(self):
""" Returns an action with either a list view of all the valued stock moves of `self` if the
valuation is set as manual or a list view of all the account move lines if the valuation is
set as automated.
"""
self.ensure_one()
to_date = self.env.context.get('to_date')
ctx = self.env.context.copy()
ctx.pop('group_by', None)
action = {
'name': _('Valuation at date'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'context': ctx,
}
if self.valuation == 'real_time':
action['res_model'] = 'account.move.line'
action['domain'] = [('id', 'in', self.with_context(to_date=to_date).stock_fifo_real_time_aml_ids.ids)]
tree_view_ref = self.env.ref('stock_account.view_stock_account_aml')
form_view_ref = self.env.ref('account.view_move_line_form')
action['views'] = [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')]
else:
action['res_model'] = 'stock.move'
action['domain'] = [('id', 'in', self.with_context(to_date=to_date).stock_fifo_manual_move_ids.ids)]
tree_view_ref = self.env.ref('stock_account.view_move_tree_valuation_at_date')
form_view_ref = self.env.ref('stock.view_move_form')
action['views'] = [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')]
return action
@api.multi
def action_open_product_moves(self):
#TODO: remove me in master
pass
@api.model
def _anglo_saxon_sale_move_lines(self, name, product, uom, qty, price_unit, currency=False, amount_currency=False, fiscal_position=False, account_analytic=False, analytic_tags=False):
"""Prepare dicts describing new journal COGS journal items for a product sale.
@@ -661,14 +452,6 @@ class ProductCategory(models.Model):
input_and_output_accounts = category.property_stock_account_input_categ_id | category.property_stock_account_output_categ_id
if valuation_account and valuation_account in input_and_output_accounts:
raise ValidationError(_('The Stock Input and/or Output accounts cannot be the same than the Stock Valuation account.'))
@api.multi
def write(self, vals):
# When going from FIFO to AVCO or to standard, we update the standard price with the
# average value in stock.
cost_method = vals.get('property_cost_method')
if cost_method and cost_method in ['average', 'standard']:
self._update_standard_price()
return super(ProductCategory, self).write(vals)
@api.onchange('property_cost_method')
def onchange_property_valuation(self):
@@ -682,11 +465,3 @@ class ProductCategory(models.Model):
}
}
def _update_standard_price(self):
updated_categories = self.filtered(lambda x: x.property_cost_method == 'fifo')
templates = self.env['product.template'].search([('categ_id', 'in', updated_categories.ids)])
for t in templates:
valuation = sum([variant._sum_remaining_values()[0] for variant in t.product_variant_ids])
qty_available = t.with_context(company_owned=True).qty_available
if qty_available:
t.standard_price = valuation / qty_available
+1 -275
View File
@@ -16,9 +16,6 @@ class StockMove(models.Model):
to_refund = fields.Boolean(string="Update quantities on SO/PO", copy=False,
help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order')
value = fields.Float(copy=False)
remaining_qty = fields.Float(copy=False)
remaining_value = fields.Float(copy=False)
account_move_ids = fields.One2many('account.move', 'stock_move_id')
stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_move_id')
@@ -41,61 +38,6 @@ class StockMove(models.Model):
price_unit = self.origin_returned_move_id.price_unit
return not self.company_id.currency_id.is_zero(price_unit) and price_unit or self.product_id.standard_price
@api.model
def _get_in_base_domain(self, company_id=False):
# Domain:
# - state is done
# - coming from a location without company, or an inventory location within the same company
# - going to a location within the same company
domain = [
('state', '=', 'done'),
'&',
'|',
('location_id.company_id', '=', False),
'&',
('location_id.usage', 'in', ['inventory', 'production']),
('location_id.company_id', '=', company_id or self.env.company.id),
('location_dest_id.company_id', '=', company_id or self.env.company.id),
]
return domain
@api.model
def _get_all_base_domain(self, company_id=False):
# Domain:
# - state is done
# Then, either 'in' or 'out' moves.
# 'in' moves:
# - coming from a location without company, or an inventory location within the same company
# - going to a location within the same company
# 'out' moves:
# - coming from to a location within the same company
# - going to a location without company, or an inventory location within the same company
domain = [
('state', '=', 'done'),
'|',
'&',
'|',
('location_id.company_id', '=', False),
'&',
('location_id.usage', 'in', ['inventory', 'production']),
('location_id.company_id', '=', company_id or self.env.company.id),
('location_dest_id.company_id', '=', company_id or self.env.company.id),
'&',
('location_id.company_id', '=', company_id or self.env.company.id),
'|',
('location_dest_id.company_id', '=', False),
'&',
('location_dest_id.usage', '=', 'inventory'),
('location_dest_id.company_id', '=', company_id or self.env.company.id),
]
return domain
def _get_in_domain(self):
return [('product_id', '=', self.product_id.id)] + self._get_in_base_domain(company_id=self.company_id.id)
def _get_all_domain(self):
return [('product_id', '=', self.product_id.id)] + self._get_all_base_domain(company_id=self.company_id.id)
@api.model
def _get_valued_types(self):
"""Returns a list of `valued_type` as strings. During `action_done`, we'll call
@@ -287,140 +229,6 @@ class StockMove(models.Model):
"""
return self._create_dropshipped_svl(forced_quantity=forced_quantity)
@api.model
def _run_fifo(self, move, quantity=None):
""" Value `move` according to the FIFO rule, meaning we consume the
oldest receipt first. Candidates receipts are marked consumed or free
thanks to their `remaining_qty` and `remaining_value` fields.
By definition, `move` should be an outgoing stock move.
:param quantity: quantity to value instead of `move.product_qty`
:returns: valued amount in absolute
"""
move.ensure_one()
# Deal with possible move lines that do not impact the valuation.
valued_move_lines = move.move_line_ids.filtered(lambda ml: ml.location_id._should_be_valued() and not ml.location_dest_id._should_be_valued() and not ml.owner_id)
valued_quantity = 0
for valued_move_line in valued_move_lines:
valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, move.product_id.uom_id)
# Find back incoming stock moves (called candidates here) to value this move.
qty_to_take_on_candidates = quantity or valued_quantity
candidates = move.product_id._get_fifo_candidates_in_move_with_company(move.company_id.id)
new_standard_price = 0
tmp_value = 0 # to accumulate the value taken on the candidates
for candidate in candidates:
new_standard_price = candidate.price_unit
if candidate.remaining_qty <= qty_to_take_on_candidates:
qty_taken_on_candidate = candidate.remaining_qty
else:
qty_taken_on_candidate = qty_to_take_on_candidates
# As applying a landed cost do not update the unit price, naivelly doing
# something like qty_taken_on_candidate * candidate.price_unit won't make
# the additional value brought by the landed cost go away.
candidate_price_unit = candidate.remaining_value / candidate.remaining_qty
value_taken_on_candidate = qty_taken_on_candidate * candidate_price_unit
candidate_vals = {
'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate,
'remaining_value': candidate.remaining_value - value_taken_on_candidate,
}
candidate.write(candidate_vals)
qty_to_take_on_candidates -= qty_taken_on_candidate
tmp_value += value_taken_on_candidate
if qty_to_take_on_candidates == 0:
break
# Update the standard price with the price of the last used candidate, if any.
if new_standard_price and move.product_id.cost_method == 'fifo':
move.product_id.sudo().with_context(force_company=move.company_id.id) \
.standard_price = new_standard_price
# If there's still quantity to value but we're out of candidates, we fall in the
# negative stock use case. We chose to value the out move at the price of the
# last out and a correction entry will be made once `_fifo_vacuum` is called.
if qty_to_take_on_candidates == 0:
move.write({
'value': -tmp_value if not quantity else move.value or -tmp_value, # outgoing move are valued negatively
'price_unit': -tmp_value / (move.product_qty or quantity),
})
elif qty_to_take_on_candidates > 0:
last_fifo_price = new_standard_price or move.product_id.standard_price
negative_stock_value = last_fifo_price * -qty_to_take_on_candidates
tmp_value += abs(negative_stock_value)
vals = {
'remaining_qty': move.remaining_qty + -qty_to_take_on_candidates,
'remaining_value': move.remaining_value + negative_stock_value,
'value': -tmp_value,
'price_unit': -1 * last_fifo_price,
}
move.write(vals)
return tmp_value
def _run_valuation(self, quantity=None):
self.ensure_one()
value_to_return = 0
if self._is_in():
valued_move_lines = self._get_in_move_lines()
valued_quantity = 0
for valued_move_line in valued_move_lines:
valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, self.product_id.uom_id)
# Note: we always compute the fifo `remaining_value` and `remaining_qty` fields no
# matter which cost method is set, to ease the switching of cost method.
vals = {}
price_unit = self._get_price_unit()
value = price_unit * (quantity or valued_quantity)
value_to_return = value if quantity is None or not self.value else self.value
vals = {
'price_unit': price_unit,
'value': value_to_return,
'remaining_value': value if quantity is None else self.remaining_value + value,
}
vals['remaining_qty'] = valued_quantity if quantity is None else self.remaining_qty + quantity
if self.product_id.cost_method == 'standard':
value = self.product_id.standard_price * (quantity or valued_quantity)
value_to_return = value if quantity is None or not self.value else self.value
vals.update({
'price_unit': self.product_id.standard_price,
'value': value_to_return,
})
self.write(vals)
elif self._is_out():
valued_move_lines = self._get_out_move_lines()
valued_quantity = 0
for valued_move_line in valued_move_lines:
valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, self.product_id.uom_id)
self.env['stock.move']._run_fifo(self, quantity=quantity)
if self.product_id.cost_method in ['standard', 'average']:
curr_rounding = self.company_id.currency_id.rounding
value = -float_round(self.product_id.standard_price * (valued_quantity if quantity is None else quantity), precision_rounding=curr_rounding)
value_to_return = value if quantity is None else self.value + value
self.write({
'value': value_to_return,
'price_unit': value / valued_quantity,
})
elif self._is_dropshipped() or self._is_dropshipped_returned():
curr_rounding = self.company_id.currency_id.rounding
if self.product_id.cost_method in ['fifo']:
price_unit = self._get_price_unit()
# see test_dropship_fifo_perpetual_anglosaxon_ordered
self.product_id.standard_price = price_unit
else:
price_unit = self.product_id.standard_price
value = float_round(self.product_qty * price_unit, precision_rounding=curr_rounding)
value_to_return = value if self._is_dropshipped() else -value
# In move have a positive value, out move have a negative value, let's arbitrary say
# dropship are positive.
self.write({
'value': value_to_return,
'price_unit': price_unit if self._is_dropshipped() else -price_unit,
})
return value_to_return
def _action_done(self, cancel_backorder=False):
# Init a dict that will group the moves by valuation type, according to `move._is_valued_type`.
valued_moves = {valued_type: self.env['stock.move'] for valued_type in self._get_valued_types()}
@@ -504,88 +312,6 @@ class StockMove(models.Model):
move.product_id.with_context(force_company=move.company_id.id).sudo().write({'standard_price': new_std_price})
std_price_update[move.company_id.id, move.product_id.id] = new_std_price
@api.model
def _fifo_vacuum(self):
""" Every moves that need to be fixed are identifiable by having a negative `remaining_qty`.
"""
for move in self.filtered(lambda m: (m._is_in() or m._is_out()) and m.remaining_qty < 0):
domain = [
('remaining_qty', '>', 0),
'|',
('date', '>', move.date),
'&',
('date', '=', move.date),
('id', '>', move.id)
]
domain += move._get_in_domain()
candidates = self.search(domain, order='date, id')
if not candidates:
continue
qty_to_take_on_candidates = abs(move.remaining_qty)
qty_taken_on_candidates = 0
tmp_value = 0
for candidate in candidates:
if candidate.remaining_qty <= qty_to_take_on_candidates:
qty_taken_on_candidate = candidate.remaining_qty
else:
qty_taken_on_candidate = qty_to_take_on_candidates
qty_taken_on_candidates += qty_taken_on_candidate
value_taken_on_candidate = qty_taken_on_candidate * candidate.price_unit
candidate_vals = {
'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate,
'remaining_value': candidate.remaining_value - value_taken_on_candidate,
}
candidate.write(candidate_vals)
qty_to_take_on_candidates -= qty_taken_on_candidate
tmp_value += value_taken_on_candidate
if qty_to_take_on_candidates == 0:
break
# When working with `price_unit`, beware that out move are negative.
move_price_unit = move.price_unit if move._is_out() else -1 * move.price_unit
# Get the estimated value we will correct.
remaining_value_before_vacuum = qty_taken_on_candidates * move_price_unit
new_remaining_qty = move.remaining_qty + qty_taken_on_candidates
new_remaining_value = new_remaining_qty * abs(move.price_unit)
corrected_value = remaining_value_before_vacuum + tmp_value
move.write({
'remaining_value': new_remaining_value,
'remaining_qty': new_remaining_qty,
'value': move.value - corrected_value,
})
if move.product_id.valuation == 'real_time':
# If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We
# force the amount in the context, but in the case it is 0 it'll create an entry
# for the entire cost of the move. This case happens when the candidates moves
# entirely compensate the problematic move.
if move.company_id.currency_id.is_zero(corrected_value):
continue
if move._is_in():
# If we just compensated an IN move that has a negative remaining
# quantity, it means the move has returned more items than it received.
# The correction should behave as a return too. As `_account_entry_move`
# will post the natural values for an IN move (credit IN account, debit
# OUT one), we inverse the sign to create the correct entries.
move.with_context(force_valuation_amount=-corrected_value, forced_quantity=0)._account_entry_move()
else:
move.with_context(force_valuation_amount=corrected_value, forced_quantity=0)._account_entry_move()
@api.model
def _run_fifo_vacuum(self):
# Call `_fifo_vacuum` on concerned moves
fifo_valued_products = self.env['product.product']
fifo_valued_categories = self.env['product.category'].search([('property_cost_method', '=', 'fifo')])
fifo_valued_products |= self.env['product.product'].search([('categ_id', 'child_of', fifo_valued_categories.ids)])
moves_to_vacuum = self.search(
[('product_id', 'in', fifo_valued_products.ids), ('remaining_qty', '<', 0)] + self._get_all_base_domain())
moves_to_vacuum._fifo_vacuum()
@api.multi
def _get_accounting_data_for_valuation(self):
""" Return the accounts and journal to use to post Journal Entries for
@@ -726,7 +452,7 @@ class StockMove(models.Model):
if not ref and self.inventory_id:
ref = self.inventory_id.name
move_lines = self.with_context(forced_ref=ref)._prepare_account_move_line(quantity, abs(self.value), credit_account_id, debit_account_id)
move_lines = self.with_context(forced_ref=ref)._prepare_account_move_line(quantity, 0, credit_account_id, debit_account_id)
if move_lines:
date = self._context.get('force_period_date', fields.Date.context_today(self))
new_account_move = AccountMove.sudo().create({
+10 -71
View File
@@ -14,39 +14,19 @@ class StockMoveLine(models.Model):
@api.model_create_multi
def create(self, vals_list):
move_lines = super(StockMoveLine, self).create(vals_list)
if self.env.context.get('svl'):
for move_line in move_lines:
if move_line.state != 'done':
continue
move = move_line.move_id
rounding = move.product_id.uom_id.rounding
diff = move_line.qty_done
if float_is_zero(diff, precision_rounding=rounding):
continue
self._create_correction_svl(move, diff)
return move_lines
# for line in move_lines:
# move = line.move_id
# if move.state == 'done':
# correction_value = move._run_valuation(line.qty_done)
# if move.product_id.valuation == 'real_time' and (move._is_in() or move._is_out()):
# move.with_context(force_valuation_amount=correction_value)._account_entry_move()
for move_line in move_lines:
if move_line.state != 'done':
continue
move = move_line.move_id
rounding = move.product_id.uom_id.rounding
diff = move_line.qty_done
if float_is_zero(diff, precision_rounding=rounding):
continue
self._create_correction_svl(move, diff)
return move_lines
def write(self, vals):
""" When editing a done stock.move.line, we impact the valuation. Users may increase or
decrease the `qty_done` field. There are three cost method available: standard, average
and fifo. We implement the logic in a similar way for standard and average: increase
or decrease the original value with the standard or average price of today. In fifo, we
have a different logic wheter the move is incoming or outgoing. If the move is incoming, we
update the value and remaining_value/qty with the unit price of the move. If the move is
outgoing and the user increases qty_done, we call _run_fifo and it'll consume layer(s) in
the stack the same way a new outgoing move would have done. If the move is outoing and the
user decreases qty_done, we either increase the last receipt candidate if one is found or
we decrease the value with the last fifo price.
"""
if 'qty_done' in vals and self.env.context.get('svl'):
if 'qty_done' in vals:
for move_line in self:
if move_line.state != 'done':
continue
@@ -56,47 +36,6 @@ class StockMoveLine(models.Model):
if float_is_zero(diff, precision_rounding=rounding):
continue
self._create_correction_svl(move, diff)
elif 'qty_done' in vals:
moves_to_update = {}
for move_line in self.filtered(lambda ml: ml.state == 'done' and (ml.move_id._is_in() or ml.move_id._is_out())):
moves_to_update[move_line.move_id] = vals['qty_done'] - move_line.qty_done
for move_id, qty_difference in moves_to_update.items():
move_vals = {}
if move_id.product_id.cost_method in ['standard', 'average']:
correction_value = qty_difference * move_id.product_id.standard_price
if move_id._is_in():
move_vals['value'] = move_id.value + correction_value
elif move_id._is_out():
move_vals['value'] = move_id.value - correction_value
else:
if move_id._is_in():
correction_value = qty_difference * move_id.price_unit
new_remaining_value = move_id.remaining_value + correction_value
move_vals['value'] = move_id.value + correction_value
move_vals['remaining_qty'] = move_id.remaining_qty + qty_difference
move_vals['remaining_value'] = move_id.remaining_value + correction_value
elif move_id._is_out() and qty_difference > 0:
correction_value = self.env['stock.move']._run_fifo(move_id, quantity=qty_difference)
# no need to adapt `remaining_qty` and `remaining_value` as `_run_fifo` took care of it
move_vals['value'] = move_id.value - correction_value
elif move_id._is_out() and qty_difference < 0:
candidates_receipt = self.env['stock.move'].search(move_id._get_in_domain(), order='date, id desc', limit=1)
if candidates_receipt:
candidates_receipt.write({
'remaining_qty': candidates_receipt.remaining_qty + -qty_difference,
'remaining_value': candidates_receipt.remaining_value + (-qty_difference * candidates_receipt.price_unit),
})
correction_value = qty_difference * candidates_receipt.price_unit
else:
correction_value = qty_difference * move_id.product_id.standard_price
move_vals['value'] = move_id.value - correction_value
move_id.write(move_vals)
if move_id.product_id.valuation == 'real_time':
move_id.with_context(force_valuation_amount=correction_value, forced_quantity=qty_difference)._account_entry_move()
if qty_difference > 0:
move_id.product_price_update_before_done(forced_qty=qty_difference)
return super(StockMoveLine, self).write(vals)
# -------------------------------------------------------------------------
@@ -243,7 +243,7 @@ class TestStockValuation(SavepointCase):
# Increase received quantity of move1 from 10 to 12, it should create
# a new stock layer at the top of the queue.
# ---------------------------------------------------------------------
move1.with_context(svl=True).quantity_done = 12
move1.quantity_done = 12
# stock_account values for move3
self.assertEqual(move1.stock_valuation_layer_ids[-1].unit_cost, 10.0)
@@ -391,7 +391,7 @@ class TestStockValuation(SavepointCase):
# ---------------------------------------------------------------------
# Edit move6, receive less: 2 in negative stock
# ---------------------------------------------------------------------
move6.with_context(svl=True).quantity_done = 8
move6.quantity_done = 8
# stock_account values for move6
self.assertEqual(move6.stock_valuation_layer_ids[-1].remaining_qty, -2)
@@ -1195,8 +1195,8 @@ class TestStockValuation(SavepointCase):
self.assertEqual(len(move3.account_move_ids), 1) # the created account move is due to the receipt
# nothing should have changed in the accounting regarding the output
self.assertEqual(self.product1.qty_available, 0)
self.assertEqual(self.product1.stock_value, 0)
self.assertEqual(self.product1.quantity_svl, 0)
self.assertEqual(self.product1.value_svl, 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0)
self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 120)
self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 120)
@@ -1419,7 +1419,6 @@ class TestStockValuation(SavepointCase):
'product_id': self.product2.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
'state': 'done', # simulate default_get override
'move_line_ids': [(0, 0, {
'product_id': self.product2.id,
'location_id': self.supplier_location.id,
@@ -1445,7 +1444,7 @@ class TestStockValuation(SavepointCase):
# ---------------------------------------------------------------------
# Edit the previous stock move, receive 11
# ---------------------------------------------------------------------
move2.with_context(svl=True).quantity_done = 11
move2.quantity_done = 11
self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), 220.0) # after correction, the move should be valued at 11@20
self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('remaining_qty')), 11.0)
@@ -1628,7 +1627,7 @@ class TestStockValuation(SavepointCase):
# Add a new move line to receive 10 more
# ---------------------------------------------------------------------
self.assertEqual(len(move1.move_line_ids), 1)
self.env['stock.move.line'].with_context(svl=True).create({
self.env['stock.move.line'].create({
'move_id': move1.id,
'product_id': move1.product_id.id,
'qty_done': 10,
@@ -1786,7 +1785,7 @@ class TestStockValuation(SavepointCase):
# Edit last move, send 14 instead
# it should send 2@10 and 4@12
# ---------------------------------------------------------------------
move3.with_context(svl=True).quantity_done = 14
move3.quantity_done = 14
self.assertEqual(move3.product_qty, 14)
# old value: -80 -(8@10)
# new value: -148 => -(10@10 + 4@12)
@@ -1879,7 +1878,7 @@ class TestStockValuation(SavepointCase):
# ---------------------------------------------------------------------
# Actually, send 8 in the last move
# ---------------------------------------------------------------------
move2.with_context(svl=True).quantity_done = 8
move2.quantity_done = 8
self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), -80.0) # the move actually sent 8@10
@@ -1892,7 +1891,7 @@ class TestStockValuation(SavepointCase):
# ---------------------------------------------------------------------
# Actually, send 10 in the last move
# ---------------------------------------------------------------------
move2.with_context(svl=True).quantity_done = 10
move2.quantity_done = 10
self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), -100.0) # the move actually sent 10@10
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
@@ -2068,7 +2067,7 @@ class TestStockValuation(SavepointCase):
self.assertEqual(self.product1.quantity_svl, -5)
self.assertEqual(self.product1.value_svl, -62.5)
move2.with_context(svl=True).move_line_ids.qty_done = 20
move2.move_line_ids.qty_done = 20
self.assertEqual(self.product1.quantity_svl, 5)
self.assertEqual(self.product1.value_svl, 87.5)
@@ -2129,7 +2128,7 @@ class TestStockValuation(SavepointCase):
move4._action_assign()
move4.move_line_ids.qty_done = 10.0
move4._action_done()
move2.with_context(svl=True).move_line_ids.qty_done = 0
move2.move_line_ids.qty_done = 0
self.assertEqual(self.product1.value_svl, -187.5)
def test_average_perpetual_4(self):
@@ -2268,7 +2267,7 @@ class TestStockValuation(SavepointCase):
self.assertAlmostEqual(self.product1.quantity_svl, 15)
self.assertAlmostEqual(self.product1.value_svl, 250)
move1.move_line_ids.with_context(svl=True).qty_done = 15
move1.move_line_ids.qty_done = 15
self.assertAlmostEqual(self.product1.standard_price, 14.0)
self.assertAlmostEqual(len(move1.stock_valuation_layer_ids), 2)
@@ -2365,7 +2364,7 @@ class TestStockValuation(SavepointCase):
self.assertEqual(move2_valuation_aml.debit, 0)
self.assertEqual(move2_valuation_aml.credit, 200)
move2.with_context(svl=True).quantity_done = 10.0
move2.quantity_done = 10.0
valuation_aml = self._get_stock_valuation_move_lines()
move2_valuation_aml = valuation_aml[-1]
@@ -2373,7 +2372,7 @@ class TestStockValuation(SavepointCase):
self.assertEqual(move2_valuation_aml.debit, 100)
self.assertEqual(move2_valuation_aml.credit, 0)
move2.with_context(svl=True).quantity_done = 11.0
move2.quantity_done = 11.0
valuation_aml = self._get_stock_valuation_move_lines()
move2_valuation_aml = valuation_aml[-1]
@@ -3139,7 +3138,7 @@ class TestStockValuation(SavepointCase):
self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).value_svl, 712.5)
# edit the done quantity of move1, decrease it
move1.with_context(svl=True).quantity_done = 5
move1.quantity_done = 5
# the change is only visible right now
self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).quantity_svl, 10)
@@ -3151,7 +3150,7 @@ class TestStockValuation(SavepointCase):
self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).value_svl, 100)
# edit move 4, send 15 instead of 20
move4.with_context(svl=True).quantity_done = 15
move4.quantity_done = 15
# -(20*5) + (5*7.5)
self.assertEqual(sum(move4.stock_valuation_layer_ids.mapped('value')), -62.5)
@@ -3278,7 +3277,7 @@ class TestStockValuation(SavepointCase):
self.assertEqual(self.product1.value_svl, 1275)
# Edit the quantity done of move1, increase it.
move1.with_context(svl=True).quantity_done = 20
move1.quantity_done = 20
self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).quantity_svl, 10)
self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).value_svl, 100)
@@ -170,7 +170,7 @@ class TestStockValuationStandard(TestStockValuationCommon):
move1 = self._make_in_move(self.product1, 10)
move2 = self._make_in_move(self.product1, 10)
move3 = self._make_out_move(self.product1, 15)
self.env['stock.move.line'].with_context(svl=True).create({
self.env['stock.move.line'].create({
'move_id': move1.id,
'product_id': move1.product_id.id,
'qty_done': 5,
@@ -221,7 +221,7 @@ class TestStockValuationStandard(TestStockValuationCommon):
move1 = self._make_in_move(self.product1, 10)
move2 = self._make_out_move(self.product1, 15)
self.env['stock.move.line'].with_context(svl=True).create({
self.env['stock.move.line'].create({
'move_id': move1.id,
'product_id': move1.product_id.id,
'qty_done': 10,
@@ -249,7 +249,7 @@ class TestStockValuationStandard(TestStockValuationCommon):
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
move1 = self._make_dropship_move(self.product1, 10)
move1.move_line_ids.with_context(svl=True).qty_done = 15
move1.move_line_ids.qty_done = 15
valuation_layers = self.product1.stock_valuation_layer_ids
self.assertEqual(len(valuation_layers), 4)
@@ -304,7 +304,7 @@ class TestStockValuationAVCO(TestStockValuationCommon):
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
move3 = self._make_out_move(self.product1, 15)
self.env['stock.move.line'].with_context(svl=True).create({
self.env['stock.move.line'].create({
'move_id': move1.id,
'product_id': move1.product_id.id,
'qty_done': 5,
@@ -321,7 +321,7 @@ class TestStockValuationAVCO(TestStockValuationCommon):
move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True)
move2 = self._make_in_move(self.product1, 10, unit_cost=20)
move3 = self._make_out_move(self.product1, 15)
self.env['stock.move.line'].with_context(svl=True).create({
self.env['stock.move.line'].create({
'product_id': move1.product_id.id,
'qty_done': 5,
'product_uom_id': move1.product_uom.id,
@@ -478,7 +478,7 @@ class TestStockValuationFIFO(TestStockValuationCommon):
move1 = self._make_in_move(self.product1, 20, unit_cost=10)
move2 = self._make_out_move(self.product1, 10)
move3 = self._make_out_move(self.product1, 10)
move1.move_line_ids.with_context(svl=True).qty_done = 10
move1.move_line_ids.qty_done = 10
move4 = self._make_in_move(self.product1, 20, unit_cost=15)
self.assertEqual(self.product1.value_svl, 150)
@@ -501,7 +501,7 @@ class TestStockValuationFIFO(TestStockValuationCommon):
move3 = self._make_out_move(self.product1, 15)
move4 = self._make_out_move(self.product1, 20)
move5 = self._make_in_move(self.product1, 100, unit_cost=15)
move1.move_line_ids.with_context(svl=True).qty_done = 20
move1.move_line_ids.qty_done = 20
self.assertEqual(self.product1.value_svl, 1375)
self.assertEqual(self.product1.quantity_svl, 95)
@@ -80,23 +80,5 @@
</field>
</record>
<record id="product_valuation_action" model="ir.actions.act_window">
<field name="name">Inventory Valuation</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.product</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_stock_product_tree2"/>
<field name="domain">[('type', '=', 'product'), ('qty_available', '!=', 0)]</field>
<field name="context">{'company_owned': True, 'create': False, 'edit' : False}</field>
<field name="search_view_id" ref="view_inventory_valuation_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new product valuation
</p><p>
If there are products, you will see its name and valuation.
</p>
</field>
</record>
</data>
</odoo>
@@ -74,22 +74,6 @@
</form>
</field>
</record>
<record id="view_stock_product_tree2" model="ir.ui.view">
<field name="name">product.stock.tree.2</field>
<field name="model">product.product</field>
<field name="arch" type="xml">
<tree>
<field name="display_name"/>
<field name="qty_at_date"/>
<field name="uom_id" groups="uom.group_uom"/>
<field name="currency_id" invisible="1"/>
<field name="cost_currency_id" invisible="1"/>
<field name="stock_value" sum="Stock Valuation" widget="monetary" options="{'currency_field': 'cost_currency_id'}"/>
<field name="cost_method" invisible="1"/>
<button name="action_valuation_at_date_details" type="object" icon="fa-info-circle" aria-label="Valuation" title="Valuation" attrs="{'invisible': [('cost_method', '!=', 'fifo')]}" />
</tree>
</field>
</record>
<record id="view_inventory_valuation_search" model="ir.ui.view">
<field name="name">product.product</field>
@@ -119,37 +103,5 @@
</record>
<menuitem id="menu_valuation" name="Inventory Valuation" parent="stock.menu_warehouse_report" sequence="110" action="action_stock_inventory_valuation"/>
<!-- AML specialized tree view -->
<record id="view_stock_account_aml" model="ir.ui.view">
<field name="name">stock.account.aml</field>
<field name="model">account.move.line</field>
<field name="arch" type="xml">
<tree>
<field name="date" />
<field name="ref" />
<field name="product_id" />
<field name="quantity" />
<field name="product_uom_id" groups="uom.group_uom" />
<field name="balance" string="Value"/>
</tree>
</field>
</record>
<!-- stock move specialized tree view -->
<record id="view_move_tree_valuation_at_date" model="ir.ui.view">
<field name="name">stock.move.tree.valuation.at.date</field>
<field name="model">stock.move</field>
<field name="arch" type="xml">
<tree>
<field name="date" />
<field name="picking_id" string="Reference"/>
<field name="state" invisible="1"/>
<field name="product_id"/>
<field name="product_uom_qty" string="Quantity"/>
<field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
<field name="value" sum="Stock Valuation"/>
</tree>
</field>
</record>
</data>
</odoo>
@@ -10,25 +10,10 @@ class StockQuantityHistory(models.TransientModel):
if not self.env.context.get('valuation'):
return super(StockQuantityHistory, self).open_table()
self.env['stock.move']._run_fifo_vacuum()
action = self.env.ref('stock_account.stock_valuation_layer_action').read()[0]
if int(self.compute_at_date):
tree_view_id = self.env.ref('stock_account.view_stock_product_tree2').id
form_view_id = self.env.ref('stock.product_form_view_procurement_button').id
search_view_id = self.env.ref('stock_account.view_inventory_valuation_search').id
# We pass `to_date` in the context so that `qty_available` will be computed across
# moves until date.
action = {
'type': 'ir.actions.act_window',
'views': [(tree_view_id, 'tree'), (form_view_id, 'form')],
'view_mode': 'tree,form',
'name': _('Inventory Valuation'),
'res_model': 'product.product',
'domain': "[('type', '=', 'product'), ('qty_available', '!=', 0)]",
'context': dict(self.env.context, to_date=self.date, company_owned=True, create=False, edit=False),
'search_view_id': search_view_id
}
action['domain'] = [('create_date', '<=', self.date)]
return action
else:
return self.env.ref('stock_account.product_valuation_action').read()[0]
return action