From 9439f6aa557be4deff566462c3900910bb43bc63 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Mon, 14 Aug 2017 12:00:37 +0200 Subject: [PATCH] [REF] stock_account: move away from the replay everything strategy The approach we tried with the current implementation lead to a replay of all the next moves since a move had to be corrected, and posting an account entry with the valuation difference. This proved to be slow and complicated, and moreover the resulting account entry could be very difficult to understand and justify. So we moved away from this strategy and implemented the following points: For FIFO * If a move goes into negative (sent more than availabale, we do not counterpart directly at the next in move: we valued it at the last fifo price and only when running `_fifo_vacuum` (which is called at every openning of the valuation report), we use some units currently in stock and post an account entry with the difference between the fake value and the real one used. * When editing a done move and increasing its quantity, the new quantity is available now and will be used at the next move out. We do not try to replay the history to make the new values go away directly. * When editing a done move and decreasing its quantity, it acts as a return to the supplier at the price unit of the move. On average price: * Don't replay anything, if we fall under the negative stock usecase, value at the last average price. We restore the previous code that set the average price on the standard price and we slightly patch it to continue to update the average price if quantity in stock is negative. We also add a link between stock moves and account moves. A stat button on the stock move's form redirect directly to them. It's therefore a little easier to understand what happened. We remove the separate fifo and average price fields that were added in saas17, we keep the single standard price (that should be renamed cost_price for consistency). In the same idea, remove the cumulated value that was stored on the move. If we need it, just recompute it (it's not a frequent operation). We remove last_done_move_id, last_done_remaining_qty and last_done_qty that were added in saas17 to help in the replay method, as we just got rid of it. We also remove the code that compensate the negative stock directy at the next in. What the fifo vacuum does is essentially the same thing as what an out move do: find a candidate and take units from it. In this case, the candidates are newer than its concerned stock move. We stored on the concerned stock move the value at which we sent it (remaining_value) and post the difference with the real value on a real product. --- addons/stock/views/stock_move_views.xml | 2 + addons/stock_account/models/__init__.py | 1 + addons/stock_account/models/account_move.py | 10 + addons/stock_account/models/product.py | 54 - addons/stock_account/models/stock.py | 344 ++--- .../tests/test_stockvaluation.py | 1132 +++++++++++++++-- addons/stock_account/views/product_views.xml | 5 +- .../views/stock_account_views.xml | 11 + 8 files changed, 1211 insertions(+), 348 deletions(-) create mode 100644 addons/stock_account/models/account_move.py 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 @@
- +