From a861b436a135ddb9d9fd74509045d61460520da8 Mon Sep 17 00:00:00 2001 From: Fatemi Lokhandwala Date: Mon, 9 Jul 2018 17:12:38 +0530 Subject: [PATCH] [ADD]:Added Upstream Patch of stock_account --- .../stock_account/data/stock_account_data.xml | 11 - .../data/stock_account_data_post_install.yml | 10 +- addons/stock_account/models/product.py | 141 ++- addons/stock_account/models/stock.py | 144 ++-- .../tests/test_stockvaluation.py | 813 +++++++++++++++++- addons/stock_account/views/product_views.xml | 84 +- .../views/stock_account_views.xml | 78 +- addons/stock_account/wizard/__init__.py | 1 + .../wizard/stock_quantity_history.py | 32 + 9 files changed, 1096 insertions(+), 218 deletions(-) create mode 100644 addons/stock_account/wizard/stock_quantity_history.py diff --git a/addons/stock_account/data/stock_account_data.xml b/addons/stock_account/data/stock_account_data.xml index 0e91543d..6dde92c8 100644 --- a/addons/stock_account/data/stock_account_data.xml +++ b/addons/stock_account/data/stock_account_data.xml @@ -26,17 +26,6 @@ manual_periodic selection - - Stock Valuation Report - ir.actions.server - code - - - -env['stock.move']._run_fifo_vacuum() -action = env.ref('stock_account.product_valuation_action').read()[0] - - diff --git a/addons/stock_account/data/stock_account_data_post_install.yml b/addons/stock_account/data/stock_account_data_post_install.yml index a2ef5882..7389eac6 100644 --- a/addons/stock_account/data/stock_account_data_post_install.yml +++ b/addons/stock_account/data/stock_account_data_post_install.yml @@ -55,10 +55,10 @@ 'fields_id': field_id, 'value': value, } - properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)]) - if properties: - # the property exist: modify it - properties.write(vals) - else: + properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)], limit=1) + if not properties: # create the property PropertyObj.create(vals) + elif not properties.value_reference: + # update the property if False + properties.write(vals) diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 38016c79..26a4fa70 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -63,7 +63,7 @@ class ProductTemplate(models.Model): if self.property_cost_method == 'fifo' and self.cost_method in ['average', 'standard']: # Cannot use the `stock_value` computed field as it's already invalidated when # entering this method. - valuation = sum([variant._sum_remaining_values() for variant in self.product_variant_ids]) + valuation = sum([variant._sum_remaining_values()[0] for variant in self.product_variant_ids]) qty_available = self.with_context(company_owned=True).qty_available if qty_available: self.standard_price = valuation / qty_available @@ -85,16 +85,7 @@ class ProductTemplate(models.Model): @api.multi def action_open_product_moves(self): - self.ensure_one() - action = self.env.ref('stock_account.stock_move_valuation_action').read()[0] - action['domain'] = [('product_tmpl_id', '=', self.id)] - action['context'] = { - 'search_default_outgoing': True, - 'search_default_incoming': True, - 'search_default_done': True, - 'is_avg': self.cost_method == 'average', - } - return action + pass @api.multi def get_product_accounts(self, fiscal_pos=None): @@ -111,6 +102,12 @@ class ProductProduct(models.Model): stock_value = fields.Float( 'Value', compute='_compute_stock_value') + qty_at_date = fields.Float( + 'Quantity', compute='_compute_stock_value') + stock_fifo_real_time_aml_ids = fields.Many2many( + 'account.move.line', compute='_compute_stock_value') + stock_fifo_manual_move_ids = fields.Many2many( + 'stock.move', compute='_compute_stock_value') @api.multi def do_change_standard_price(self, new_price, account_id): @@ -148,11 +145,13 @@ class ProductProduct(models.Model): 'account_id': debit_account_id, 'debit': abs(diff * qty_available), 'credit': 0, + 'product_id': product.id, }), (0, 0, { 'name': _('Standard Price changed - %s') % (product.display_name), 'account_id': credit_account_id, 'debit': 0, 'credit': abs(diff * qty_available), + 'product_id': product.id, })], } move = AccountMove.create(move_vals) @@ -173,29 +172,98 @@ class ProductProduct(models.Model): StockMove = self.env['stock.move'] domain = [('product_id', '=', self.id)] + StockMove._get_all_base_domain() moves = StockMove.search(domain) - return sum(moves.mapped('remaining_value')) + return sum(moves.mapped('remaining_value')), moves @api.multi - @api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'product_tmpl_id.cost_method') + @api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'stock_move_ids.remaining_value', 'product_tmpl_id.cost_method', 'product_tmpl_id.standard_price', 'product_tmpl_id.property_valuation', 'product_tmpl_id.categ_id.property_valuation') def _compute_stock_value(self): + StockMove = self.env['stock.move'] + to_date = self.env.context.get('to_date') + + self.env['account.move.line'].check_access_rights('read') + fifo_automated_values = {} + query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id) + FROM account_move_line AS aml + WHERE aml.product_id IS NOT NULL AND aml.company_id=%%s %s + GROUP BY aml.product_id, aml.account_id""" + params = (self.env.user.company_id.id,) + if to_date: + query = query % ('AND aml.date <= %s',) + params = params + (to_date,) + else: + query = query % ('',) + self.env.cr.execute(query, params=params) + + res = self.env.cr.fetchall() + for row in res: + fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4])) + for product in self: if product.cost_method in ['standard', 'average']: - product.stock_value = product.standard_price * product.with_context(company_owned=True).qty_available + qty_available = product.with_context(company_owned=True, owner_id=False).qty_available + price_used = product.standard_price + if to_date: + price_used = product.get_history_price( + self.env.user.company_id.id, + date=to_date, + ) + product.stock_value = price_used * qty_available + product.qty_at_date = qty_available elif product.cost_method == 'fifo': - product.stock_value = product._sum_remaining_values() + if to_date: + if product.product_tmpl_id.valuation == 'manual_periodic': + domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain() + moves = StockMove.search(domain) + product.stock_value = sum(moves.mapped('value')) + product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available + product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids) + elif product.product_tmpl_id.valuation == 'real_time': + valuation_account_id = product.categ_id.property_stock_valuation_account_id.id + value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, []) + product.stock_value = value + product.qty_at_date = quantity + product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids) + else: + product.stock_value, moves = product._sum_remaining_values() + product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available + if product.product_tmpl_id.valuation == 'manual_periodic': + product.stock_fifo_manual_move_ids = moves + elif product.product_tmpl_id.valuation == 'real_time': + valuation_account_id = product.categ_id.property_stock_valuation_account_id.id + value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, []) + product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids) + + def action_valuation_at_date_details(self): + """ Returns an action with either a list view of all the valued stock moves of `self` if the + valuation is set as manual or a list view of all the account move lines if the valuation is + set as automated. + """ + self.ensure_one() + to_date = self.env.context.get('to_date') + action = { + 'name': _('Valuation at date'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'tree,form', + 'context': self.env.context, + } + if self.valuation == 'real_time': + action['res_model'] = 'account.move.line' + action['domain'] = [('id', 'in', self.with_context(to_date=to_date).stock_fifo_real_time_aml_ids.ids)] + tree_view_ref = self.env.ref('stock_account.view_stock_account_aml') + form_view_ref = self.env.ref('account.view_move_line_form') + action['views'] = [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')] + else: + action['res_model'] = 'stock.move' + action['domain'] = [('id', 'in', self.with_context(to_date=to_date).stock_fifo_manual_move_ids.ids)] + tree_view_ref = self.env.ref('stock_account.view_move_tree_valuation_at_date') + form_view_ref = self.env.ref('stock.view_move_form') + action['views'] = [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')] + return action @api.multi def action_open_product_moves(self): - self.ensure_one() - action = self.env.ref('stock_account.stock_move_valuation_action').read()[0] - action['domain'] = [('product_id', '=', self.id)] - action['context'] = { - 'search_default_outgoing': True, - 'search_default_incoming': True, - 'search_default_done': True, - 'is_avg': self.cost_method == 'average', - } - return action + pass @api.model def _anglo_saxon_sale_move_lines(self, name, product, uom, qty, price_unit, currency=False, amount_currency=False, fiscal_position=False, account_analytic=False, analytic_tags=False): @@ -260,6 +328,28 @@ class ProductProduct(models.Model): return price or 0.0 return self.uom_id._compute_price(price, uom) + def _compute_average_price(self, qty_done, quantity, moves): + average_price_unit = 0 + qty_delivered = 0 + invoiced_qty = 0 + for move in moves: + if move.state != 'done': + continue + invoiced_qty += move.product_qty + if invoiced_qty <= qty_done: + continue + qty_to_consider = move.product_qty + if invoiced_qty - move.product_qty < qty_done: + qty_to_consider = invoiced_qty - qty_done + qty_to_consider = min(qty_to_consider, quantity - qty_delivered) + qty_delivered += qty_to_consider + # `move.price_unit` is negative if the move is out and positive if the move is + # dropshipped. Use its absolute value to compute the average price unit. + average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered + if qty_delivered == quantity: + break + return average_price_unit + class ProductCategory(models.Model): _inherit = 'product.category' @@ -311,4 +401,3 @@ class ProductCategory(models.Model): 'message': _("Changing your cost method is an important change that will impact your inventory valuation. Are you sure you want to make that change?"), } } - diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 374c94a0..05a38253 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -71,37 +71,45 @@ class StockMoveLine(models.Model): if move.product_id.valuation == 'real_time' and (move._is_in() or move._is_out()): move.with_context(force_valuation_amount=correction_value)._account_entry_move() return res - + @api.multi def write(self, vals): + """ When editing a done stock.move.line, we impact the valuation. Users may increase or + decrease the `qty_done` field. There are three cost method available: standard, average + and fifo. We implement the logic in a similar way for standard and average: increase + or decrease the original value with the standard or average price of today. In fifo, we + have a different logic wheter the move is incoming or outgoing. If the move is incoming, we + update the value and remaining_value/qty with the unit price of the move. If the move is + outgoing and the user increases qty_done, we call _run_fifo and it'll consume layer(s) in + the stack the same way a new outgoing move would have done. If the move is outoing and the + user decreases qty_done, we either increase the last receipt candidate if one is found or + we decrease the value with the last fifo price. + """ if 'qty_done' in vals: - # 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' and (ml.move_id._is_in() or ml.move_id._is_out())): moves_to_update[move_line.move_id] = vals['qty_done'] - move_line.qty_done for move_id, qty_difference in moves_to_update.items(): - # more/less units are available, update `remaining_value` and - # `remaining_qty` on the linked stock move. - move_vals = {'remaining_qty': move_id.remaining_qty + qty_difference} - new_remaining_value = 0 + move_vals = {} if move_id.product_id.cost_method in ['standard', 'average']: correction_value = qty_difference * move_id.product_id.standard_price - move_vals['value'] = move_id.value - correction_value - move_vals.pop('remaining_qty') + if move_id._is_in(): + move_vals['value'] = move_id.value + correction_value + elif move_id._is_out(): + move_vals['value'] = move_id.value - correction_value else: - # FIFO handling if move_id._is_in(): correction_value = qty_difference * move_id.price_unit new_remaining_value = move_id.remaining_value + correction_value + move_vals['value'] = move_id.value + correction_value + move_vals['remaining_qty'] = move_id.remaining_qty + qty_difference + move_vals['remaining_value'] = move_id.remaining_value + correction_value elif move_id._is_out() and qty_difference > 0: - # send more, run fifo again correction_value = self.env['stock.move']._run_fifo(move_id, quantity=qty_difference) - new_remaining_value = move_id.remaining_value + correction_value - move_vals.pop('remaining_qty') + # no need to adapt `remaining_qty` and `remaining_value` as `_run_fifo` took care of it + move_vals['value'] = move_id.value - correction_value elif move_id._is_out() and qty_difference < 0: - # fake return, find the last receipt and augment its qties candidates_receipt = self.env['stock.move'].search(move_id._get_in_domain(), order='date, id desc', limit=1) if candidates_receipt: candidates_receipt.write({ @@ -111,15 +119,11 @@ class StockMoveLine(models.Model): correction_value = qty_difference * candidates_receipt.price_unit else: correction_value = qty_difference * move_id.product_id.standard_price - move_vals.pop('remaining_qty') - if move_id._is_out(): - move_vals['remaining_value'] = new_remaining_value if new_remaining_value < 0 else 0 - else: - move_vals['remaining_value'] = new_remaining_value + move_vals['value'] = move_id.value - correction_value move_id.write(move_vals) if move_id.product_id.valuation == 'real_time': - move_id.with_context(force_valuation_amount=correction_value)._account_entry_move() + move_id.with_context(force_valuation_amount=correction_value, forced_quantity=qty_difference)._account_entry_move() if qty_difference > 0: move_id.product_price_update_before_done(forced_qty=qty_difference) return super(StockMoveLine, self).write(vals) @@ -210,13 +214,23 @@ class StockMove(models.Model): @api.model def _run_fifo(self, move, quantity=None): + """ Value `move` according to the FIFO rule, meaning we consume the + oldest receipt first. Candidates receipts are marked consumed or free + thanks to their `remaining_qty` and `remaining_value` fields. + By definition, `move` should be an outgoing stock move. + + :param quantity: quantity to value instead of `move.product_qty` + :returns: valued amount in absolute + """ move.ensure_one() - # Find back incoming stock moves (called candidates here) to value this move. + + # Deal with possible move lines that do not impact the valuation. valued_move_lines = move.move_line_ids.filtered(lambda ml: ml.location_id._should_be_valued() and not ml.location_dest_id._should_be_valued() and not ml.owner_id) valued_quantity = 0 for valued_move_line in valued_move_lines: valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, move.product_id.uom_id) + # Find back incoming stock moves (called candidates here) to value this move. qty_to_take_on_candidates = quantity or valued_quantity candidates = move.product_id._get_fifo_candidates_in_move() new_standard_price = 0 @@ -246,7 +260,7 @@ class StockMove(models.Model): # Update the standard price with the price of the last used candidate, if any. if new_standard_price and move.product_id.cost_method == 'fifo': - move.product_id.standard_price = new_standard_price + move.product_id.sudo().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 @@ -259,11 +273,12 @@ class StockMove(models.Model): elif qty_to_take_on_candidates > 0: last_fifo_price = new_standard_price or move.product_id.standard_price negative_stock_value = last_fifo_price * -qty_to_take_on_candidates + tmp_value += abs(negative_stock_value) vals = { 'remaining_qty': move.remaining_qty + -qty_to_take_on_candidates, 'remaining_value': move.remaining_value + negative_stock_value, - 'value': -tmp_value + negative_stock_value, - 'price_unit': (-tmp_value + negative_stock_value) / (move.product_qty or quantity), + 'value': -tmp_value, + 'price_unit': -1 * last_fifo_price, } move.write(vals) return tmp_value @@ -297,7 +312,9 @@ class StockMove(models.Model): self.write(vals) elif self._is_out(): valued_move_lines = self.move_line_ids.filtered(lambda ml: ml.location_id._should_be_valued() and not ml.location_dest_id._should_be_valued() and not ml.owner_id) - valued_quantity = sum(valued_move_lines.mapped('qty_done')) + valued_quantity = 0 + for valued_move_line in valued_move_lines: + valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, self.product_id.uom_id) self.env['stock.move']._run_fifo(self, quantity=quantity) if self.product_id.cost_method in ['standard', 'average']: curr_rounding = self.company_id.currency_id.rounding @@ -355,6 +372,7 @@ class StockMove(models.Model): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id] rounding = move.product_id.uom_id.rounding + qty_done = 0.0 if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding): @@ -362,10 +380,11 @@ class StockMove(models.Model): 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 - qty = forced_qty or move.product_qty - new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + move.product_qty) + qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id) + qty = forced_qty or qty_done + new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done) - tmpl_dict[move.product_id.id] += move.product_qty + tmpl_dict[move.product_id.id] += qty_done # 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 @@ -388,12 +407,14 @@ class StockMove(models.Model): if not candidates: continue qty_to_take_on_candidates = abs(move.remaining_qty) + qty_taken_on_candidates = 0 tmp_value = 0 for candidate in candidates: if candidate.remaining_qty <= qty_to_take_on_candidates: qty_taken_on_candidate = candidate.remaining_qty else: qty_taken_on_candidate = qty_to_take_on_candidates + qty_taken_on_candidates += qty_taken_on_candidate value_taken_on_candidate = qty_taken_on_candidate * candidate.price_unit candidate_vals = { @@ -407,27 +428,22 @@ class StockMove(models.Model): if qty_to_take_on_candidates == 0: break - remaining_value_before_vacuum = move.remaining_value - # If `remaining_qty` should be updated to 0, we wipe `remaining_value`. If it was set - # it was only used to infer the correction entry anyway. - new_remaining_qty = -qty_to_take_on_candidates - new_remaining_value = 0 if not new_remaining_qty else move.remaining_value + tmp_value + # When working with `price_unit`, beware that out move are negative. + move_price_unit = move.price_unit if move._is_out() else -1 * move.price_unit + # Get the estimated value we will correct. + remaining_value_before_vacuum = qty_taken_on_candidates * move_price_unit + new_remaining_qty = move.remaining_qty + qty_taken_on_candidates + new_remaining_value = new_remaining_qty * abs(move.price_unit) + + corrected_value = remaining_value_before_vacuum + tmp_value move.write({ 'remaining_value': new_remaining_value, 'remaining_qty': new_remaining_qty, + 'value': move.value - corrected_value, }) if move.product_id.valuation == 'real_time': - # If `move.remaining_value` is negative, it means that we initially valued this move at - # an estimated price *and* posted an entry. `tmp_value` is the real value we took to - # compensate and should always be positive, but if the remaining value is still negative - # we have to take care to not overvalue by decreasing the correction entry by what's - # already been posted. - corrected_value = tmp_value - if remaining_value_before_vacuum < 0: - corrected_value += remaining_value_before_vacuum - # If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We # force the amount in the context, but in the case it is 0 it'll create an entry # for the entire cost of the move. This case happens when the candidates moves @@ -441,9 +457,9 @@ class StockMove(models.Model): # 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() + move.with_context(force_valuation_amount=-corrected_value, forced_quantity=0)._account_entry_move() else: - move.with_context(force_valuation_amount=corrected_value)._account_entry_move() + move.with_context(force_valuation_amount=corrected_value, forced_quantity=0)._account_entry_move() @api.model def _run_fifo_vacuum(self): @@ -502,6 +518,11 @@ class StockMove(models.Model): else: valuation_amount = cost + if self._context.get('forced_ref'): + ref = self._context['forced_ref'] + else: + ref = self.picking_id.name + # the standard_price of the product may be in another decimal precision, or not compatible with the coinage of # the company currency... so we need to use round() before creating the accounting entries. debit_value = self.company_id.currency_id.round(valuation_amount) @@ -511,24 +532,13 @@ class StockMove(models.Model): raise UserError(_("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation.") % (self.product_id.name,)) credit_value = debit_value - if self.product_id.cost_method == 'average' and self.company_id.anglo_saxon_accounting: - # in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input - # account books the real purchase price, while the stock account books the average price. The difference is - # booked in the dedicated price difference account. - if self.location_dest_id.usage == 'supplier' and self.origin_returned_move_id and self.origin_returned_move_id.purchase_line_id: - debit_value = self.origin_returned_move_id.price_unit * qty - # in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation - # is made using the original average price to negate the delivery effect. - if self.location_id.usage == 'customer' and self.origin_returned_move_id: - debit_value = self.origin_returned_move_id.price_unit * qty - credit_value = debit_value partner_id = (self.picking_id.partner_id and self.env['res.partner']._find_accounting_partner(self.picking_id.partner_id).id) or False debit_line_vals = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, - 'ref': self.picking_id.name, + 'ref': ref, 'partner_id': partner_id, 'debit': debit_value if debit_value > 0 else 0, 'credit': -debit_value if debit_value < 0 else 0, @@ -539,7 +549,7 @@ class StockMove(models.Model): 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, - 'ref': self.picking_id.name, + 'ref': ref, 'partner_id': partner_id, 'credit': credit_value if credit_value > 0 else 0, 'debit': -credit_value if credit_value < 0 else 0, @@ -559,7 +569,7 @@ class StockMove(models.Model): 'product_id': self.product_id.id, 'quantity': qty, 'product_uom_id': self.product_id.uom_id.id, - 'ref': self.picking_id.name, + 'ref': ref, 'partner_id': partner_id, 'credit': diff_amount > 0 and diff_amount or 0, 'debit': diff_amount < 0 and -diff_amount or 0, @@ -571,14 +581,26 @@ class StockMove(models.Model): def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id): self.ensure_one() AccountMove = self.env['account.move'] - move_lines = self._prepare_account_move_line(self.product_qty, abs(self.value), credit_account_id, debit_account_id) + quantity = self.env.context.get('forced_quantity', self.product_qty) + quantity = quantity if self._is_in() else -1 * quantity + + # Make an informative `ref` on the created account move to differentiate between classic + # movements, vacuum and edition of past moves. + ref = self.picking_id.name + if self.env.context.get('force_valuation_amount'): + if self.env.context.get('forced_quantity') == 0: + ref = 'Revaluation of %s (negative inventory)' % ref + elif self.env.context.get('forced_quantity') is not None: + ref = 'Correction of %s (modification of past move)' % ref + + move_lines = self.with_context(forced_ref=ref)._prepare_account_move_line(quantity, abs(self.value), credit_account_id, debit_account_id) if move_lines: date = self._context.get('force_period_date', fields.Date.context_today(self)) - new_account_move = AccountMove.create({ + new_account_move = AccountMove.sudo().create({ 'journal_id': journal_id, 'line_ids': move_lines, 'date': date, - 'ref': self.picking_id.name, + 'ref': ref, 'stock_move_id': self.id, }) new_account_move.post() diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index 74948ac9..d90b9344 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. +from datetime import timedelta + from flectra.exceptions import UserError +from flectra.fields import Date from flectra.tests.common import TransactionCase @@ -115,6 +118,9 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 1) self.assertEqual(move1_valuation_aml.debit, 100) self.assertEqual(move1_valuation_aml.credit, 0) + self.assertEqual(move1_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move1_valuation_aml.quantity, 10) + self.assertEqual(move1_valuation_aml.product_uom_id.id, self.uom_unit.id) output_aml = self._get_stock_output_move_lines() self.assertEqual(len(output_aml), 0) @@ -159,6 +165,9 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 2) self.assertEqual(move2_valuation_aml.debit, 80) self.assertEqual(move2_valuation_aml.credit, 0) + self.assertEqual(move2_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move2_valuation_aml.quantity, 10) + self.assertEqual(move2_valuation_aml.product_uom_id.id, self.uom_unit.id) output_aml = self._get_stock_output_move_lines() self.assertEqual(len(output_aml), 0) @@ -207,6 +216,9 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 3) self.assertEqual(move3_valuation_aml.debit, 0) self.assertEqual(move3_valuation_aml.credit, 30) + self.assertEqual(move3_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move3_valuation_aml.quantity, -3) + self.assertEqual(move3_valuation_aml.product_uom_id.id, self.uom_unit.id) output_aml = self._get_stock_output_move_lines() move3_output_aml = output_aml[-1] @@ -243,7 +255,7 @@ class TestStockValuation(TransactionCase): 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, 100.0) + self.assertEqual(move1.value, 120.0) # move 1 is now 10@10 + 2@10 self.assertEqual(move1.remaining_value, 90.0) # account values for move1 @@ -258,6 +270,9 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 4) self.assertEqual(move1_correction_valuation_aml.debit, 20) self.assertEqual(move1_correction_valuation_aml.credit, 0) + self.assertEqual(move1_correction_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move1_correction_valuation_aml.quantity, 2) + self.assertEqual(move1_correction_valuation_aml.product_uom_id.id, self.uom_unit.id) output_aml = self._get_stock_output_move_lines() self.assertEqual(len(output_aml), 1) @@ -311,6 +326,9 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 5) self.assertEqual(move4_valuation_aml.debit, 0) self.assertEqual(move4_valuation_aml.credit, 90) + self.assertEqual(move4_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move4_valuation_aml.quantity, -9) + self.assertEqual(move4_valuation_aml.product_uom_id.id, self.uom_unit.id) output_aml = self._get_stock_output_move_lines() move4_output_aml = output_aml[-1] @@ -326,7 +344,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(move1.product_uom_qty, 12) self.assertEqual(move1.price_unit, 10.0) self.assertEqual(move1.remaining_qty, 0) - self.assertEqual(move1.value, 100) + 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) @@ -373,6 +391,9 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 6) self.assertEqual(move5_valuation_aml.debit, 0) self.assertEqual(move5_valuation_aml.credit, 160) + self.assertEqual(move5_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move5_valuation_aml.quantity, -20) + self.assertEqual(move5_valuation_aml.product_uom_id.id, self.uom_unit.id) output_aml = self._get_stock_output_move_lines() move5_output_aml = output_aml[-1] @@ -388,7 +409,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(move1.product_uom_qty, 12) self.assertEqual(move1.price_unit, 10.0) self.assertEqual(move1.remaining_qty, 0) - self.assertEqual(move1.value, 100) + 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) @@ -445,6 +466,9 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 7) self.assertEqual(move6_valuation_aml.debit, 120) self.assertEqual(move6_valuation_aml.credit, 0) + self.assertEqual(move6_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move6_valuation_aml.quantity, 10) + self.assertEqual(move6_valuation_aml.product_uom_id.id, self.uom_unit.id) output_aml = self._get_stock_output_move_lines() self.assertEqual(len(output_aml), 3) @@ -457,7 +481,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(move1.product_uom_qty, 12) self.assertEqual(move1.price_unit, 10.0) self.assertEqual(move1.remaining_qty, 0) - self.assertEqual(move1.value, 100) + 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) @@ -495,6 +519,9 @@ class TestStockValuation(TransactionCase): vacuum_valuation_aml = valuation_aml[-1] self.assertEqual(len(valuation_aml), 8) self.assertEqual(vacuum_valuation_aml.balance, -40) + self.assertEqual(vacuum_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(vacuum_valuation_aml.quantity, 0) + self.assertEqual(vacuum_valuation_aml.product_uom_id.id, self.uom_unit.id) output_aml = self._get_stock_output_move_lines() vacuum_output_aml = output_aml[-1] @@ -505,7 +532,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(move1.product_uom_qty, 12) self.assertEqual(move1.price_unit, 10.0) self.assertEqual(move1.remaining_qty, 0) - self.assertEqual(move1.value, 100) + 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) @@ -522,7 +549,11 @@ class TestStockValuation(TransactionCase): 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, -160.0) + + # move5 sent 10@8 and 10@estimated price of 8 + # the vacuum compensated the 10@8 by 12@10 + # -(10*8 + 10@12) = -200 + 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) @@ -553,7 +584,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(move6.product_uom_qty, 8) self.assertEqual(move6.price_unit, 12) self.assertEqual(move6.remaining_qty, -2) - self.assertEqual(move6.value, 120) + self.assertEqual(move6.value, 96) # move6 is now 8@12 self.assertEqual(move6.remaining_value, -24) # account values for move1 @@ -566,6 +597,9 @@ class TestStockValuation(TransactionCase): move6_correction_valuation_aml = valuation_aml[-1] self.assertEqual(move6_correction_valuation_aml.debit, 0) self.assertEqual(move6_correction_valuation_aml.credit, 24) + self.assertEqual(move6_correction_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move6_correction_valuation_aml.quantity, -2) + self.assertEqual(move6_correction_valuation_aml.product_uom_id.id, self.uom_unit.id) # link between stock move and account move self.assertEqual(len(move6.account_move_ids), 2) @@ -607,6 +641,9 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 10) self.assertEqual(move7_valuation_aml.debit, 60) self.assertEqual(move7_valuation_aml.credit, 0) + self.assertEqual(move7_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move7_valuation_aml.quantity, 4) + self.assertEqual(move7_valuation_aml.product_uom_id.id, self.uom_unit.id) # link between stock move and account move self.assertEqual(len(move7.account_move_ids), 1) @@ -629,12 +666,15 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(valuation_aml), 11) self.assertEqual(move6_correction2_valuation_aml.debit, 0) self.assertEqual(move6_correction2_valuation_aml.credit, 6) + self.assertEqual(move6_correction2_valuation_aml.product_id.id, self.product1.id) + self.assertEqual(move6_correction2_valuation_aml.quantity, 0) + self.assertEqual(move6_correction_valuation_aml.product_uom_id.id, self.uom_unit.id) # 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, 100) + 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) @@ -651,12 +691,12 @@ class TestStockValuation(TransactionCase): 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, -160.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, 120) + self.assertEqual(move6.value, 90) self.assertEqual(move6.remaining_value, 0) self.assertEqual(move7.product_uom_qty, 4.0) self.assertEqual(move7.price_unit, 15.0) @@ -681,14 +721,28 @@ class TestStockValuation(TransactionCase): # --------------------------------------------------------------------- # Ending # --------------------------------------------------------------------- + # check on remaining_qty self.assertEqual(self.product1.qty_available, 2) + # check on remaining_value self.assertEqual(self.product1.stock_value, 30) + # check on accounting entries self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 30) self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 380) self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 380) self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 350) self.assertEqual(sum(self._get_stock_output_move_lines().mapped('debit')), 320) self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0) + moves = move1 + move2 + move3 + move4 + move5 + move6 + move7 + # check on value + self.assertEqual(sum(moves.mapped('value')), 30) + # check on product_qty + qty = 0 + for move in moves: + if move._is_in(): + qty += move.product_qty + else: + qty -= move.product_qty + self.assertEqual(qty, 2) def test_fifo_perpetual_2(self): # http://accountingexplained.com/financial/inventories/fifo-method @@ -1224,8 +1278,8 @@ class TestStockValuation(TransactionCase): self.env['stock.move']._run_fifo_vacuum() # stock values for move1 and move2 - self.assertEqual(move1.value, -400.0) - self.assertEqual(move1.remaining_value, 200.0) + self.assertEqual(move1.value, -680.0) # 40@15 + 10@8 + self.assertEqual(move1.remaining_value, -80.0) self.assertEqual(move1.remaining_qty, -10.0) self.assertEqual(move2.value, 600.0) self.assertEqual(move2.remaining_value, 0.0) @@ -1235,14 +1289,13 @@ class TestStockValuation(TransactionCase): valuation_aml = self._get_stock_valuation_move_lines() vacuum1_valuation_aml = valuation_aml[-1] self.assertEqual(vacuum1_valuation_aml.debit, 0) - # 200 was credited more in valuation (we initially credited 400 but the vacuum realized - # that 600 was the right value) - self.assertEqual(vacuum1_valuation_aml.credit, 200) + # 280 was credited more in valuation (we compensated 40 items here, so initially 40 were + # valued at 8 -> 320 in credit but now we actually sent 40@15 = 600, so the difference is + # 280 more credited) + self.assertEqual(vacuum1_valuation_aml.credit, 280) output_aml = self._get_stock_output_move_lines() vacuum1_output_aml = output_aml[-1] - # 200 was debited more in output (we initially debited 400 but the vacuum realized - # that 600 was the right value) - self.assertEqual(vacuum1_output_aml.debit, 200) + self.assertEqual(vacuum1_output_aml.debit, 280) self.assertEqual(vacuum1_output_aml.credit, 0) self.assertTrue(set(move1.mapped('account_move_ids.line_ids').ids) == {move1_valuation_aml.id, move1_output_aml.id, vacuum1_valuation_aml.id, vacuum1_output_aml.id}) @@ -1290,7 +1343,7 @@ class TestStockValuation(TransactionCase): self.env['stock.move']._run_fifo_vacuum() # stock values for move1-3 - self.assertEqual(move1.value, -400.0) + self.assertEqual(move1.value, -850.0) # 40@15 + 10@25 self.assertEqual(move1.remaining_value, 0.0) self.assertEqual(move1.remaining_qty, 0.0) self.assertEqual(move2.value, 600.0) @@ -1304,14 +1357,11 @@ class TestStockValuation(TransactionCase): valuation_aml = self._get_stock_valuation_move_lines() vacuum2_valuation_aml = valuation_aml[-1] self.assertEqual(vacuum2_valuation_aml.debit, 0) - # 250 was credited more in valuation (we initially credited 400+200 but the vacuum realized - # that 850 was the right value) - self.assertEqual(vacuum2_valuation_aml.credit, 250) + # there is still 10@8 to compensate with 10@25 -> 170 to credit more in the valuation account + self.assertEqual(vacuum2_valuation_aml.credit, 170) output_aml = self._get_stock_output_move_lines() vacuum2_output_aml = output_aml[-1] - # 250 was debited more in output (we initially debited 400+200 but the vacuum realized - # that 850 was the right value) - self.assertEqual(vacuum2_output_aml.debit, 250) + self.assertEqual(vacuum2_output_aml.debit, 170) self.assertEqual(vacuum2_output_aml.credit, 0) self.assertTrue(set(move1.mapped('account_move_ids.line_ids').ids) == {move1_valuation_aml.id, move1_output_aml.id, vacuum1_valuation_aml.id, vacuum1_output_aml.id, vacuum2_valuation_aml.id, vacuum2_output_aml.id}) @@ -1655,6 +1705,18 @@ class TestStockValuation(TransactionCase): self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0) def test_fifo_add_move_in_done_picking_1(self): + """ The flow is: + + product2 std price = 20 + IN01 10@10 product1 + IN01 10@20 product2 + IN01 correction 10@20 -> 11@20 (product2) + DO01 11 product2 + DO02 1 product2 + DO02 correction 1 -> 2 (negative stock) + IN03 2@30 product2 + vacuum + """ self.product1.product_tmpl_id.cost_method = 'fifo' # --------------------------------------------------------------------- @@ -1725,16 +1787,21 @@ class TestStockValuation(TransactionCase): self.assertEqual(self.product2.qty_available, 10) self.assertEqual(self.product2.stock_value, 200) + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 300) + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 0) + # --------------------------------------------------------------------- # Edit the previous stock move, receive 11 # --------------------------------------------------------------------- move2.quantity_done = 11 - self.assertEqual(move2.value, 200.0) + self.assertEqual(move2.value, 220.0) # after correction, the move should be valued at 11@20 self.assertEqual(move2.remaining_qty, 11.0) self.assertEqual(move2.price_unit, 20.0) self.assertEqual(move2.remaining_value, 220.0) + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320) + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 0) # --------------------------------------------------------------------- # Send 11 product 2 # --------------------------------------------------------------------- @@ -1768,9 +1835,13 @@ class TestStockValuation(TransactionCase): self.assertEqual(move3.remaining_qty, 0.0) self.assertEqual(move3.price_unit, -20.0) self.assertEqual(move3.remaining_value, 0.0) + self.assertEqual(self.product2.qty_available, 0) + + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320) + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 220) # --------------------------------------------------------------------- - # Add one move of product 2 + # Add one move of product 2, this'll make some negative stock. # --------------------------------------------------------------------- move4 = self.env['stock.move'].create({ 'picking_id': delivery.id, @@ -1794,12 +1865,18 @@ class TestStockValuation(TransactionCase): self.assertEqual(move4.price_unit, -20.0) self.assertEqual(move4.remaining_value, -20.0) + self.assertEqual(self.product2.qty_available, -1) + + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 320) + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 240) + # --------------------------------------------------------------------- # edit the created move, add 1 # --------------------------------------------------------------------- move4.quantity_done = 2 - self.assertEqual(move4.value, -20.0) + self.assertEqual(self.product2.qty_available, -2) + self.assertEqual(move4.value, -40.0) self.assertEqual(move4.remaining_qty, -2.0) self.assertEqual(move4.price_unit, -20.0) self.assertEqual(move4.remaining_value, -40.0) @@ -1823,7 +1900,7 @@ class TestStockValuation(TransactionCase): # --------------------------------------------------------------------- # receive 2 products 2 @ 30 # --------------------------------------------------------------------- - move1 = self.env['stock.move'].create({ + move5 = self.env['stock.move'].create({ 'picking_id': receipt.id, 'name': '10 in', 'location_id': self.supplier_location.id, @@ -1840,8 +1917,11 @@ class TestStockValuation(TransactionCase): 'qty_done': 2.0, })] }) - move1._action_confirm() - move1._action_done() + move5._action_confirm() + move5._action_done() + + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 380) + self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('credit')), 260) # --------------------------------------------------------------------- # run vacuum @@ -1858,6 +1938,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(self.product2.qty_available, 0) self.assertEqual(self.product2.stock_value, 0) self.assertEqual(move4.remaining_value, 0) + self.assertEqual(move4.value, -60) # after correction, the move is valued -(2*30) def test_fifo_add_moveline_in_done_move_1(self): self.product1.product_tmpl_id.cost_method = 'fifo' @@ -1921,7 +2002,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0) def test_fifo_edit_done_move1(self): - """ Check that incrementing the done quantity will correctly re-run a fifo lookup. + """ Increase OUT done move while quantities are available. """ self.product1.product_tmpl_id.cost_method = 'fifo' @@ -1965,6 +2046,8 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(move1.account_move_ids), 1) + self.assertAlmostEqual(self.product1.qty_available, 10.0) + self.assertAlmostEqual(self.product1.qty_at_date, 10.0) self.assertEqual(self.product1.stock_value, 100) # --------------------------------------------------------------------- @@ -2007,6 +2090,8 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(move2.account_move_ids), 1) + self.assertAlmostEqual(self.product1.qty_available, 20.0) + self.assertAlmostEqual(self.product1.qty_at_date, 20.0) self.assertEqual(self.product1.stock_value, 220) # --------------------------------------------------------------------- @@ -2051,6 +2136,8 @@ class TestStockValuation(TransactionCase): self.assertEqual(len(move3.account_move_ids), 1) + self.assertAlmostEqual(self.product1.qty_available, 12.0) + self.assertAlmostEqual(self.product1.qty_at_date, 12.0) self.assertEqual(self.product1.stock_value, 140) # --------------------------------------------------------------------- @@ -2059,6 +2146,9 @@ class TestStockValuation(TransactionCase): # --------------------------------------------------------------------- move3.quantity_done = 14 self.assertEqual(move3.product_qty, 14) + # old value: -80 -(8@10) + # new value: -148 => -(10@10 + 4@12) + self.assertEqual(move3.value, -148) # older move self.assertEqual(move1.remaining_value, 0) # before, 20 @@ -2081,6 +2171,7 @@ class TestStockValuation(TransactionCase): # Ending # --------------------------------------------------------------------- self.assertEqual(self.product1.qty_available, 6) + self.assertAlmostEqual(self.product1.qty_at_date, 6.0) self.assertEqual(self.product1.stock_value, 72) self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0) self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 220) @@ -2090,6 +2181,8 @@ class TestStockValuation(TransactionCase): self.assertEqual(sum(self._get_stock_output_move_lines().mapped('credit')), 0) def test_fifo_edit_done_move2(self): + """ Decrease, then increase OUT done move while quantities are available. + """ self.product1.product_tmpl_id.cost_method = 'fifo' # --------------------------------------------------------------------- @@ -2152,7 +2245,7 @@ class TestStockValuation(TransactionCase): # --------------------------------------------------------------------- move2.quantity_done = 8 - self.assertEqual(move2.value, -100.0) + self.assertEqual(move2.value, -80.0) # the move actually sent 8@10 self.assertEqual(move2.remaining_qty, 0.0) self.assertEqual(move2.remaining_value, 0.0) @@ -2167,7 +2260,7 @@ class TestStockValuation(TransactionCase): # --------------------------------------------------------------------- move2.with_context(debug=True).quantity_done = 10 - self.assertEqual(move2.value, -100.0) + self.assertEqual(move2.value, -100.0) # the move actually sent 10@10 self.assertEqual(move2.remaining_qty, 0.0) self.assertEqual(move2.remaining_value, 0.0) @@ -2403,6 +2496,64 @@ class TestStockValuation(TransactionCase): move2.move_line_ids.qty_done = 0 self.assertEqual(self.product1.stock_value, -187.5) + def test_average_perpetual_4(self): + self.product1.product_tmpl_id.cost_method = 'average' + + move1 = self.env['stock.move'].create({ + 'name': 'Receive 1 unit at 10', + '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': 1.0, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 1.0 + move1._action_done() + + move2 = self.env['stock.move'].create({ + 'name': 'Receive 3 units at 5', + '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': 3.0, + 'price_unit': 5, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 1.0 + move2._action_done() + + self.assertAlmostEqual(self.product1.qty_available, 2.0) + self.assertAlmostEqual(self.product1.qty_at_date, 2.0) + self.assertAlmostEqual(self.product1.standard_price, 7.5) + + def test_average_perpetual_5(self): + ''' Set owner on incoming move => no valuation ''' + self.product1.product_tmpl_id.cost_method = 'average' + + move1 = self.env['stock.move'].create({ + 'name': 'Receive 1 unit at 10', + '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': 1.0, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 1.0 + move1.move_line_ids.owner_id = self.owner1.id + move1._action_done() + + self.assertAlmostEqual(self.product1.qty_available, 1.0) + self.assertAlmostEqual(self.product1.qty_at_date, 0.0) + self.assertAlmostEqual(self.product1.stock_value, 0.0) + def test_average_negative_1(self): """ Test edit in the past. Receive 10, send 20, edit the second move to only send 10. """ @@ -2691,6 +2842,77 @@ class TestStockValuation(TransactionCase): self.assertEqual(move7.value, 100.0) self.assertEqual(self.product1.standard_price, 10) + def test_average_manual_1(self): + ''' Set owner on incoming move => no valuation ''' + self.product1.product_tmpl_id.cost_method = 'average' + self.product1.product_tmpl_id.valuation = 'manual_periodic' + + move1 = self.env['stock.move'].create({ + 'name': 'Receive 1 unit at 10', + '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': 1.0, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 1.0 + move1.move_line_ids.owner_id = self.owner1.id + move1._action_done() + + self.assertAlmostEqual(self.product1.qty_available, 1.0) + self.assertAlmostEqual(self.product1.qty_at_date, 0.0) + self.assertAlmostEqual(self.product1.stock_value, 0.0) + + def test_standard_perpetual_1(self): + ''' Set owner on incoming move => no valuation ''' + self.product1.product_tmpl_id.cost_method = 'standard' + + move1 = self.env['stock.move'].create({ + 'name': 'Receive 1 unit at 10', + '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': 1.0, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 1.0 + move1.move_line_ids.owner_id = self.owner1.id + move1._action_done() + + self.assertAlmostEqual(self.product1.qty_available, 1.0) + self.assertAlmostEqual(self.product1.qty_at_date, 0.0) + self.assertAlmostEqual(self.product1.stock_value, 0.0) + + def test_standard_manual_1(self): + ''' Set owner on incoming move => no valuation ''' + self.product1.product_tmpl_id.cost_method = 'standard' + self.product1.product_tmpl_id.valuation = 'manual_periodic' + + move1 = self.env['stock.move'].create({ + 'name': 'Receive 1 unit at 10', + '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': 1.0, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 1.0 + move1.move_line_ids.owner_id = self.owner1.id + move1._action_done() + + self.assertAlmostEqual(self.product1.qty_available, 1.0) + self.assertAlmostEqual(self.product1.qty_at_date, 0.0) + self.assertAlmostEqual(self.product1.stock_value, 0.0) + def test_change_cost_method_1(self): """ Change the cost method from FIFO to AVCO. """ @@ -2743,6 +2965,8 @@ class TestStockValuation(TransactionCase): move3.move_line_ids.qty_done = 1.0 move3._action_done() + self.assertAlmostEqual(self.product1.qty_available, 19) + self.assertAlmostEqual(self.product1.qty_at_date, 19) self.assertEqual(self.product1.stock_value, 240) # --------------------------------------------------------------------- @@ -2811,6 +3035,8 @@ class TestStockValuation(TransactionCase): move3.move_line_ids.qty_done = 1.0 move3._action_done() + self.assertAlmostEqual(self.product1.qty_available, 19) + self.assertAlmostEqual(self.product1.qty_at_date, 19) self.assertEqual(self.product1.stock_value, 240) # --------------------------------------------------------------------- @@ -2884,6 +3110,8 @@ class TestStockValuation(TransactionCase): self.assertEqual(move1.value, 10) self.assertEqual(move1.remaining_value, 10) self.assertEqual(move1.remaining_qty, 1) + self.assertAlmostEqual(self.product1.qty_available, 0.0) + self.assertAlmostEqual(self.product1.qty_at_date, 2.0) self.assertEqual(self.product1.stock_value, 10) self.assertTrue(len(move1.account_move_ids), 1) @@ -3002,3 +3230,520 @@ class TestStockValuation(TransactionCase): with self.assertRaises(UserError): move2._action_done() + def test_at_date_standard_1(self): + self.product1.product_tmpl_id.cost_method = 'standard' + + now = Date.from_string(Date.today()) + date1 = now - timedelta(days=8) + date2 = now - timedelta(days=7) + date3 = now - timedelta(days=6) + date4 = now - timedelta(days=5) + date5 = now - timedelta(days=4) + date6 = now - timedelta(days=3) + date7 = now - timedelta(days=2) + date8 = now - timedelta(days=1) + + # set the standard price to 10 + self.product1.product_tmpl_id.standard_price = 10 + self.env['product.price.history'].search([('product_id', '=', self.product1.id)], order='datetime desc, id DESC', limit=1).datetime = date1 + + # receive 10 + move1 = self.env['stock.move'].create({ + 'name': 'in 10', + '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, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 10 + move1._action_done() + move1.date = date2 + move1.account_move_ids.write({'date': date2}) + + self.assertEqual(self.product1.qty_available, 10) + self.assertAlmostEqual(self.product1.qty_at_date, 10.0) + self.assertEqual(self.product1.stock_value, 100) + + # receive 20 + move2 = self.env['stock.move'].create({ + 'name': 'in 10', + '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, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 20 + move2._action_done() + move2.date = date3 + move2.account_move_ids.write({'date': date3}) + + self.assertEqual(self.product1.qty_available, 30) + self.assertAlmostEqual(self.product1.qty_at_date, 30.0) + self.assertEqual(self.product1.stock_value, 300) + + # send 15 + move3 = self.env['stock.move'].create({ + 'name': 'out 10', + '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': 15, + }) + move3._action_confirm() + move3._action_assign() + move3.move_line_ids.qty_done = 15 + move3._action_done() + move3.date = date4 + move3.account_move_ids.write({'date': date4}) + + self.assertEqual(self.product1.qty_available, 15) + self.assertAlmostEqual(self.product1.qty_at_date, 15.0) + self.assertEqual(self.product1.stock_value, 150) + + # set the standard price to 5 + self.product1.product_tmpl_id.standard_price = 5 + self.env['product.price.history'].search([('product_id', '=', self.product1.id)], order='datetime desc, id DESC', limit=1).datetime = date5 + + self.assertEqual(self.product1.qty_available, 15) + self.assertAlmostEqual(self.product1.qty_at_date, 15.0) + self.assertEqual(self.product1.stock_value, 75) + + # send 20 + move4 = self.env['stock.move'].create({ + 'name': 'out 10', + '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, + }) + move4._action_confirm() + move4._action_assign() + move4.move_line_ids.qty_done = 20 + move4._action_done() + move4.date = date6 + move4.account_move_ids.write({'date': date6}) + + self.assertEqual(self.product1.qty_available, -5) + self.assertAlmostEqual(self.product1.qty_at_date, -5.0) + self.assertEqual(self.product1.stock_value, -25) + + # set the standard price to 7.5 + self.product1.product_tmpl_id.standard_price = 7.5 + self.env['product.price.history'].search([('product_id', '=', self.product1.id)], order='datetime desc, id DESC', limit=1).datetime = date7 + + # receive 100 + move5 = self.env['stock.move'].create({ + 'name': 'in 10', + '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': 100, + }) + move5._action_confirm() + move5._action_assign() + move5.move_line_ids.qty_done = 100 + move5._action_done() + move5.date = date8 + move5.account_move_ids.write({'date': date8}) + + self.assertEqual(self.product1.qty_available, 95) + self.assertAlmostEqual(self.product1.qty_at_date, 95.0) + self.assertEqual(self.product1.stock_value, 712.5) + + # Quantity available at date + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_available, 0) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_available, 30) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_available, 15) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_available, 15) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -5) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date7)).qty_available, -5) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date8)).qty_available, 95) + + # Valuation at date + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, 300) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, 150) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 75) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -25) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date8)).stock_value, 712.5) + + # Quantity at date + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 0.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 10.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, 30.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, 15.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 15.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -5.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date7)).qty_at_date, -5.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date8)).qty_at_date, 95.0) + + # edit the done quantity of move1, decrease it + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 10) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 10.0) + move1.quantity_done = 5 + move1.account_move_ids.write({'date': date2}) + + # the quantity at date will reflect the change directly + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_available, 5) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 5.0) + + # as when we decrease a quantity on a recreipt, we consider it as a out move with the price + # of today, the value will be decrease of 100 - (5*7.5) + self.assertEqual(move1.value, 62.5) + + # the valuatin at date will take the qty at date * the standard price at date, that's why + # it is different. + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 50) + + # edit move 4, send 15 instead of 20 + # we now have +5 + 20 - 15 -20 = -10 * a standard price of 5 + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -10.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -10.0) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -50) + move4.quantity_done = 15 + move4.account_move_ids.write({'date': date6}) + + # -(20*5) + (5*7.5) + self.assertEqual(move4.value, -62.5) + # we now have +5 + 20 - 15 -15 = -5 * a standard price of 5 + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_available, -5.0) + self.assertAlmostEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, -5.0) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, -25) + + def test_at_date_fifo_1(self): + """ Make some operations at different dates, check that the results of the valuation at + date wizard are consistent. Afterwards, edit the done quantity of some operations. The + valuation at date results should take these changes into account. + """ + self.product1.product_tmpl_id.cost_method = 'fifo' + + now = Date.from_string(Date.today()) + date1 = now - timedelta(days=8) + date2 = now - timedelta(days=7) + date3 = now - timedelta(days=6) + date4 = now - timedelta(days=5) + date5 = now - timedelta(days=4) + date6 = now - timedelta(days=3) + + # receive 10@10 + move1 = self.env['stock.move'].create({ + 'name': 'in 10', + '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, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 10 + move1._action_done() + move1.date = date1 + move1.account_move_ids.write({'date': date1}) + + self.assertEqual(self.product1.qty_available, 10) + self.assertAlmostEqual(self.product1.qty_at_date, 10.0) + self.assertEqual(self.product1.stock_value, 100) + + # receive 10@12 + move2 = self.env['stock.move'].create({ + 'name': 'in 10', + '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, + 'price_unit': 12, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 10 + move2._action_done() + move2.date = date2 + move2.account_move_ids.write({'date': date2}) + + self.assertEqual(self.product1.qty_available, 20) + self.assertAlmostEqual(self.product1.qty_at_date, 20) + self.assertEqual(self.product1.stock_value, 220) + + # send 15 + move3 = self.env['stock.move'].create({ + 'name': 'out 10', + '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': 15, + }) + move3._action_confirm() + move3._action_assign() + move3.move_line_ids.qty_done = 15 + move3._action_done() + move3.date = date3 + move3.account_move_ids.write({'date': date3}) + + self.assertEqual(self.product1.qty_available, 5) + self.assertAlmostEqual(self.product1.qty_at_date, 5.0) + self.assertEqual(self.product1.stock_value, 60) + + # send 20 + move4 = self.env['stock.move'].create({ + 'name': 'out 10', + '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, + }) + move4._action_confirm() + move4._action_assign() + move4.move_line_ids.qty_done = 20 + move4._action_done() + move4.date = date4 + move4.account_move_ids.write({'date': date4}) + + self.assertEqual(self.product1.qty_available, -15) + self.assertAlmostEqual(self.product1.qty_at_date, -15.0) + self.assertEqual(self.product1.stock_value, -180) + + # receive 100@15 + move5 = self.env['stock.move'].create({ + 'name': 'in 10', + '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': 100, + 'price_unit': 15, + }) + move5._action_confirm() + move5._action_assign() + move5.move_line_ids.qty_done = 100 + move5._action_done() + move5.date = date5 + move5.account_move_ids.write({'date': date5}) + + self.assertEqual(self.product1.qty_available, 85) + self.assertAlmostEqual(self.product1.qty_at_date, 85.0) + self.assertEqual(self.product1.stock_value, 1320) + + # run the vacuum to compensate the negative stock move + self.env['stock.move']._run_fifo_vacuum() + move4.account_move_ids[0].write({'date': date6}) + + self.assertEqual(self.product1.qty_available, 85) + self.assertAlmostEqual(self.product1.qty_at_date, 85.0) + self.assertEqual(self.product1.stock_value, 1275) + + # Edit the quantity done of move1, increase it. + move1.quantity_done = 20 + + # --------------------------------------------------------------------- + # ending: manual valuation + # --------------------------------------------------------------------- + self.product1.product_tmpl_id.valuation = 'manual_periodic' + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 20) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).stock_value, 200) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 30) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 320) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, 15) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, 160) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, -5) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, -125) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 95) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 1375) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, 95) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 1375) + self.assertEqual(self.product1.qty_at_date, 95) + self.assertEqual(self.product1.stock_value, 1375) + + # --------------------------------------------------------------------- + # ending: perpetual valuation + # --------------------------------------------------------------------- + self.product1.product_tmpl_id.valuation = 'real_time' + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 20) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 220) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, 5) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, 60) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, -15) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, -180) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 85) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 1320) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, 85) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 1275) + self.assertEqual(self.product1.qty_at_date, 95) + self.assertEqual(self.product1.stock_value, 1375) + + def test_at_date_fifo_2(self): + self.product1.product_tmpl_id.cost_method = 'fifo' + + now = Date.from_string(Date.today()) + date1 = now - timedelta(days=8) + date2 = now - timedelta(days=7) + date3 = now - timedelta(days=6) + date4 = now - timedelta(days=5) + date5 = now - timedelta(days=4) + date6 = now - timedelta(days=3) + + # receive 10@10 + move1 = self.env['stock.move'].create({ + 'name': 'in 10@10', + '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, + 'price_unit': 10, + }) + move1._action_confirm() + move1._action_assign() + move1.move_line_ids.qty_done = 10 + move1._action_done() + move1.date = date1 + move1.account_move_ids.write({'date': date1}) + + self.assertEqual(self.product1.qty_available, 10) + self.assertAlmostEqual(self.product1.qty_at_date, 10.0) + self.assertEqual(self.product1.stock_value, 100) + + # receive 10@15 + move2 = self.env['stock.move'].create({ + 'name': 'in 10@15', + '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, + 'price_unit': 15, + }) + move2._action_confirm() + move2._action_assign() + move2.move_line_ids.qty_done = 10 + move2._action_done() + move2.date = date2 + move2.account_move_ids.write({'date': date2}) + + self.assertEqual(self.product1.qty_available, 20) + self.assertAlmostEqual(self.product1.qty_at_date, 20.0) + self.assertEqual(self.product1.stock_value, 250) + + # send 30 + move3 = self.env['stock.move'].create({ + 'name': 'out 30', + '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, + }) + move3._action_confirm() + move3._action_assign() + move3.move_line_ids.qty_done = 30 + move3._action_done() + move3.date = date3 + move3.account_move_ids.write({'date': date3}) + + self.assertEqual(self.product1.qty_available, -10) + self.assertAlmostEqual(self.product1.qty_at_date, -10.0) + self.assertEqual(self.product1.stock_value, -150) + + # receive 10@20 + move4 = self.env['stock.move'].create({ + 'name': 'in 10@20', + '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, + 'price_unit': 20, + }) + move4._action_confirm() + move4._action_assign() + move4.move_line_ids.qty_done = 10 + move4._action_done() + move4.date = date4 + move4.account_move_ids.write({'date': date4}) + + self.assertEqual(self.product1.qty_available, 0) + self.assertAlmostEqual(self.product1.qty_at_date, 0.0) + self.assertEqual(self.product1.stock_value, 50) + + # receive 10@10 + move5 = self.env['stock.move'].create({ + 'name': 'in 10@10', + '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, + 'price_unit': 10, + }) + move5._action_confirm() + move5._action_assign() + move5.move_line_ids.qty_done = 10 + move5._action_done() + move5.date = date5 + move5.account_move_ids.write({'date': date5}) + + self.assertEqual(self.product1.qty_available, 10) + self.assertAlmostEqual(self.product1.qty_at_date, 10.0) + self.assertEqual(self.product1.stock_value, 150) + + # run the vacuum to compensate the negative stock move + self.env['stock.move']._run_fifo_vacuum() + move3.account_move_ids[0].write({'date': date6}) + + self.assertEqual(self.product1.qty_available, 10) + self.assertAlmostEqual(self.product1.qty_at_date, 10.0) + self.assertEqual(self.product1.stock_value, 100) + + # --------------------------------------------------------------------- + # ending: manual valuation + # --------------------------------------------------------------------- + self.product1.product_tmpl_id.valuation = 'manual_periodic' + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 20) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 250) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, -10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, -200) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, 0) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, 0) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 100) + self.assertEqual(self.product1.qty_at_date, 10) + self.assertEqual(self.product1.stock_value, 100) + + # --------------------------------------------------------------------- + # ending: perpetual valuation + # --------------------------------------------------------------------- + self.product1.product_tmpl_id.valuation = 'real_time' + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date1)).stock_value, 100) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).qty_at_date, 20) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date2)).stock_value, 250) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).qty_at_date, -10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date3)).stock_value, -150) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).qty_at_date, 0) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date4)).stock_value, 50) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date5)).stock_value, 150) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).qty_at_date, 10) + self.assertEqual(self.product1.with_context(to_date=Date.to_string(date6)).stock_value, 100) + self.assertEqual(self.product1.qty_at_date, 10) + self.assertEqual(self.product1.stock_value, 100) diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml index 5ae05e8e..aac7d49e 100644 --- a/addons/stock_account/views/product_views.xml +++ b/addons/stock_account/views/product_views.xml @@ -86,97 +86,21 @@ - - - product.valuation.tree - product.product - - - - - - - - - - + Product Valuation ir.actions.act_window product.product tree,form form - + [('type', '=', 'product'), ('qty_available', '!=', 0)] - {} + {'company_owned': True}

If there are products, you will see its name and valuation.

- - - product.product - product.product - - - -