[REF] stock_account: fifo "negative" cost method with valuation layers

Re-implement _run_fifo_vacuum on the product this time with
float_is_zero and without remaining_value.

Trigger the vacuum at each receipt. Before it was done in the scheduler
each night, meaning the valuation on the move was not really
understandable nor consistent (the estimated value could be fixed by
some other move than the next receipt).

task-1875873
This commit is contained in:
Simon Lejeune
2019-06-10 20:42:59 +00:00
parent 8d21d0f888
commit 54d1adf31a
4 changed files with 246 additions and 597 deletions
+75
View File
@@ -255,6 +255,81 @@ class ProductProduct(models.Model):
}
return vals
def _run_fifo_vacuum(self):
"""Compensate layer valued at an estimated price with the price of future receipts
if any. If the estimated price is equals to the real price, no layer is created but
the original layer is marked as compensated.
"""
self.ensure_one()
svls_to_vacuum = self.env['stock.valuation.layer'].search([
('product_id', '=', self.id),
('remaining_qty', '<', 0),
('stock_move_id', '!=', False),
])
for svl_to_vacuum in svls_to_vacuum:
domain = [
('company_id', '=', svl_to_vacuum.company_id.id),
('product_id', '=', self.id),
('remaining_qty', '>', 0),
'|',
('create_date', '>', svl_to_vacuum.create_date),
'&',
('create_date', '=', svl_to_vacuum.create_date),
('id', '>', svl_to_vacuum.id)
]
candidates = self.env['stock.valuation.layer'].search(domain)
if not candidates:
continue
qty_to_take_on_candidates = abs(svl_to_vacuum.remaining_qty)
qty_taken_on_candidates = 0
tmp_value = 0
for candidate in candidates:
qty_taken_on_candidate = min(candidate.remaining_qty, qty_to_take_on_candidates)
qty_taken_on_candidates += qty_taken_on_candidate
value_taken_on_candidate = qty_taken_on_candidate * candidate.unit_cost
candidate_vals = {
'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate,
}
candidate.write(candidate_vals)
qty_to_take_on_candidates -= qty_taken_on_candidate
tmp_value += value_taken_on_candidate
if float_is_zero(qty_to_take_on_candidates, precision_rounding=self.uom_id.rounding):
break
# Get the estimated value we will correct.
remaining_value_before_vacuum = svl_to_vacuum.unit_cost * qty_taken_on_candidates
new_remaining_qty = svl_to_vacuum.remaining_qty + qty_taken_on_candidates
corrected_value = remaining_value_before_vacuum - tmp_value
svl_to_vacuum.write({
'remaining_qty': new_remaining_qty,
})
# Don't create a layer or an accounting entry if the corrected value is zero.
if svl_to_vacuum.currency_id.is_zero(corrected_value):
continue
corrected_value = svl_to_vacuum.currency_id.round(corrected_value)
move = svl_to_vacuum.stock_move_id
vals = {
'product_id': self.id,
'value': corrected_value,
'unit_cost': 0,
'quantity': 0,
'remaining_qty': 0,
'description': 'vacuum',
'stock_move_id': move.id,
'company_id': move.company_id.id,
'description': 'Revaluation of %s (negative inventory)' % move.picking_id.name or move.name,
}
vacuum_svl = self.env['stock.valuation.layer'].create(vals)
# Create the account move.
if self.valuation != 'real_time':
continue
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
# -------------------------------------------------------------------------
+10
View File
@@ -402,6 +402,12 @@ class StockMove(models.Model):
if not stock_valuation_layer.product_id.valuation == 'real_time':
continue
stock_valuation_layer.stock_move_id.with_context(svl_id=stock_valuation_layer.id, force_valuation_amount=abs(stock_valuation_layer.value), forced_quantity=stock_valuation_layer.quantity)._account_entry_move()
# For every in move, run the vacuum for the linked product.
products_to_vacuum = valued_moves['in'].mapped('product_id')
for product_to_vacuum in products_to_vacuum:
product_to_vacuum._run_fifo_vacuum()
return res
def _sanity_check_for_valuation(self):
@@ -714,6 +720,10 @@ class StockMove(models.Model):
# Create Journal Entry for products leaving the company
if self._is_out():
if self.env.context.get('force_valuation_amount'):
force_valuation_amount = self.env.context['force_valuation_amount']
self.env.context = dict(self.env.context, force_valuation_amount=-1*force_valuation_amount)
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation()
if location_to and location_to.usage == 'supplier': # goods returned to supplier
self.with_context(force_company=company_from.id)._create_account_move_line(acc_valuation, acc_src, journal_id)
File diff suppressed because it is too large Load Diff
@@ -312,6 +312,19 @@ class TestStockValuationFIFO(TestStockValuationCommon):
self.assertEqual(self.product1.quantity_svl, 5)
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 5)
def test_negative_1(self):
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
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, 30)
self.assertEqual(move3.stock_valuation_layer_ids.remaining_qty, -10)
move4 = self._make_in_move(self.product1, 10, unit_cost=30)
self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0)
move5 = self._make_in_move(self.product1, 10, unit_cost=40)
self.assertEqual(self.product1.value_svl, 400)
self.assertEqual(self.product1.quantity_svl, 10)
def test_change_in_past_decrease_in_1(self):
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
move1 = self._make_in_move(self.product1, 20, unit_cost=10)
@@ -321,14 +334,13 @@ class TestStockValuationFIFO(TestStockValuationCommon):
self.assertEqual(self.product1.value_svl, 0)
self.assertEqual(self.product1.quantity_svl, 0)
@skip('fifo negative')
def test_change_in_past_decrease_in_2(self):
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
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.qty_done = 10
move1 = self._make_in_move(self.product1, 20, unit_cost=15)
move1.move_line_ids.with_context(svl=True).qty_done = 10
move4 = self._make_in_move(self.product1, 20, unit_cost=15)
self.assertEqual(self.product1.value_svl, 150)
self.assertEqual(self.product1.quantity_svl, 10)
@@ -343,7 +355,6 @@ class TestStockValuationFIFO(TestStockValuationCommon):
self.assertEqual(self.product1.value_svl, 100)
self.assertEqual(self.product1.quantity_svl, 10)
@skip('fifo negative')
def test_change_in_past_increase_in_2(self):
self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic'
move1 = self._make_in_move(self.product1, 10, unit_cost=10)
@@ -351,7 +362,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.qty_done = 20
move1.move_line_ids.with_context(svl=True).qty_done = 20
self.assertEqual(self.product1.value_svl, 1375)
self.assertEqual(self.product1.quantity_svl, 95)