diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index 6c9e5230575..fa4bcf3c348 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -231,6 +231,8 @@ +
+
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 @@
- +