diff --git a/addons/stock_account/models/__init__.py b/addons/stock_account/models/__init__.py
index 21bf84ae9b2..7ba53628062 100644
--- a/addons/stock_account/models/__init__.py
+++ b/addons/stock_account/models/__init__.py
@@ -3,6 +3,7 @@
from . import account_chart_template
from . import account_invoice
+from . import account_move
from . import product
from . import stock
from . import stock_config_settings
diff --git a/addons/stock_account/models/account_move.py b/addons/stock_account/models/account_move.py
new file mode 100644
index 00000000000..7919da15090
--- /dev/null
+++ b/addons/stock_account/models/account_move.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+
+from odoo import fields, models
+
+
+class AccountMove(models.Model):
+ _inherit = 'account.move'
+
+ stock_move_id = fields.Many2one('stock.move', string='Stock Move')
+
diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py
index 20c39f6f367..d98bc8f2533 100644
--- a/addons/stock_account/models/product.py
+++ b/addons/stock_account/models/product.py
@@ -38,19 +38,6 @@ class ProductTemplate(models.Model):
company_dependent=True, domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used.")
- average_price = fields.Float(
- 'Average Cost', compute='_compute_average_price',
- digits=dp.get_precision('Product Price'), groups="base.group_user",
- help="Average cost of the product, in the default unit of measure of the product.")
-
- @api.multi
- def _compute_average_price(self):
- unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1)
- for template in unique_variants:
- template.average_price = template.product_variant_ids.average_price
- for template in (self - unique_variants):
- template.average_price = 0.0
-
@api.one
@api.depends('property_valuation', 'categ_id.property_valuation')
@@ -100,12 +87,6 @@ class ProductTemplate(models.Model):
class ProductProduct(models.Model):
_inherit = 'product.product'
- average_price = fields.Float(
- 'Average Cost',
- digits=dp.get_precision('Product Price'),
- groups="base.group_user",
- compute='_compute_average_price',
- help="Calculated average cost")
stock_value = fields.Float(
'Value', compute='_compute_stock_value')
@@ -156,24 +137,6 @@ class ProductProduct(models.Model):
self.write({'standard_price': new_price})
return True
- def _get_latest_cumulated_value(self, exclude_move=False):
- self.ensure_one()
- domain = [('product_id', '=', self.id)] + self.env['stock.move']._get_all_base_domain()
- if exclude_move:
- domain += [('id', '!=', exclude_move.id)]
- latest = self.env['stock.move'].search(domain, order='date desc, id desc', limit=1)
- if not latest:
- return 0.0
- return latest.cumulated_value
-
- def _get_fifo_candidates_out_move(self):
- """ Find OUT moves that were not valued in time because of negative stock.
- """
- self.ensure_one()
- domain = [('product_id', '=', self.id), ('remaining_qty', '>', 0.0)] + self.env['stock.move']._get_out_base_domain()
- candidates = self.env['stock.move'].search(domain, order='date, id')
- return candidates
-
def _get_fifo_candidates_in_move(self):
""" Find IN moves that can be used to value OUT moves.
"""
@@ -182,23 +145,6 @@ class ProductProduct(models.Model):
candidates = self.env['stock.move'].search(domain, order='date, id')
return candidates
- @api.multi
- @api.depends('stock_move_ids.product_qty', 'stock_move_ids.state')
- def _compute_average_price(self):
- """ Compute the average price of a product.product according to the latest cumulated value
- and the quantity that is in stock (for all the company owned locations, i.e. including the
- warehouses transit ones). If nothing is in stick, we fallback first on the latest average
- price on an out move used for this product then on the standard price.
- """
- for p_set, product in pycompat.izip(self, self.with_context(company_owned=True)):
- if product.qty_available > 0:
- last_cumulated_value = product._get_latest_cumulated_value()
- p_set.average_price = last_cumulated_value / product.qty_available
- else:
- domain = [('product_id', '=', product.id), ('last_done_qty', '>', 0.0)] + self.env['stock.move']._get_all_base_domain()
- move = self.env['stock.move'].search(domain, order='date desc, id desc', limit=1)
- p_set.average_price = move and move.cumulated_value / move.last_done_qty or product.standard_price
-
@api.multi
def _compute_stock_value(self):
for product in self:
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 10da8ee47e7..d4564d97959 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -55,11 +55,25 @@ class StockMoveLine(models.Model):
@api.multi
def write(self, vals):
- super(StockMoveLine, self).write(vals)
if 'qty_done' in vals:
- for move in self.mapped('move_id').filtered(lambda m: m.state == 'done'):
- move.value = move.quantity_done * move.price_unit
- move.replay()
+ # We need to update the `value`, `remaining_value` and `remaining_qty` on the linked
+ # stock move.
+ moves_to_update = {}
+ for move_line in self.filtered(lambda ml: ml.state == 'done'):
+ moves_to_update[move_line.move_id] = vals['qty_done'] - move_line.qty_done
+
+ for move_id, qty_difference in moves_to_update.items():
+ # more/less units are available, update `value`, `remaining_value` and
+ # `remaining_qty` on the linked stock move.
+ move_vals = {
+ 'value': move_id.value + qty_difference * move_id.price_unit,
+ 'remaining_value': move_id.remaining_value + qty_difference * move_id.price_unit,
+ 'remaining_qty': move_id.remaining_qty + qty_difference,
+ }
+ move_id.write(move_vals)
+ if move_id.product_id.valuation == 'real_time':
+ move_id.with_context(force_valuation_amount=qty_difference*move_id.price_unit)._account_entry_move()
+ return super(StockMoveLine, self).write(vals)
class StockMove(models.Model):
@@ -67,50 +81,26 @@ class StockMove(models.Model):
to_refund = fields.Boolean(string="To Refund (update SO/PO)",
help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order')
-
value = fields.Float()
- cumulated_value = fields.Float()
remaining_qty = fields.Float()
- last_done_move_id = fields.Many2one('stock.move')
- last_done_remaining_qty = fields.Float()
- last_done_qty = fields.Float()
-
- # TODO: add constraints remaining_qty > 0
- # TODO: add constrain price_unit = 0 on done move?
+ remaining_value = fields.Float()
+ account_move_ids = fields.One2many('account.move', 'stock_move_id')
@api.multi
- def _get_price_unit(self):
+ def action_get_account_moves(self):
self.ensure_one()
- if self.product_id.cost_method == 'average':
- return self.product_id.average_price or self.product_id.standard_price
- if self.product_id.cost_method == 'fifo':
- move = self.search([('product_id', '=', self.product_id.id),
- ('state', '=', 'done'),
- ('location_id.usage', '=', 'internal'),
- ('location_dest_id.usage', '!=', 'internal')], order='date desc', limit=1)
- if move:
- return move.price_unit or self.product_id.standard_price
- return self.product_id.standard_price
+ action_ref = self.env.ref('account.action_move_journal_line')
+ if not action_ref:
+ return False
+ action_data = action_ref.read()[0]
+ action_data['domain'] = [('id', 'in', self.account_move_ids.ids)]
+ return action_data
- def _update_fifo_future_cumulated_value(self, value):
- """ This method is intended to be called on an OUT move that was not valued in time because
- of negative stock with `value` argument representing its new value.
- """
- self.ensure_one()
- domain = self._get_all_domain()
- domain += [
- '|',
- ('date', '>', self.date),
- '&',
- ('date', '=', self.date),
- ('id', '>', self.id)
- ]
- moves = self.search(domain, order='date, id')
- for move in moves:
- # As `self` is always an out move, we decrease its value on the future moves.
- move.cumulated_value -= value
+ def get_price_unit(self):
+ """ Returns the unit price to store on the quant """
+ return self.price_unit or self.product_id.standard_price
- @ api.model
+ @api.model
def _get_in_base_domain(self, company_id=False):
domain = [
('state', '=', 'done'),
@@ -119,16 +109,7 @@ class StockMove(models.Model):
]
return domain
- @ api.model
- def _get_out_base_domain(self, company_id=False):
- domain = [
- ('state', '=', 'done'),
- ('location_id.company_id', '=', company_id or self.env.user.company_id.id),
- ('location_dest_id.company_id', '=', False)
- ]
- return domain
-
- @ api.model
+ @api.model
def _get_all_base_domain(self, company_id=False):
domain = [
('state', '=', 'done'),
@@ -145,9 +126,6 @@ class StockMove(models.Model):
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_out_domain(self):
- return [('product_id', '=', self.product_id.id)] + self._get_out_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)
@@ -167,133 +145,171 @@ class StockMove(models.Model):
"""
return self.location_id.company_id.id == self.company_id.id and not self.location_dest_id.company_id
-
-# def change_move_value_in_the_past(self, value):
-# self.ensure_one()
-# if self.product_id.cost_method == 'fifo':
-# moves = self.search([('state', '=', 'done'),
-# ('date', '>', self.date),
-# ('product_id', '=', self.product_id.id)])
-# if self.location_id.usage not in ('internal', 'transit'):
-# if move.last_done_move_id and move.last_done_remaining_qty:
-
- @api.multi
- def replay(self):
- # Easy scenario: avergae /done
- # search last move before this one
- start_move = self.search([('product_id', '=', self.product_id.id),
- ('state', '=', 'done'),
- ('date', '<', self.date)], limit=1, order='date desc') #filter on outgoing/incoming moves
- next_moves = self.search([('product_id', '=', self.product_id.id),
- ('state', '=', 'done'),
- ('date', '>', start_move.date)], order='date') # filter on outgoing/incoming moves
- if start_move:
- last_cumulated_value = start_move.cumulated_value
- last_done_qty_available = start_move.last_done_qty
- else:
- last_cumulated_value = 0.0
- last_done_qty_available = 0.0
- if self.product_id.cost_method == 'average':
- for move in next_moves:
- if move.location_id.usage in ('internal', 'transit') and move.location_dest_id.usage not in ('internal', 'transit'):
- if last_done_qty_available:
- move.value = - ((last_cumulated_value / last_done_qty_available) * move.product_qty)
- last_done_qty_available -= move.product_qty
- else:
- last_done_qty_available += move.product_qty
- move.cumulated_value = last_cumulated_value + move.value
- last_cumulated_value = move.cumulated_value
- move.last_done_qty = last_done_qty_available
-
-
- # FIFO: needs dict with qty_remaining to replay algorithm
-
-
- # update cumulated_value according to value
-
- # update outs according to values
-
- # update
-
-
@api.multi
def action_done(self):
- average_price = {}
- for move in self.filtered(lambda m: m.product_id.cost_method == 'average'):
- # For the average costing method, the value depends of the quantity in stock at the
- # moment of the transfer, so fetch the value before calling super.
- average_price[move.product_id.id] = move.product_id.average_price
+ self.product_price_update_before_done()
res = super(StockMove, self).action_done()
for move in res:
if move._is_in():
if move.product_id.cost_method in ['fifo', 'average']:
- if not move.price_unit:
- move.price_unit = move._get_price_unit()
- move.value = move.price_unit * move.product_qty
- move.cumulated_value = move.product_id._get_latest_cumulated_value(exclude_move=move) + move.value
+ price_unit = move.price_unit or move.get_price_unit()
+ value = price_unit * move.product_qty
+ vals = {
+ 'price_unit': price_unit,
+ 'value': value,
+ 'remaining_value': value,
+ }
if move.product_id.cost_method == 'fifo':
- move.remaining_qty = move.product_qty
-
- # If there are some OUT moves that we could not value in time, find them
- # and value them now with this new IN move. After that, update the cumulated
- # value along the moves done after the newly valued move.
- candidates_out = move.product_id._get_fifo_candidates_out_move()
- qty_to_take = move.product_qty
- for candidate in candidates_out:
- if candidate.remaining_qty < qty_to_take:
- qty_taken_for_candidate = candidate.remaining_qty
- else:
- qty_taken_for_candidate = qty_to_take
- candidate.remaining_qty -= qty_taken_for_candidate
- move.remaining_qty -= qty_taken_for_candidate
- qty_to_take -= qty_taken_for_candidate
- candidate.value -= move.price_unit * qty_taken_for_candidate
- candidate.cumulated_value -= move.price_unit * qty_taken_for_candidate
- candidate._update_fifo_future_cumulated_value(move.price_unit * qty_taken_for_candidate)
- candidate.price_unit = candidate.value / candidate.product_qty
- move.last_done_qty = move.product_id.qty_available
- else:
- move.price_unit = move.product_id.standard_price
- move.value = move.price_unit * move.product_qty
+ vals['remaining_qty'] = move.product_qty
+ move.write(vals)
+ else: # standard
+ move.write({
+ 'price_unit': move.product_id.standard_price,
+ 'value': move.product_id.standard_price * move.product_qty,
+ })
elif move._is_out():
if move.product_id.cost_method == 'fifo':
- qty_to_take = move.product_qty
- tmp_value = 0
+ # Find back incoming stock moves (called candidates here) to value this move.
+ qty_to_take_on_candidates = move.product_qty
candidates = move.product_id._get_fifo_candidates_in_move()
- last_candidate = False
+ new_standard_price = 0
+ tmp_value = 0 # to accumulate the value taken on the candidates
for candidate in candidates:
- if candidate.remaining_qty <= qty_to_take:
+ 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
- tmp_value += qty_taken_on_candidate * candidate.price_unit
- candidate.remaining_qty -= qty_taken_on_candidate
- qty_to_take -= qty_taken_on_candidate
- if qty_to_take == 0:
+ qty_taken_on_candidate = qty_to_take_on_candidates
+
+ 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
- last_candidate = candidate
- if last_candidate:
- move.last_done_move_id = last_candidate.id
- move.last_done_remaining_qty = last_candidate.remaining_qty
- if qty_to_take > 0:
- move.remaining_qty = qty_to_take # In case there are no candidates to match, put standard price on it
- move.value = -tmp_value
- move.cumulated_value = move.product_id._get_latest_cumulated_value(exclude_move=move) + move.value
- move.last_done_qty = move.product_id.qty_available
- elif move.product_id.cost_method == 'average':
+
+ # Update the standard price with the price of the last used candidate, if any.
+ if new_standard_price:
+ move.product_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, # outgoing move are valued negatively
+ 'price_unit': -tmp_value / move.product_qty,
+ })
+ 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
+ vals = {
+ 'remaining_qty': -qty_to_take_on_candidates,
+ 'remaining_value': negative_stock_value,
+ 'value': -tmp_value + negative_stock_value,
+ 'price_unit': (-tmp_value + negative_stock_value) / move.product_qty,
+ }
+ move.write(vals)
+ elif move.product_id.cost_method in ['standard', 'average']:
curr_rounding = move.company_id.currency_id.rounding
- avg_price_unit = average_price[move.product_id.id]
- move.value = float_round(-avg_price_unit * move.product_qty, precision_rounding=curr_rounding)
- move.remaining_qty = 0
- move.cumulated_value = move.product_id._get_latest_cumulated_value(exclude_move=move) + move.value
- move.last_done_qty = move.product_id.qty_available
- elif move.product_id.cost_method == 'standard':
- move.value = - move.product_id.standard_price * move.product_qty
-
+ value = -float_round(move.product_id.standard_price * move.product_qty, precision_rounding=curr_rounding)
+ move.write({
+ 'value': value,
+ 'price_unit': value / move.product_qty,
+ })
for move in res.filtered(lambda m: m.product_id.valuation == 'real_time'):
move._account_entry_move()
return res
+ @api.multi
+ def product_price_update_before_done(self):
+ tmpl_dict = defaultdict(lambda: 0.0)
+ # adapt standard price on incomming moves if the product cost_method is 'average'
+ std_price_update = {}
+ for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move.product_id.cost_method == 'average'):
+ product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
+
+ if product_tot_qty_available == 0:
+ new_std_price = move.get_price_unit()
+ else:
+ # Get the standard price
+ amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
+ new_std_price = ((amount_unit * product_tot_qty_available) + (move.get_price_unit() * move.product_qty)) / (product_tot_qty_available + move.product_qty)
+
+ tmpl_dict[move.product_id.id] += move.product_qty
+ # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
+ 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`.
+ """
+ # FIXME: sort by date (does filtered lose the order?)
+ for move in self.filtered(lambda m: (m._is_in() or m._is_out()) and m.remaining_qty < 0):
+ domain = [
+ '|',
+ ('date', '>', move.date),
+ '&',
+ ('date', '=', move.date),
+ ('id', '>', move.id)
+ ]
+ domain += self._get_in_domain()
+ candidates = self.search(domain, order='date, id')
+ if not candidates:
+ return
+ qty_to_take_on_candidates = abs(move.remaining_qty)
+ 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
+
+ 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
+
+ corrected_value = move.remaining_value + tmp_value
+ if move.product_id.valuation == 'real_time':
+ 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)._account_entry_move()
+ else:
+ move.with_context(force_valuation_amount=corrected_value)._account_entry_move()
+
+ if qty_to_take_on_candidates == 0:
+ move.write({
+ 'value': move.value - corrected_value,
+ 'remaining_value': 0,
+ 'remaining_qty': 0,
+ })
+ elif qty_to_take_on_candidates > 0:
+ # It's possible that `remaining_value` is equals to 0 even if the move needs to be
+ # compensated (negative stock for the first ever out in FIFO).
+ move.write({
+ 'value': move.value - corrected_value,
+ 'remaining_value': 0 if not move.remaining_value else move.remaining_value + corrected_value,
+ 'remaining_qty': -qty_to_take_on_candidates,
+ })
+
@api.multi
def _get_accounting_data_for_valuation(self):
""" Return the accounts and journal to use to post Journal Entries for
@@ -415,7 +431,9 @@ class StockMove(models.Model):
'journal_id': journal_id,
'line_ids': move_lines,
'date': date,
- 'ref': self.picking_id.name})
+ 'ref': self.picking_id.name,
+ 'stock_move_id': self.id,
+ })
new_account_move.post()
def _account_entry_move(self):
diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py
index d5178fd7ca9..fd50a6aef98 100644
--- a/addons/stock_account/tests/test_stockvaluation.py
+++ b/addons/stock_account/tests/test_stockvaluation.py
@@ -17,7 +17,657 @@ class TestStockValuation(TransactionCase):
'categ_id': self.env.ref('product.product_category_all').id,
})
+ self.product1.product_tmpl_id.valuation = 'real_time'
+ Account = self.env['account.account']
+ self.stock_input_account = Account.create({
+ 'name': 'Stock Input',
+ 'code': 'StockIn',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
+ self.stock_output_account = Account.create({
+ 'name': 'Stock Output',
+ 'code': 'StockOut',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
+ self.stock_valuation_account = Account.create({
+ 'name': 'Stock Valuation',
+ 'code': 'Stock Valuation',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
+ self.stock_journal = self.env['account.journal'].create({
+ 'name': 'Stock Journal',
+ 'code': 'STJTEST',
+ 'type': 'general',
+ })
+ self.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': self.stock_input_account.id,
+ 'property_stock_account_output_categ_id': self.stock_output_account.id,
+ 'property_stock_valuation_account_id': self.stock_valuation_account.id,
+ 'property_stock_journal': self.stock_journal.id,
+ })
+ self.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': self.stock_input_account.id,
+ 'property_stock_account_output_categ_id': self.stock_output_account.id,
+ 'property_stock_valuation_account_id': self.stock_valuation_account.id,
+ 'property_stock_journal': self.stock_journal.id,
+ })
+
+ def _get_stock_input_move_lines(self):
+ return self.env['account.move.line'].search([
+ ('product_id', '=', self.product1.id),
+ ('account_id', '=', self.stock_input_account.id),
+ ], order='date, id')
+
+ def _get_stock_output_move_lines(self):
+ return self.env['account.move.line'].search([
+ ('product_id', '=', self.product1.id),
+ ('account_id', '=', self.stock_output_account.id),
+ ], order='date, id')
+
+ def _get_stock_valuation_move_lines(self):
+ return self.env['account.move.line'].search([
+ ('product_id', '=', self.product1.id),
+ ('account_id', '=', self.stock_valuation_account.id),
+ ], order='date, id')
+
def test_fifo_perpetual_1(self):
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+
+ # ---------------------------------------------------------------------
+ # receive 10 units @ 10.00 per unit
+ # ---------------------------------------------------------------------
+ move1 = self.env['stock.move'].create({
+ 'name': 'IN 10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10.0,
+ })
+ move1.action_confirm()
+ move1.action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1.action_done()
+
+ # stock_account values for move1
+ self.assertEqual(move1.product_uom_qty, 10.0)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 10.0)
+ self.assertEqual(move1.value, 100.0)
+ self.assertEqual(move1.remaining_value, 100.0)
+
+ # account values for move1
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 1)
+ move1_input_aml = input_aml[-1]
+ self.assertEqual(move1_input_aml.debit, 0)
+ self.assertEqual(move1_input_aml.credit, 100)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move1_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 1)
+ self.assertEqual(move1_valuation_aml.debit, 100)
+ self.assertEqual(move1_valuation_aml.credit, 0)
+
+ output_aml = self._get_stock_output_move_lines()
+ self.assertEqual(len(output_aml), 0)
+
+ # link between stock move and account move
+ self.assertEqual(len(move1.account_move_ids), 1)
+ self.assertTrue(set(move1.account_move_ids.line_ids.ids) == {move1_valuation_aml.id, move1_input_aml.id})
+
+ # ---------------------------------------------------------------------
+ # receive 10 units @ 8.00 per unit
+ # ---------------------------------------------------------------------
+ move2 = self.env['stock.move'].create({
+ 'name': 'IN 10 units @ 8.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 8.0,
+ })
+ move2.action_confirm()
+ move2.action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2.action_done()
+
+ # stock_account values for move2
+ self.assertEqual(move2.product_uom_qty, 10.0)
+ self.assertEqual(move2.price_unit, 8.0)
+ self.assertEqual(move2.remaining_qty, 10.0)
+ self.assertEqual(move2.value, 80.0)
+ self.assertEqual(move2.remaining_value, 80.0)
+
+ # account values for move2
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 2)
+ move2_input_aml = input_aml[-1]
+ self.assertEqual(move2_input_aml.debit, 0)
+ self.assertEqual(move2_input_aml.credit, 80)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 2)
+ self.assertEqual(move2_valuation_aml.debit, 80)
+ self.assertEqual(move2_valuation_aml.credit, 0)
+
+ output_aml = self._get_stock_output_move_lines()
+ self.assertEqual(len(output_aml), 0)
+
+ # link between stock move and account move
+ self.assertEqual(len(move2.account_move_ids), 1)
+ self.assertTrue(set(move2.account_move_ids.line_ids.ids) == {move2_valuation_aml.id, move2_input_aml.id})
+
+ # older moves
+ self.assertEqual(move1.product_uom_qty, 10.0)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 10.0)
+ self.assertEqual(move1.value, 100.0)
+ self.assertEqual(move1.remaining_value, 100.0)
+ self.assertEqual(len(move1.account_move_ids), 1)
+ self.assertTrue(set(move1.account_move_ids.line_ids.ids) == {move1_valuation_aml.id, move1_input_aml.id})
+
+ # ---------------------------------------------------------------------
+ # sale 3 units
+ # ---------------------------------------------------------------------
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 3 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 3.0,
+ })
+ move3.action_confirm()
+ move3.action_assign()
+ move3.move_line_ids.qty_done = 3.0
+ move3.action_done()
+
+ # stock_account values for move3
+ self.assertEqual(move3.product_uom_qty, 3.0)
+ self.assertEqual(move3.remaining_qty, 0.0) # unused in out move
+ self.assertEqual(move3.value, -30.0) # took 3 items from move 1 @ 10.00 per unit
+ self.assertEqual(move3.remaining_value, 0.0) # took 3 items from move 1 @ 10.00 per unit
+
+ # account values for move3
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 2)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move3_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 3)
+ self.assertEqual(move3_valuation_aml.debit, 0)
+ self.assertEqual(move3_valuation_aml.credit, 30)
+
+ output_aml = self._get_stock_output_move_lines()
+ move3_output_aml = output_aml[-1]
+ self.assertEqual(len(output_aml), 1)
+ self.assertEqual(move3_output_aml.debit, 30)
+ self.assertEqual(move3_output_aml.credit, 0)
+
+ # link between stock move and account move
+ self.assertEqual(len(move3.account_move_ids), 1)
+ self.assertTrue(set(move3.account_move_ids.line_ids.ids) == {move3_valuation_aml.id, move3_output_aml.id})
+
+ # older moves
+ self.assertEqual(move1.product_uom_qty, 10.0)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 7)
+ self.assertEqual(move1.remaining_value, 70)
+ self.assertEqual(move1.value, 100.0)
+ self.assertEqual(move2.product_uom_qty, 10.0)
+ self.assertEqual(move2.price_unit, 8.0)
+ self.assertEqual(move2.remaining_qty, 10.0)
+ self.assertEqual(move2.value, 80.0)
+ self.assertEqual(move2.remaining_value, 80.0)
+ self.assertTrue(set(move1.account_move_ids.line_ids.ids) == {move1_valuation_aml.id, move1_input_aml.id})
+ self.assertTrue(set(move2.account_move_ids.line_ids.ids) == {move2_valuation_aml.id, move2_input_aml.id})
+
+ # ---------------------------------------------------------------------
+ # Increase received quantity of move1 from 10 to 12, it should updates
+ # the remaining quantity, the value and remaining value on this move
+ # without impacting any of the next ones.
+ # ---------------------------------------------------------------------
+ move1.quantity_done = 12
+
+ # stock_account values for move3
+ self.assertEqual(move1.product_uom_qty, 12.0)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 9.0)
+ self.assertEqual(move1.value, 120.0)
+ self.assertEqual(move1.remaining_value, 90.0)
+
+ # account values for move1
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 3)
+ move1_correction_input_aml = input_aml[-1]
+ self.assertEqual(move1_correction_input_aml.debit, 0)
+ self.assertEqual(move1_correction_input_aml.credit, 20)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move1_correction_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 4)
+ self.assertEqual(move1_correction_valuation_aml.debit, 20)
+ self.assertEqual(move1_correction_valuation_aml.credit, 0)
+
+ output_aml = self._get_stock_output_move_lines()
+ self.assertEqual(len(output_aml), 1)
+
+ # link between stock move and account move
+ self.assertEqual(len(move1.account_move_ids), 2)
+ self.assertTrue(set(move1.account_move_ids.mapped('line_ids').ids) == {move1_input_aml.id, move1_valuation_aml.id, move1_correction_input_aml.id, move1_correction_valuation_aml.id})
+
+ # older moves
+ self.assertEqual(move2.product_uom_qty, 10.0)
+ self.assertEqual(move2.price_unit, 8.0)
+ self.assertEqual(move2.remaining_qty, 10.0)
+ self.assertEqual(move2.value, 80.0)
+ self.assertEqual(move2.remaining_value, 80.0)
+ self.assertEqual(move3.product_uom_qty, 3.0)
+ self.assertEqual(move3.remaining_qty, 0.0)
+ self.assertEqual(move3.value, -30.0)
+ self.assertEqual(move3.remaining_value, 0.0)
+ self.assertTrue(set(move2.account_move_ids.line_ids.ids) == {move2_valuation_aml.id, move2_input_aml.id})
+ self.assertTrue(set(move3.account_move_ids.line_ids.ids) == {move3_valuation_aml.id, move3_output_aml.id})
+
+ # ---------------------------------------------------------------------
+ # Sale 9 units, the units available from the previous increase are sent
+ # immediately.
+ # ---------------------------------------------------------------------
+ move4 = self.env['stock.move'].create({
+ 'name': 'Sale 9 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 9.0,
+ })
+ move4.action_confirm()
+ move4.action_assign()
+ move4.move_line_ids.qty_done = 9.0
+ move4.action_done()
+
+ # stock_account values for move4
+ self.assertEqual(move4.product_uom_qty, 9.0)
+ self.assertEqual(move4.remaining_qty, 0.0) # unused in out move
+ self.assertEqual(move4.value, -90.0) # took 9 items from move 1 @ 10.00 per unit
+ self.assertEqual(move4.remaining_value, 0.0) # took 3 items from move 1 @ 10.00 per unit
+
+ # account values for move4
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 3)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move4_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 5)
+ self.assertEqual(move4_valuation_aml.debit, 0)
+ self.assertEqual(move4_valuation_aml.credit, 90)
+
+ output_aml = self._get_stock_output_move_lines()
+ move4_output_aml = output_aml[-1]
+ self.assertEqual(len(output_aml), 2)
+ self.assertEqual(move4_output_aml.debit, 90)
+ self.assertEqual(move4_output_aml.credit, 0)
+
+ # link between stock move and account move
+ self.assertEqual(len(move4.account_move_ids), 1)
+ self.assertTrue(set(move4.account_move_ids.line_ids.ids) == {move4_valuation_aml.id, move4_output_aml.id})
+
+ # older moves
+ self.assertEqual(move1.product_uom_qty, 12)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 0)
+ self.assertEqual(move1.value, 120)
+ self.assertEqual(move1.remaining_value, 0)
+ self.assertEqual(move2.product_uom_qty, 10.0)
+ self.assertEqual(move2.price_unit, 8.0)
+ self.assertEqual(move2.remaining_qty, 10.0)
+ self.assertEqual(move2.value, 80.0)
+ self.assertEqual(move2.remaining_value, 80.0)
+ self.assertEqual(move3.product_uom_qty, 3.0)
+ self.assertEqual(move3.remaining_qty, 0.0)
+ self.assertEqual(move3.value, -30.0)
+ self.assertEqual(move3.remaining_value, 0.0)
+ self.assertTrue(set(move1.account_move_ids.mapped('line_ids').ids) == {move1_input_aml.id, move1_valuation_aml.id, move1_correction_input_aml.id, move1_correction_valuation_aml.id})
+ self.assertTrue(set(move2.account_move_ids.line_ids.ids) == {move2_valuation_aml.id, move2_input_aml.id})
+ self.assertTrue(set(move3.account_move_ids.line_ids.ids) == {move3_valuation_aml.id, move3_output_aml.id})
+
+ # ---------------------------------------------------------------------
+ # Sale 20 units, we fall in negative stock for 10 units. Theses are
+ # valued at the last FIFO cost and the total is negative.
+ # ---------------------------------------------------------------------
+ move5 = self.env['stock.move'].create({
+ 'name': 'Sale 20 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20.0,
+ })
+ move5.action_confirm()
+ move5.action_assign()
+ move5.move_line_ids.qty_done = 20.0
+ move5.action_done()
+
+ # stock_account values for move5
+ self.assertEqual(move5.product_uom_qty, 20.0)
+ self.assertEqual(move5.remaining_qty, -10.0)
+ self.assertEqual(move5.value, -160.0)
+ self.assertEqual(move5.remaining_value, -80.0)
+
+ # account values for move5
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 3)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move5_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 6)
+ self.assertEqual(move5_valuation_aml.debit, 0)
+ self.assertEqual(move5_valuation_aml.credit, 160)
+
+ output_aml = self._get_stock_output_move_lines()
+ move5_output_aml = output_aml[-1]
+ self.assertEqual(len(output_aml), 3)
+ self.assertEqual(move5_output_aml.debit, 160)
+ self.assertEqual(move5_output_aml.credit, 0)
+
+ # link between stock move and account move
+ self.assertEqual(len(move5.account_move_ids), 1)
+ self.assertTrue(set(move5.account_move_ids.line_ids.ids) == {move5_valuation_aml.id, move5_output_aml.id})
+
+ # older moves
+ self.assertEqual(move1.product_uom_qty, 12)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 0)
+ self.assertEqual(move1.value, 120)
+ self.assertEqual(move1.remaining_value, 0)
+ self.assertEqual(move2.product_uom_qty, 10.0)
+ self.assertEqual(move2.price_unit, 8.0)
+ self.assertEqual(move2.remaining_qty, 0)
+ self.assertEqual(move2.value, 80.0)
+ self.assertEqual(move2.remaining_value, 0)
+ self.assertEqual(move3.product_uom_qty, 3.0)
+ self.assertEqual(move3.remaining_qty, 0.0)
+ self.assertEqual(move3.value, -30.0)
+ self.assertEqual(move3.remaining_value, 0.0)
+ self.assertEqual(move4.product_uom_qty, 9.0)
+ self.assertEqual(move4.remaining_qty, 0.0)
+ self.assertEqual(move4.value, -90.0)
+ self.assertEqual(move4.remaining_value, 0)
+ self.assertTrue(set(move1.account_move_ids.mapped('line_ids').ids) == {move1_input_aml.id, move1_valuation_aml.id, move1_correction_input_aml.id, move1_correction_valuation_aml.id})
+ self.assertTrue(set(move2.account_move_ids.line_ids.ids) == {move2_valuation_aml.id, move2_input_aml.id})
+ self.assertTrue(set(move3.account_move_ids.line_ids.ids) == {move3_valuation_aml.id, move3_output_aml.id})
+ self.assertTrue(set(move4.account_move_ids.line_ids.ids) == {move4_valuation_aml.id, move4_output_aml.id})
+
+ # ---------------------------------------------------------------------
+ # Receive 10 units @ 12.00 to counterbalance the negative, we do not do
+ # the operation right now.
+ # ---------------------------------------------------------------------
+ move6 = self.env['stock.move'].create({
+ 'name': 'IN 10 units @ 12.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 12.0,
+ })
+ move6.action_confirm()
+ move6.action_assign()
+ move6.move_line_ids.qty_done = 10.0
+ move6.action_done()
+
+ # stock_account values for move6
+ self.assertEqual(move6.product_uom_qty, 10)
+ self.assertEqual(move6.price_unit, 12.0)
+ self.assertEqual(move6.remaining_qty, 10.0)
+ self.assertEqual(move6.value, 120)
+ self.assertEqual(move6.remaining_value, 120)
+
+ # account values for move6
+ input_aml = self._get_stock_input_move_lines()
+ move6_input_aml = input_aml[-1]
+ self.assertEqual(len(input_aml), 4)
+ self.assertEqual(move6_input_aml.debit, 0)
+ self.assertEqual(move6_input_aml.credit, 120)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move6_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 7)
+ self.assertEqual(move6_valuation_aml.debit, 120)
+ self.assertEqual(move6_valuation_aml.credit, 0)
+
+ output_aml = self._get_stock_output_move_lines()
+ self.assertEqual(len(output_aml), 3)
+
+ # link between stock move and account move
+ self.assertEqual(len(move6.account_move_ids), 1)
+ self.assertTrue(set(move6.account_move_ids.line_ids.ids) == {move6_valuation_aml.id, move6_input_aml.id})
+
+ # older moves
+ self.assertEqual(move1.product_uom_qty, 12)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 0)
+ self.assertEqual(move1.value, 120)
+ self.assertEqual(move1.remaining_value, 0)
+ self.assertEqual(move2.product_uom_qty, 10.0)
+ self.assertEqual(move2.price_unit, 8.0)
+ self.assertEqual(move2.remaining_qty, 0)
+ self.assertEqual(move2.value, 80.0)
+ self.assertEqual(move2.remaining_value, 0)
+ self.assertEqual(move3.product_uom_qty, 3.0)
+ self.assertEqual(move3.remaining_qty, 0.0)
+ self.assertEqual(move3.value, -30.0)
+ self.assertEqual(move3.remaining_value, 0.0)
+ self.assertEqual(move4.product_uom_qty, 9.0)
+ self.assertEqual(move4.remaining_qty, 0.0)
+ self.assertEqual(move4.value, -90.0)
+ self.assertEqual(move4.remaining_value, 0)
+ self.assertEqual(move5.product_uom_qty, 20.0)
+ self.assertEqual(move5.remaining_qty, -10.0)
+ self.assertEqual(move5.value, -160.0)
+ self.assertEqual(move5.remaining_value, -80.0)
+ self.assertTrue(set(move1.account_move_ids.mapped('line_ids').ids) == {move1_input_aml.id, move1_valuation_aml.id, move1_correction_input_aml.id, move1_correction_valuation_aml.id})
+ self.assertTrue(set(move2.account_move_ids.line_ids.ids) == {move2_valuation_aml.id, move2_input_aml.id})
+ self.assertTrue(set(move3.account_move_ids.line_ids.ids) == {move3_valuation_aml.id, move3_output_aml.id})
+ self.assertTrue(set(move4.account_move_ids.line_ids.ids) == {move4_valuation_aml.id, move4_output_aml.id})
+ self.assertTrue(set(move5.account_move_ids.line_ids.ids) == {move5_valuation_aml.id, move5_output_aml.id})
+
+ # ---------------------------------------------------------------------
+ # Vacuum is called, we cleanup the negatives.
+ # ---------------------------------------------------------------------
+ move5._fifo_vacuum()
+
+ # account values after vacuum
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 4)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ vacuum_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 8)
+ self.assertEqual(vacuum_valuation_aml.balance, -40)
+
+ output_aml = self._get_stock_output_move_lines()
+ vacuum_output_aml = output_aml[-1]
+ self.assertEqual(len(output_aml), 4)
+ self.assertEqual(vacuum_output_aml.balance, 40)
+
+ # stock_account values
+ self.assertEqual(move1.product_uom_qty, 12)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 0)
+ self.assertEqual(move1.value, 120)
+ self.assertEqual(move1.remaining_value, 0)
+ self.assertEqual(move2.product_uom_qty, 10.0)
+ self.assertEqual(move2.price_unit, 8.0)
+ self.assertEqual(move2.remaining_qty, 0)
+ self.assertEqual(move2.value, 80.0)
+ self.assertEqual(move2.remaining_value, 0)
+ self.assertEqual(move3.product_uom_qty, 3.0)
+ self.assertEqual(move3.remaining_qty, 0.0)
+ self.assertEqual(move3.value, -30.0)
+ self.assertEqual(move3.remaining_value, 0.0)
+ self.assertEqual(move4.product_uom_qty, 9.0)
+ self.assertEqual(move4.remaining_qty, 0.0)
+ self.assertEqual(move4.value, -90.0)
+ self.assertEqual(move4.remaining_value, 0)
+ self.assertEqual(move5.product_uom_qty, 20.0)
+ self.assertEqual(move5.remaining_qty, 0.0)
+ self.assertEqual(move5.value, -200.0)
+ self.assertEqual(move5.remaining_value, 0.0)
+ self.assertEqual(move6.product_uom_qty, 10)
+ self.assertEqual(move6.price_unit, 12.0)
+ self.assertEqual(move6.remaining_qty, 0.0)
+ self.assertEqual(move6.value, 120)
+ self.assertEqual(move6.remaining_value, 0)
+ self.assertTrue(set(move1.account_move_ids.mapped('line_ids').ids) == {move1_input_aml.id, move1_valuation_aml.id, move1_correction_input_aml.id, move1_correction_valuation_aml.id})
+ self.assertTrue(set(move2.account_move_ids.line_ids.ids) == {move2_valuation_aml.id, move2_input_aml.id})
+ self.assertTrue(set(move3.account_move_ids.line_ids.ids) == {move3_valuation_aml.id, move3_output_aml.id})
+ self.assertTrue(set(move4.account_move_ids.line_ids.ids) == {move4_valuation_aml.id, move4_output_aml.id})
+ # a new account move was created for move 5, to compensate the negative stock we had
+ move5_correction_account_move = self.env['account.move'].browse(max(move5.account_move_ids.ids))
+ move5_correction_output_aml = move5_correction_account_move.line_ids.filtered(lambda ml: ml.account_id == self.stock_output_account)
+ self.assertEqual(move5_correction_output_aml.debit, 40)
+ self.assertEqual(move5_correction_output_aml.credit, 0)
+ move5_correction_valuation_aml = move5_correction_account_move.line_ids.filtered(lambda ml: ml.account_id == self.stock_valuation_account)
+ self.assertEqual(move5_correction_valuation_aml.debit, 0)
+ self.assertEqual(move5_correction_valuation_aml.credit, 40)
+ self.assertTrue(set(move5.account_move_ids.mapped('line_ids').ids) == {move5_valuation_aml.id, move5_output_aml.id, move5_correction_output_aml.id, move5_correction_valuation_aml.id})
+ self.assertTrue(set(move6.account_move_ids.line_ids.ids) == {move6_valuation_aml.id, move6_input_aml.id})
+
+ # ---------------------------------------------------------------------
+ # Edit move6, receive less
+ # ---------------------------------------------------------------------
+ move6.quantity_done = 8
+
+ # stock_account values for move6
+ self.assertEqual(move6.product_uom_qty, 8)
+ self.assertEqual(move6.price_unit, 12)
+ self.assertEqual(move6.remaining_qty, -2)
+ self.assertEqual(move6.value, 96)
+ self.assertEqual(move6.remaining_value, -24)
+
+ # account values for move1
+ input_aml = self._get_stock_input_move_lines()
+ move6_correction_input_aml = input_aml[-1]
+ self.assertEqual(move6_correction_input_aml.debit, 24)
+ self.assertEqual(move6_correction_input_aml.credit, 0)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move6_correction_valuation_aml = valuation_aml[-1]
+ self.assertEqual(move6_correction_valuation_aml.debit, 0)
+ self.assertEqual(move6_correction_valuation_aml.credit, 24)
+
+ # link between stock move and account move
+ self.assertEqual(len(move6.account_move_ids), 2)
+ self.assertTrue(set(move6.account_move_ids.mapped('line_ids').ids) == {move6_input_aml.id, move6_valuation_aml.id, move6_correction_input_aml.id, move6_correction_valuation_aml.id})
+
+ # -----------------------------------------------------------
+ # receive 4, do not counterbalance now
+ # -----------------------------------------------------------
+ move7 = self.env['stock.move'].create({
+ 'name': 'IN 4 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ 'price_unit': 15.0,
+ })
+ move7.action_confirm()
+ move7.action_assign()
+ move7.move_line_ids.qty_done = 4.0
+ move7.action_done()
+
+ # stock_account values for move1
+ self.assertEqual(move7.product_uom_qty, 4.0)
+ self.assertEqual(move7.price_unit, 15.0)
+ self.assertEqual(move7.remaining_qty, 4.0)
+ self.assertEqual(move7.value, 60.0)
+ self.assertEqual(move7.remaining_value, 60.0)
+
+ # account values for move7
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 6)
+ move7_input_aml = input_aml[-1]
+ self.assertEqual(move7_input_aml.debit, 0)
+ self.assertEqual(move7_input_aml.credit, 60)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move7_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 10)
+ self.assertEqual(move7_valuation_aml.debit, 60)
+ self.assertEqual(move7_valuation_aml.credit, 0)
+
+ # link between stock move and account move
+ self.assertEqual(len(move7.account_move_ids), 1)
+ self.assertTrue(set(move7.account_move_ids.line_ids.ids) == {move7_valuation_aml.id, move7_input_aml.id})
+
+ # -----------------------------------------------------------
+ # vacuum, compensate in
+ # -----------------------------------------------------------
+ move6._fifo_vacuum()
+
+ # account values after vacuum
+ input_aml = self._get_stock_input_move_lines()
+ self.assertEqual(len(input_aml), 7)
+ move6_correction2_input_aml = input_aml[-1]
+ self.assertEqual(move6_correction2_input_aml.debit, 6)
+ self.assertEqual(move6_correction2_input_aml.credit, 0)
+
+ valuation_aml = self._get_stock_valuation_move_lines()
+ move6_correction2_valuation_aml = valuation_aml[-1]
+ self.assertEqual(len(valuation_aml), 11)
+ self.assertEqual(move6_correction2_valuation_aml.debit, 0)
+ self.assertEqual(move6_correction2_valuation_aml.credit, 6)
+
+ # stock_account values
+ self.assertEqual(move1.product_uom_qty, 12)
+ self.assertEqual(move1.price_unit, 10.0)
+ self.assertEqual(move1.remaining_qty, 0)
+ self.assertEqual(move1.value, 120)
+ self.assertEqual(move1.remaining_value, 0)
+ self.assertEqual(move2.product_uom_qty, 10.0)
+ self.assertEqual(move2.price_unit, 8.0)
+ self.assertEqual(move2.remaining_qty, 0)
+ self.assertEqual(move2.value, 80.0)
+ self.assertEqual(move2.remaining_value, 0)
+ self.assertEqual(move3.product_uom_qty, 3.0)
+ self.assertEqual(move3.remaining_qty, 0.0)
+ self.assertEqual(move3.value, -30.0)
+ self.assertEqual(move3.remaining_value, 0.0)
+ self.assertEqual(move4.product_uom_qty, 9.0)
+ self.assertEqual(move4.remaining_qty, 0.0)
+ self.assertEqual(move4.value, -90.0)
+ self.assertEqual(move4.remaining_value, 0)
+ self.assertEqual(move5.product_uom_qty, 20.0)
+ self.assertEqual(move5.remaining_qty, 0.0)
+ self.assertEqual(move5.value, -200.0)
+ self.assertEqual(move5.remaining_value, 0.0)
+ self.assertEqual(move6.product_uom_qty, 8)
+ self.assertEqual(move6.price_unit, 12.0)
+ self.assertEqual(move6.remaining_qty, 0.0)
+ self.assertEqual(move6.value, 90)
+ self.assertEqual(move6.remaining_value, 0)
+ self.assertTrue(set(move1.account_move_ids.mapped('line_ids').ids) == {move1_input_aml.id, move1_valuation_aml.id, move1_correction_input_aml.id, move1_correction_valuation_aml.id})
+ self.assertTrue(set(move2.account_move_ids.line_ids.ids) == {move2_valuation_aml.id, move2_input_aml.id})
+ self.assertTrue(set(move3.account_move_ids.line_ids.ids) == {move3_valuation_aml.id, move3_output_aml.id})
+ self.assertTrue(set(move4.account_move_ids.line_ids.ids) == {move4_valuation_aml.id, move4_output_aml.id})
+ # a new account move was created for move 5, to compensate the negative stock we had
+ move5_correction_account_move = self.env['account.move'].browse(max(move5.account_move_ids.ids))
+ move5_correction_output_aml = move5_correction_account_move.line_ids.filtered(lambda ml: ml.account_id == self.stock_output_account)
+ self.assertEqual(move5_correction_output_aml.debit, 40)
+ self.assertEqual(move5_correction_output_aml.credit, 0)
+ move5_correction_valuation_aml = move5_correction_account_move.line_ids.filtered(lambda ml: ml.account_id == self.stock_valuation_account)
+ self.assertEqual(move5_correction_valuation_aml.debit, 0)
+ self.assertEqual(move5_correction_valuation_aml.credit, 40)
+ self.assertTrue(set(move5.account_move_ids.mapped('line_ids').ids) == {move5_valuation_aml.id, move5_output_aml.id, move5_correction_output_aml.id, move5_correction_valuation_aml.id})
+ self.assertTrue(set(move6.account_move_ids.mapped('line_ids').ids) == {move6_valuation_aml.id, move6_input_aml.id, move6_correction_input_aml.id, move6_correction_valuation_aml.id, move6_correction2_input_aml.id, move6_correction2_valuation_aml.id})
+
+ def test_fifo_perpetual_2(self):
# http://accountingexplained.com/financial/inventories/fifo-method
self.product1.product_tmpl_id.cost_method = 'fifo'
@@ -37,7 +687,6 @@ class TestStockValuation(TransactionCase):
move1.action_done()
self.assertEqual(move1.value, 1020.0)
- self.assertEqual(move1.cumulated_value, 1020.0)
self.assertEqual(move1.remaining_qty, 68.0)
@@ -57,7 +706,6 @@ class TestStockValuation(TransactionCase):
move2.action_done()
self.assertEqual(move2.value, 2170.0)
- self.assertEqual(move2.cumulated_value, 3190.0)
self.assertEqual(move1.remaining_qty, 68.0)
self.assertEqual(move2.remaining_qty, 140.0)
@@ -76,12 +724,10 @@ class TestStockValuation(TransactionCase):
move3.move_line_ids.qty_done = 94.0
move3.action_done()
- self.assertEqual(move3.price_unit, 0.0) # unused in out moves
# note: it' ll have to get 68 units from the first batch and 26 from the second one
# so its value should be -((68*15) + (26*15.5)) = -1423
self.assertEqual(move3.value, -1423.0)
- self.assertEqual(move3.cumulated_value, 1767.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 114)
@@ -103,7 +749,6 @@ class TestStockValuation(TransactionCase):
move4.action_done()
self.assertEqual(move4.value, 640.0)
- self.assertEqual(move4.cumulated_value, 2407.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 114)
@@ -126,7 +771,6 @@ class TestStockValuation(TransactionCase):
move5.action_done()
self.assertEqual(move5.value, 1287.0)
- self.assertEqual(move5.cumulated_value, 3694.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 114)
@@ -151,7 +795,6 @@ class TestStockValuation(TransactionCase):
# note: it' ll have to get 114 units from the move2 and 2 from move4
# so its value should be -((114*15.5) + (2*16)) = 1735
self.assertEqual(move6.value, -1799.0)
- self.assertEqual(move6.cumulated_value, 1895.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 0)
@@ -177,7 +820,6 @@ class TestStockValuation(TransactionCase):
# note: it' ll have to get 38 units from the move4 and 24 from move5
# so its value should be -((38*16) + (24*16.5)) = 608 + 396
self.assertEqual(move7.value, -1004.0)
- self.assertEqual(move7.cumulated_value, 891.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 0)
@@ -206,7 +848,6 @@ class TestStockValuation(TransactionCase):
move8.action_done()
self.assertEqual(move8.value, 0.0)
- self.assertEqual(move8.cumulated_value, 0.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 0)
@@ -234,7 +875,6 @@ class TestStockValuation(TransactionCase):
# note: it' ll have to get 10 units from move5 so its value should
# be -(10*16.50) = -165
self.assertEqual(move9.value, -165.0)
- self.assertEqual(move9.cumulated_value, 726.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 0)
@@ -246,8 +886,7 @@ class TestStockValuation(TransactionCase):
self.assertEqual(move8.remaining_qty, 0.0) # unused in internal moves
self.assertEqual(move9.remaining_qty, 0.0) # unused in out moves
- def test_fifo_perpetual_2(self):
- # https://docs.google.com/spreadsheets/d/1NI0u9N1gFByXxYHfdiXuxQCrycXXOh76TpPQ3CWeyDw/edit?ts=58da749b#gid=0
+ def test_fifo_perpetual_3(self):
self.product1.cost_method = 'fifo'
# in 10 @ 100
@@ -266,7 +905,6 @@ class TestStockValuation(TransactionCase):
move1.action_done()
self.assertEqual(move1.value, 1000.0)
- self.assertEqual(move1.cumulated_value, 1000.0)
self.assertEqual(move1.remaining_qty, 10.0)
@@ -286,7 +924,6 @@ class TestStockValuation(TransactionCase):
move2.action_done()
self.assertEqual(move2.value, 800.0)
- self.assertEqual(move2.cumulated_value, 1800.0)
self.assertEqual(move1.remaining_qty, 10.0)
self.assertEqual(move2.remaining_qty, 10.0)
@@ -305,12 +942,10 @@ class TestStockValuation(TransactionCase):
move3.move_line_ids.qty_done = 15.0
move3.action_done()
- self.assertEqual(move3.price_unit, 0.0) # unused in out moves
# note: it' ll have to get 10 units from move1 and 5 from move2
# so its value should be -((10*100) + (5*80)) = -1423
self.assertEqual(move3.value, -1400.0)
- self.assertEqual(move3.cumulated_value, 400.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 5)
@@ -332,7 +967,6 @@ class TestStockValuation(TransactionCase):
move4.action_done()
self.assertEqual(move4.value, 300.0)
- self.assertEqual(move4.cumulated_value, 700.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 5)
@@ -356,7 +990,6 @@ class TestStockValuation(TransactionCase):
# note: it' ll have to get 5 units from the move2 and 2 from move4
# so its value should be -((5*80) + (2*60)) = 520
self.assertEqual(move5.value, -520.0)
- self.assertEqual(move5.cumulated_value, 180.0)
self.assertEqual(move1.remaining_qty, 0)
self.assertEqual(move2.remaining_qty, 0)
@@ -364,103 +997,6 @@ class TestStockValuation(TransactionCase):
self.assertEqual(move4.remaining_qty, 3.0)
self.assertEqual(move5.remaining_qty, 0.0) # unused in out moves
- def test_average_perpetual_1(self):
- # http://accountingexplained.com/financial/inventories/avco-method
-
- self.product1.product_tmpl_id.cost_method = 'average'
-
- # Beginning Inventory: 60 units @ 15.00 per unit
- move1 = self.env['stock.move'].create({
- 'name': '60 units @ 15.00 per unit',
- 'location_id': self.supplier_location.id,
- 'location_dest_id': self.stock_location.id,
- 'product_id': self.product1.id,
- 'product_uom': self.uom_unit.id,
- 'product_uom_qty': 60.0,
- 'price_unit': 15,
- })
- move1.action_confirm()
- move1.action_assign()
- move1.move_line_ids.qty_done = 60.0
- move1.action_done()
-
- self.assertEqual(move1.value, 900.0)
- self.assertEqual(move1.cumulated_value, 900.0)
- self.assertEqual(move1.remaining_qty, 0.0) # unusedin average move
-
- # Purchase 140 units @ 15.50 per unit
- move2 = self.env['stock.move'].create({
- 'name': '140 units @ 15.50 per unit',
- 'location_id': self.supplier_location.id,
- 'location_dest_id': self.stock_location.id,
- 'product_id': self.product1.id,
- 'product_uom': self.uom_unit.id,
- 'product_uom_qty': 140.0,
- 'price_unit': 15.50,
- })
- move2.action_confirm()
- move2.action_assign()
- move2.move_line_ids.qty_done = 140.0
- move2.action_done()
-
- self.assertEqual(move2.value, 2170.0)
- self.assertEqual(move2.cumulated_value, 3070.0)
-
- # Sale 190 units @ 15.35 per unit
- move3 = self.env['stock.move'].create({
- 'name': 'Sale 190 units @ 19.00 per unit',
- 'location_id': self.stock_location.id,
- 'location_dest_id': self.customer_location.id,
- 'product_id': self.product1.id,
- 'product_uom': self.uom_unit.id,
- 'product_uom_qty': 190.0,
- })
- move3.action_confirm()
- move3.action_assign()
- move3.move_line_ids.qty_done = 190.0
- move3.action_done()
-
- self.assertEqual(move3.price_unit, 0.0) # unused in out moves
-
- self.assertEqual(move3.value, -2916.5)
- self.assertEqual(move3.cumulated_value, 153.5)
-
- # Purchase 70 units @ $16.00 per unit
- move4 = self.env['stock.move'].create({
- 'name': '70 units @ $16.00 per unit',
- 'location_id': self.supplier_location.id,
- 'location_dest_id': self.stock_location.id,
- 'product_id': self.product1.id,
- 'product_uom': self.uom_unit.id,
- 'product_uom_qty': 70.0,
- 'price_unit': 16.00,
- })
- move4.action_confirm()
- move4.action_assign()
- move4.move_line_ids.qty_done = 70.0
- move4.action_done()
-
- self.assertEqual(move4.value, 1120.0)
- self.assertEqual(move4.cumulated_value, 1273.5)
-
- # Sale 30 units @ $19.50 per unit
- move5 = self.env['stock.move'].create({
- 'name': '30 units @ $19.50 per unit',
- 'location_id': self.stock_location.id,
- 'location_dest_id': self.customer_location.id,
- 'product_id': self.product1.id,
- 'product_uom': self.uom_unit.id,
- 'product_uom_qty': 30.0,
- })
- move5.action_confirm()
- move5.action_assign()
- move5.move_line_ids.qty_done = 30.0
- move5.with_context(debug=True).action_done()
-
- self.assertEqual(move5.value, -477.6)
- self.assertEqual(move5.cumulated_value, 795.9) # fuck you, rounding
- # self.assertEqual(move5.cumulated_value, 796)
-
def test_fifo_negative_1(self):
self.product1.product_tmpl_id.cost_method = 'fifo'
move1 = self.env['stock.move'].create({
@@ -483,9 +1019,8 @@ class TestStockValuation(TransactionCase):
move1.action_done()
self.assertEqual(move1.value, 0.0)
- self.assertEqual(move1.cumulated_value, 0.0)
# normally unused in out moves, but as it moved negative stock we mark it
- self.assertEqual(move1.remaining_qty, 50.0)
+ self.assertEqual(move1.remaining_qty, -50.0)
move2 = self.env['stock.move'].create({
'name': '40 in @15',
@@ -506,12 +1041,12 @@ class TestStockValuation(TransactionCase):
move2.action_confirm()
move2.action_done()
+ move1._fifo_vacuum()
+
self.assertEqual(move1.value, -600.0)
- self.assertEqual(move1.cumulated_value, -600.0)
- self.assertEqual(move1.remaining_qty, 10.0)
+ self.assertEqual(move1.remaining_qty, -10.0)
self.assertEqual(move2.value, 600.0)
self.assertEqual(move2.remaining_qty, 0.0)
- self.assertEqual(move2.cumulated_value, 0.0)
move3 = self.env['stock.move'].create({
'name': '20 in @25',
@@ -532,15 +1067,150 @@ class TestStockValuation(TransactionCase):
move3.action_confirm()
move3.action_done()
+ move1._fifo_vacuum()
+
self.assertEqual(move1.value, -850.0)
- self.assertEqual(move1.cumulated_value, -850.0)
self.assertEqual(move1.remaining_qty, 0.0)
self.assertEqual(move2.value, 600.0)
self.assertEqual(move2.remaining_qty, 0.0)
- self.assertEqual(move2.cumulated_value, -250.0)
self.assertEqual(move3.value, 500.0)
self.assertEqual(move3.remaining_qty, 10.0)
- self.assertEqual(move3.cumulated_value, 250.0)
+
+ def test_fifo_negative_2(self):
+ """ Receives 10 units, send more, the extra quantity should be valued at the last fifo
+ price, running the vacuum should not do anything.
+ """
+ self.product1.product_tmpl_id.cost_method = 'fifo'
+
+ move1 = self.env['stock.move'].create({
+ 'name': '10 in',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 10.0,
+ })]
+ })
+ move1.action_confirm()
+ move1.action_done()
+
+ move2 = self.env['stock.move'].create({
+ 'name': '10 in',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 12.0,
+ 'price_unit': 0,
+ 'move_line_ids': [(0, 0, {
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'qty_done': 12.0,
+ })]
+ })
+ move2.action_confirm()
+ move2.action_done()
+
+ move2._fifo_vacuum()
+ self.assertEqual(len(move2.account_move_ids), 1)
+
+ def test_average_perpetual_1(self):
+ # http://accountingexplained.com/financial/inventories/avco-method
+ self.product1.product_tmpl_id.cost_method = 'average'
+
+ # Beginning Inventory: 60 units @ 15.00 per unit
+ move1 = self.env['stock.move'].create({
+ 'name': '60 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 60.0,
+ 'price_unit': 15,
+ })
+ move1.action_confirm()
+ move1.action_assign()
+ move1.move_line_ids.qty_done = 60.0
+ move1.action_done()
+
+ self.assertEqual(move1.value, 900.0)
+ self.assertEqual(move1.remaining_qty, 0.0) # unusedin average move
+
+ # Purchase 140 units @ 15.50 per unit
+ move2 = self.env['stock.move'].create({
+ 'name': '140 units @ 15.50 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 140.0,
+ 'price_unit': 15.50,
+ })
+ move2.action_confirm()
+ move2.action_assign()
+ move2.move_line_ids.qty_done = 140.0
+ move2.action_done()
+
+ self.assertEqual(move2.value, 2170.0)
+
+ # Sale 190 units @ 15.35 per unit
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 190 units @ 19.00 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 190.0,
+ })
+ move3.action_confirm()
+ move3.action_assign()
+ move3.move_line_ids.qty_done = 190.0
+ move3.action_done()
+
+
+ self.assertEqual(move3.value, -2916.5)
+
+ # Purchase 70 units @ $16.00 per unit
+ move4 = self.env['stock.move'].create({
+ 'name': '70 units @ $16.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 70.0,
+ 'price_unit': 16.00,
+ })
+ move4.action_confirm()
+ move4.action_assign()
+ move4.move_line_ids.qty_done = 70.0
+ move4.action_done()
+
+ self.assertEqual(move4.value, 1120.0)
+
+ # Sale 30 units @ $19.50 per unit
+ move5 = self.env['stock.move'].create({
+ 'name': '30 units @ $19.50 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 30.0,
+ })
+ move5.action_confirm()
+ move5.action_assign()
+ move5.move_line_ids.qty_done = 30.0
+ move5.action_done()
+
+ self.assertEqual(move5.value, -477.6)
def test_average_negative_1(self):
""" Send goods that you don't have in stock and never received any unit.
@@ -565,7 +1235,6 @@ class TestStockValuation(TransactionCase):
move1.quantity_done = 10.0
move1.action_done()
self.assertEqual(move1.value, -990.0) # as no move out were done for this product, fallback on the standard price
- self.assertEqual(move1.cumulated_value, -990.0)
self.assertEqual(move1.remaining_qty, 0.0) # unused in average move
def test_average_negative_2(self):
@@ -592,7 +1261,6 @@ class TestStockValuation(TransactionCase):
move1.action_done()
self.assertEqual(move1.value, 100.0)
- self.assertEqual(move1.cumulated_value, 100.0)
self.assertEqual(move1.remaining_qty, 0.0) # unused in average move
# send 10 products
@@ -610,7 +1278,6 @@ class TestStockValuation(TransactionCase):
move2.action_done()
self.assertEqual(move2.value, -100.0)
- self.assertEqual(move2.cumulated_value, 0.0)
self.assertEqual(move2.remaining_qty, 0.0) # unused in average move
# send 10 products again
@@ -628,5 +1295,216 @@ class TestStockValuation(TransactionCase):
move3.action_done()
self.assertEqual(move3.value, -100.0) # as no move out were done for this product, fallback on latest cost
- self.assertEqual(move3.cumulated_value, -100.0)
self.assertEqual(move3.remaining_qty, 0.0) # unused in average move
+
+ def test_average_negative_3(self):
+ """ Send goods that you don't have in stock but received and send some units before.
+ """
+ self.product1.product_tmpl_id.cost_method = 'average'
+
+ # set a standard price
+ self.product1.standard_price = 99
+
+ # Receives 10 produts at 10
+ move1 = self.env['stock.move'].create({
+ 'name': '68 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1.action_confirm()
+ move1.action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1.action_done()
+
+ self.assertEqual(move1.value, 100.0)
+ self.assertEqual(move1.remaining_qty, 0.0) # unused in average move
+
+ # send 10 products
+ move2 = self.env['stock.move'].create({
+ 'name': 'Sale 94 units @ 19.00 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move2.action_confirm()
+ move2.action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2.action_done()
+
+ self.assertEqual(move2.value, -100.0)
+ self.assertEqual(move2.remaining_qty, 0.0) # unused in average move
+
+ # send 10 products again
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 94 units @ 19.00 per unit',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ })
+ move3.action_confirm()
+ move3.force_assign()
+ move3.quantity_done = 10.0
+ move3.action_done()
+
+ self.assertEqual(move3.value, -100.0) # as no move out were done for this product, fallback on latest cost
+ self.assertEqual(move3.remaining_qty, 0.0) # unused in average move
+
+ def test_average_negative_4(self):
+ self.product1.product_tmpl_id.cost_method = 'average'
+
+ # set a standard price
+ self.product1.standard_price = 99
+
+ # Receives 10 produts at 10
+ move1 = self.env['stock.move'].create({
+ 'name': '68 units @ 15.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1.action_confirm()
+ move1.action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1.action_done()
+
+ self.assertEqual(move1.value, 100.0)
+ self.assertEqual(move1.remaining_qty, 0.0) # unused in average move
+
+ def test_average_negative_5(self):
+ self.product1.product_tmpl_id.cost_method = 'average'
+
+ # in 10 @ 10
+ move1 = self.env['stock.move'].create({
+ 'name': '10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move1.action_confirm()
+ move1.action_assign()
+ move1.move_line_ids.qty_done = 10.0
+ move1.action_done()
+
+ self.assertEqual(move1.value, 100.0)
+ self.assertEqual(self.product1.standard_price, 10)
+
+ # in 10 @ 20
+ move2 = self.env['stock.move'].create({
+ 'name': '10 units @ 20.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 20,
+ })
+ move2.action_confirm()
+ move2.action_assign()
+ move2.move_line_ids.qty_done = 10.0
+ move2.action_done()
+
+ self.assertEqual(move2.value, 200.0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ # send 5
+ move3 = self.env['stock.move'].create({
+ 'name': 'Sale 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move3.action_confirm()
+ move3.force_assign()
+ move3.quantity_done = 5.0
+ move3.action_done()
+
+ self.assertEqual(move3.value, -75.0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ # send 30
+ move4 = self.env['stock.move'].create({
+ 'name': 'Sale 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 30.0,
+ })
+ move4.action_confirm()
+ move4.force_assign()
+ move4.quantity_done = 30.0
+ move4.action_done()
+
+ self.assertEqual(move4.value, -450.0)
+ self.assertEqual(self.product1.standard_price, 15)
+
+ # in 20 @ 20
+ move5 = self.env['stock.move'].create({
+ 'name': '20 units @ 20.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 20.0,
+ 'price_unit': 20,
+ })
+ move5.action_confirm()
+ move5.action_assign()
+ move5.move_line_ids.qty_done = 20.0
+ move5.action_done()
+
+ self.assertEqual(move5.value, 400.0)
+ self.assertEqual(self.product1.standard_price, 35)
+
+ self.assertEqual(self.product1.qty_available, 5)
+
+ # send 5 products to empty the inventory, the average price should not go to 0
+ move6 = self.env['stock.move'].create({
+ 'name': 'Sale 5 units',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move6.action_confirm()
+ move6.force_assign()
+ move6.quantity_done = 5.0
+ move6.action_done()
+
+ self.assertEqual(move6.value, -175.0)
+ self.assertEqual(self.product1.standard_price, 35)
+
+ # in 10 @ 10, the new average price should be 10
+ move7 = self.env['stock.move'].create({
+ 'name': '10 units @ 10.00 per unit',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 10.0,
+ 'price_unit': 10,
+ })
+ move7.action_confirm()
+ move7.action_assign()
+ move7.move_line_ids.qty_done = 10.0
+ move7.action_done()
+
+ self.assertEqual(move7.value, 100.0)
+ self.assertEqual(self.product1.standard_price, 10)
diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml
index 654514d05c5..33c686fdba0 100644
--- a/addons/stock_account/views/product_views.xml
+++ b/addons/stock_account/views/product_views.xml
@@ -39,11 +39,8 @@
-
+