diff --git a/addons/stock_account/models/__init__.py b/addons/stock_account/models/__init__.py index 9a0a869ef08..a47ce5720ee 100644 --- a/addons/stock_account/models/__init__.py +++ b/addons/stock_account/models/__init__.py @@ -5,7 +5,6 @@ from . import account_chart_template from . import account_invoice from . import account_move from . import account_reconciliation_widget -from . import procurement_group from . import product from . import stock_move from . import stock_inventory diff --git a/addons/stock_account/models/procurement_group.py b/addons/stock_account/models/procurement_group.py deleted file mode 100644 index 2372c13bd57..00000000000 --- a/addons/stock_account/models/procurement_group.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models - - -class ProcurementGroup(models.Model): - _inherit = 'procurement.group' - - @api.model - def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False): - super(ProcurementGroup, self)._run_scheduler_tasks(use_new_cursor=use_new_cursor, company_id=company_id) - self.env['stock.move']._run_fifo_vacuum() - diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 84271711164..c14e18c5672 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -32,11 +32,6 @@ class ProductTemplate(models.Model): }) return accounts - @api.multi - def action_open_product_moves(self): - # TODO: remove me in master - pass - @api.multi def get_product_accounts(self, fiscal_pos=None): """ Add the stock journal related to product to the result of super() @@ -50,16 +45,6 @@ class ProductTemplate(models.Model): class ProductProduct(models.Model): _inherit = 'product.product' - stock_value_currency_id = fields.Many2one('res.currency', compute='_compute_stock_value_currency') - 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') - value_svl = fields.Float(compute='_compute_value_svl') quantity_svl = fields.Float(compute='_compute_value_svl') stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'product_id') @@ -331,202 +316,8 @@ class ProductProduct(models.Model): vacuum_svl.stock_move_id.with_context(svl_id=vacuum_svl.id, force_valuation_amount=vacuum_svl.value, forced_quantity=vacuum_svl.quantity, forced_ref=vacuum_svl.description)._account_entry_move() # ------------------------------------------------------------------------- - # Miscellaneous + # Anglo saxon helpers # ------------------------------------------------------------------------- - @api.multi - def do_change_standard_price(self, new_price, account_id): - """ Changes the Standard Price of Product and creates an account move accordingly.""" - AccountMove = self.env['account.move'] - - quant_locs = self.env['stock.quant'].sudo().read_group([('product_id', 'in', self.ids)], ['location_id'], ['location_id']) - quant_loc_ids = [loc['location_id'][0] for loc in quant_locs] - locations = self.env['stock.location'].search([('usage', '=', 'internal'), ('company_id', '=', self.env.company.id), ('id', 'in', quant_loc_ids)]) - - product_accounts = {product.id: product.product_tmpl_id.get_product_accounts() for product in self} - - for location in locations: - for product in self.with_context(location=location.id, compute_child=False).filtered(lambda r: r.valuation == 'real_time'): - diff = product.standard_price - new_price - if float_is_zero(diff, precision_rounding=product.currency_id.rounding): - raise UserError(_("No difference between the standard price and the new price.")) - if not product_accounts[product.id].get('stock_valuation', False): - raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.')) - qty_available = product.qty_available - if qty_available: - # Accounting Entries - if diff * qty_available > 0: - debit_account_id = account_id - credit_account_id = product_accounts[product.id]['stock_valuation'].id - else: - debit_account_id = product_accounts[product.id]['stock_valuation'].id - credit_account_id = account_id - - move_vals = { - 'journal_id': product_accounts[product.id]['stock_journal'].id, - 'company_id': location.company_id.id, - 'ref': product.default_code, - 'line_ids': [(0, 0, { - 'name': _('%s changed cost from %s to %s - %s') % (self.env.user.name, product.standard_price, new_price, product.display_name), - 'account_id': debit_account_id, - 'debit': abs(diff * qty_available), - 'credit': 0, - 'product_id': product.id, - }), (0, 0, { - 'name': _('%s changed cost from %s to %s - %s') % (self.env.user.name, product.standard_price, new_price, product.display_name), - 'account_id': credit_account_id, - 'debit': 0, - 'credit': abs(diff * qty_available), - 'product_id': product.id, - })], - } - move = AccountMove.create(move_vals) - move.post() - - self.write({'standard_price': new_price}) - return True - - def _get_fifo_candidates_in_move(self): - """ Find IN moves that can be used to value OUT moves. - """ - return self._get_fifo_candidates_in_move_with_company() - - def _get_fifo_candidates_in_move_with_company(self, move_company_id=False): - self.ensure_one() - domain = [('product_id', '=', self.id), ('remaining_qty', '>', 0.0)] + self.env['stock.move']._get_in_base_domain(move_company_id) - candidates = self.env['stock.move'].search(domain, order='date, id') - return candidates - - def _sum_remaining_values(self): - 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')), moves - - @api.multi - def _compute_stock_value_currency(self): - currency_id = self.env.company.currency_id - for product in self: - product.stock_value_currency_id = currency_id - - @api.multi - @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.categ_id.property_valuation') - def _compute_stock_value(self): - StockMove = self.env['stock.move'] - to_date = self.env.context.get('to_date') - - real_time_product_ids = [product.id for product in self if product.product_tmpl_id.valuation == 'real_time'] - if real_time_product_ids: - 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 IN %%s AND aml.company_id=%%s %s - GROUP BY aml.product_id, aml.account_id""" - params = (tuple(real_time_product_ids), self.env.company.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])) - - product_values = {product.id: 0 for product in self} - product_move_ids = {product.id: [] for product in self} - - if to_date: - domain = [('product_id', 'in', self.ids), ('date', '<=', to_date)] + StockMove._get_all_base_domain() - value_field_name = 'value' - else: - domain = [('product_id', 'in', self.ids)] + StockMove._get_all_base_domain() - value_field_name = 'remaining_value' - - StockMove.check_access_rights('read') - query = StockMove._where_calc(domain) - StockMove._apply_ir_rules(query, 'read') - from_clause, where_clause, params = query.get_sql() - query_str = """ - SELECT stock_move.product_id, SUM(COALESCE(stock_move.{}, 0.0)), ARRAY_AGG(stock_move.id) - FROM {} - WHERE {} - GROUP BY stock_move.product_id - """.format(value_field_name, from_clause, where_clause) - self.env.cr.execute(query_str, params) - for product_id, value, move_ids in self.env.cr.fetchall(): - product_values[product_id] = value - product_move_ids[product_id] = move_ids - - for product in self: - if product.cost_method in ['standard', 'average']: - 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.company.id, - date=to_date, - ) - product.stock_value = price_used * qty_available - product.qty_at_date = qty_available - elif product.cost_method == 'fifo': - if to_date: - if product.product_tmpl_id.valuation == 'manual_periodic': - product.stock_value = product_values[product.id] - product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available - product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product.id]) - 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 = product_values[product.id] - 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 = StockMove.browse(product_move_ids[product.id]) - 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') - ctx = self.env.context.copy() - ctx.pop('group_by', None) - action = { - 'name': _('Valuation at date'), - 'type': 'ir.actions.act_window', - 'view_type': 'form', - 'view_mode': 'tree,form', - 'context': ctx, - } - 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): - #TODO: remove me in master - 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): """Prepare dicts describing new journal COGS journal items for a product sale. @@ -661,14 +452,6 @@ class ProductCategory(models.Model): input_and_output_accounts = category.property_stock_account_input_categ_id | category.property_stock_account_output_categ_id if valuation_account and valuation_account in input_and_output_accounts: raise ValidationError(_('The Stock Input and/or Output accounts cannot be the same than the Stock Valuation account.')) - @api.multi - def write(self, vals): - # When going from FIFO to AVCO or to standard, we update the standard price with the - # average value in stock. - cost_method = vals.get('property_cost_method') - if cost_method and cost_method in ['average', 'standard']: - self._update_standard_price() - return super(ProductCategory, self).write(vals) @api.onchange('property_cost_method') def onchange_property_valuation(self): @@ -682,11 +465,3 @@ class ProductCategory(models.Model): } } - def _update_standard_price(self): - updated_categories = self.filtered(lambda x: x.property_cost_method == 'fifo') - templates = self.env['product.template'].search([('categ_id', 'in', updated_categories.ids)]) - for t in templates: - valuation = sum([variant._sum_remaining_values()[0] for variant in t.product_variant_ids]) - qty_available = t.with_context(company_owned=True).qty_available - if qty_available: - t.standard_price = valuation / qty_available diff --git a/addons/stock_account/models/stock_move.py b/addons/stock_account/models/stock_move.py index 89c69562a36..23f3e278a0b 100644 --- a/addons/stock_account/models/stock_move.py +++ b/addons/stock_account/models/stock_move.py @@ -16,9 +16,6 @@ class StockMove(models.Model): to_refund = fields.Boolean(string="Update quantities on SO/PO", copy=False, help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order') - value = fields.Float(copy=False) - remaining_qty = fields.Float(copy=False) - remaining_value = fields.Float(copy=False) account_move_ids = fields.One2many('account.move', 'stock_move_id') stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_move_id') @@ -41,61 +38,6 @@ class StockMove(models.Model): price_unit = self.origin_returned_move_id.price_unit return not self.company_id.currency_id.is_zero(price_unit) and price_unit or self.product_id.standard_price - @api.model - def _get_in_base_domain(self, company_id=False): - # Domain: - # - state is done - # - coming from a location without company, or an inventory location within the same company - # - going to a location within the same company - domain = [ - ('state', '=', 'done'), - '&', - '|', - ('location_id.company_id', '=', False), - '&', - ('location_id.usage', 'in', ['inventory', 'production']), - ('location_id.company_id', '=', company_id or self.env.company.id), - ('location_dest_id.company_id', '=', company_id or self.env.company.id), - ] - return domain - - @api.model - def _get_all_base_domain(self, company_id=False): - # Domain: - # - state is done - # Then, either 'in' or 'out' moves. - # 'in' moves: - # - coming from a location without company, or an inventory location within the same company - # - going to a location within the same company - # 'out' moves: - # - coming from to a location within the same company - # - going to a location without company, or an inventory location within the same company - domain = [ - ('state', '=', 'done'), - '|', - '&', - '|', - ('location_id.company_id', '=', False), - '&', - ('location_id.usage', 'in', ['inventory', 'production']), - ('location_id.company_id', '=', company_id or self.env.company.id), - ('location_dest_id.company_id', '=', company_id or self.env.company.id), - '&', - ('location_id.company_id', '=', company_id or self.env.company.id), - '|', - ('location_dest_id.company_id', '=', False), - '&', - ('location_dest_id.usage', '=', 'inventory'), - ('location_dest_id.company_id', '=', company_id or self.env.company.id), - ] - return domain - - 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_all_domain(self): - return [('product_id', '=', self.product_id.id)] + self._get_all_base_domain(company_id=self.company_id.id) - @api.model def _get_valued_types(self): """Returns a list of `valued_type` as strings. During `action_done`, we'll call @@ -287,140 +229,6 @@ class StockMove(models.Model): """ return self._create_dropshipped_svl(forced_quantity=forced_quantity) - @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() - - # 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_with_company(move.company_id.id) - new_standard_price = 0 - tmp_value = 0 # to accumulate the value taken on the candidates - for candidate in candidates: - 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_on_candidates - - # As applying a landed cost do not update the unit price, naivelly doing - # something like qty_taken_on_candidate * candidate.price_unit won't make - # the additional value brought by the landed cost go away. - candidate_price_unit = candidate.remaining_value / candidate.remaining_qty - 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 - - # 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.sudo().with_context(force_company=move.company_id.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 if not quantity else move.value or -tmp_value, # outgoing move are valued negatively - 'price_unit': -tmp_value / (move.product_qty or quantity), - }) - 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, - 'price_unit': -1 * last_fifo_price, - } - move.write(vals) - return tmp_value - - def _run_valuation(self, quantity=None): - self.ensure_one() - value_to_return = 0 - if self._is_in(): - valued_move_lines = self._get_in_move_lines() - 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) - - # Note: we always compute the fifo `remaining_value` and `remaining_qty` fields no - # matter which cost method is set, to ease the switching of cost method. - vals = {} - price_unit = self._get_price_unit() - value = price_unit * (quantity or valued_quantity) - value_to_return = value if quantity is None or not self.value else self.value - vals = { - 'price_unit': price_unit, - 'value': value_to_return, - 'remaining_value': value if quantity is None else self.remaining_value + value, - } - vals['remaining_qty'] = valued_quantity if quantity is None else self.remaining_qty + quantity - - if self.product_id.cost_method == 'standard': - value = self.product_id.standard_price * (quantity or valued_quantity) - value_to_return = value if quantity is None or not self.value else self.value - vals.update({ - 'price_unit': self.product_id.standard_price, - 'value': value_to_return, - }) - self.write(vals) - elif self._is_out(): - valued_move_lines = self._get_out_move_lines() - 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 - value = -float_round(self.product_id.standard_price * (valued_quantity if quantity is None else quantity), precision_rounding=curr_rounding) - value_to_return = value if quantity is None else self.value + value - self.write({ - 'value': value_to_return, - 'price_unit': value / valued_quantity, - }) - elif self._is_dropshipped() or self._is_dropshipped_returned(): - curr_rounding = self.company_id.currency_id.rounding - if self.product_id.cost_method in ['fifo']: - price_unit = self._get_price_unit() - # see test_dropship_fifo_perpetual_anglosaxon_ordered - self.product_id.standard_price = price_unit - else: - price_unit = self.product_id.standard_price - value = float_round(self.product_qty * price_unit, precision_rounding=curr_rounding) - value_to_return = value if self._is_dropshipped() else -value - # In move have a positive value, out move have a negative value, let's arbitrary say - # dropship are positive. - self.write({ - 'value': value_to_return, - 'price_unit': price_unit if self._is_dropshipped() else -price_unit, - }) - return value_to_return - def _action_done(self, cancel_backorder=False): # Init a dict that will group the moves by valuation type, according to `move._is_valued_type`. valued_moves = {valued_type: self.env['stock.move'] for valued_type in self._get_valued_types()} @@ -504,88 +312,6 @@ class StockMove(models.Model): 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`. - """ - for move in self.filtered(lambda m: (m._is_in() or m._is_out()) and m.remaining_qty < 0): - domain = [ - ('remaining_qty', '>', 0), - '|', - ('date', '>', move.date), - '&', - ('date', '=', move.date), - ('id', '>', move.id) - ] - domain += move._get_in_domain() - candidates = self.search(domain, order='date, id') - 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 = { - '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 - - - # 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 `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 - # entirely compensate the problematic move. - if move.company_id.currency_id.is_zero(corrected_value): - continue - - 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, forced_quantity=0)._account_entry_move() - else: - move.with_context(force_valuation_amount=corrected_value, forced_quantity=0)._account_entry_move() - - @api.model - def _run_fifo_vacuum(self): - # Call `_fifo_vacuum` on concerned moves - fifo_valued_products = self.env['product.product'] - fifo_valued_categories = self.env['product.category'].search([('property_cost_method', '=', 'fifo')]) - fifo_valued_products |= self.env['product.product'].search([('categ_id', 'child_of', fifo_valued_categories.ids)]) - moves_to_vacuum = self.search( - [('product_id', 'in', fifo_valued_products.ids), ('remaining_qty', '<', 0)] + self._get_all_base_domain()) - moves_to_vacuum._fifo_vacuum() - @api.multi def _get_accounting_data_for_valuation(self): """ Return the accounts and journal to use to post Journal Entries for @@ -726,7 +452,7 @@ class StockMove(models.Model): if not ref and self.inventory_id: ref = self.inventory_id.name - move_lines = self.with_context(forced_ref=ref)._prepare_account_move_line(quantity, abs(self.value), credit_account_id, debit_account_id) + move_lines = self.with_context(forced_ref=ref)._prepare_account_move_line(quantity, 0, 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.sudo().create({ diff --git a/addons/stock_account/models/stock_move_line.py b/addons/stock_account/models/stock_move_line.py index 4cdee59c77b..390d0276017 100644 --- a/addons/stock_account/models/stock_move_line.py +++ b/addons/stock_account/models/stock_move_line.py @@ -14,39 +14,19 @@ class StockMoveLine(models.Model): @api.model_create_multi def create(self, vals_list): move_lines = super(StockMoveLine, self).create(vals_list) - if self.env.context.get('svl'): - for move_line in move_lines: - if move_line.state != 'done': - continue - move = move_line.move_id - rounding = move.product_id.uom_id.rounding - diff = move_line.qty_done - if float_is_zero(diff, precision_rounding=rounding): - continue - self._create_correction_svl(move, diff) - return move_lines - -# for line in move_lines: -# move = line.move_id -# if move.state == 'done': -# correction_value = move._run_valuation(line.qty_done) -# 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() + for move_line in move_lines: + if move_line.state != 'done': + continue + move = move_line.move_id + rounding = move.product_id.uom_id.rounding + diff = move_line.qty_done + if float_is_zero(diff, precision_rounding=rounding): + continue + self._create_correction_svl(move, diff) return move_lines 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 and self.env.context.get('svl'): + if 'qty_done' in vals: for move_line in self: if move_line.state != 'done': continue @@ -56,47 +36,6 @@ class StockMoveLine(models.Model): if float_is_zero(diff, precision_rounding=rounding): continue self._create_correction_svl(move, diff) - elif 'qty_done' in vals: - 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(): - move_vals = {} - if move_id.product_id.cost_method in ['standard', 'average']: - correction_value = qty_difference * move_id.product_id.standard_price - 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: - 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: - correction_value = self.env['stock.move']._run_fifo(move_id, quantity=qty_difference) - # 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: - candidates_receipt = self.env['stock.move'].search(move_id._get_in_domain(), order='date, id desc', limit=1) - if candidates_receipt: - candidates_receipt.write({ - 'remaining_qty': candidates_receipt.remaining_qty + -qty_difference, - 'remaining_value': candidates_receipt.remaining_value + (-qty_difference * candidates_receipt.price_unit), - }) - correction_value = qty_difference * candidates_receipt.price_unit - else: - correction_value = qty_difference * move_id.product_id.standard_price - 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, 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) # ------------------------------------------------------------------------- diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index 66a695fc956..74f340b9706 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -243,7 +243,7 @@ class TestStockValuation(SavepointCase): # Increase received quantity of move1 from 10 to 12, it should create # a new stock layer at the top of the queue. # --------------------------------------------------------------------- - move1.with_context(svl=True).quantity_done = 12 + move1.quantity_done = 12 # stock_account values for move3 self.assertEqual(move1.stock_valuation_layer_ids[-1].unit_cost, 10.0) @@ -391,7 +391,7 @@ class TestStockValuation(SavepointCase): # --------------------------------------------------------------------- # Edit move6, receive less: 2 in negative stock # --------------------------------------------------------------------- - move6.with_context(svl=True).quantity_done = 8 + move6.quantity_done = 8 # stock_account values for move6 self.assertEqual(move6.stock_valuation_layer_ids[-1].remaining_qty, -2) @@ -1195,8 +1195,8 @@ class TestStockValuation(SavepointCase): self.assertEqual(len(move3.account_move_ids), 1) # the created account move is due to the receipt # nothing should have changed in the accounting regarding the output - self.assertEqual(self.product1.qty_available, 0) - self.assertEqual(self.product1.stock_value, 0) + self.assertEqual(self.product1.quantity_svl, 0) + self.assertEqual(self.product1.value_svl, 0) self.assertEqual(sum(self._get_stock_input_move_lines().mapped('debit')), 0) self.assertEqual(sum(self._get_stock_input_move_lines().mapped('credit')), 120) self.assertEqual(sum(self._get_stock_valuation_move_lines().mapped('debit')), 120) @@ -1419,7 +1419,6 @@ class TestStockValuation(SavepointCase): 'product_id': self.product2.id, 'product_uom': self.uom_unit.id, 'product_uom_qty': 10.0, - 'state': 'done', # simulate default_get override 'move_line_ids': [(0, 0, { 'product_id': self.product2.id, 'location_id': self.supplier_location.id, @@ -1445,7 +1444,7 @@ class TestStockValuation(SavepointCase): # --------------------------------------------------------------------- # Edit the previous stock move, receive 11 # --------------------------------------------------------------------- - move2.with_context(svl=True).quantity_done = 11 + move2.quantity_done = 11 self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), 220.0) # after correction, the move should be valued at 11@20 self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('remaining_qty')), 11.0) @@ -1628,7 +1627,7 @@ class TestStockValuation(SavepointCase): # Add a new move line to receive 10 more # --------------------------------------------------------------------- self.assertEqual(len(move1.move_line_ids), 1) - self.env['stock.move.line'].with_context(svl=True).create({ + self.env['stock.move.line'].create({ 'move_id': move1.id, 'product_id': move1.product_id.id, 'qty_done': 10, @@ -1786,7 +1785,7 @@ class TestStockValuation(SavepointCase): # Edit last move, send 14 instead # it should send 2@10 and 4@12 # --------------------------------------------------------------------- - move3.with_context(svl=True).quantity_done = 14 + move3.quantity_done = 14 self.assertEqual(move3.product_qty, 14) # old value: -80 -(8@10) # new value: -148 => -(10@10 + 4@12) @@ -1879,7 +1878,7 @@ class TestStockValuation(SavepointCase): # --------------------------------------------------------------------- # Actually, send 8 in the last move # --------------------------------------------------------------------- - move2.with_context(svl=True).quantity_done = 8 + move2.quantity_done = 8 self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), -80.0) # the move actually sent 8@10 @@ -1892,7 +1891,7 @@ class TestStockValuation(SavepointCase): # --------------------------------------------------------------------- # Actually, send 10 in the last move # --------------------------------------------------------------------- - move2.with_context(svl=True).quantity_done = 10 + move2.quantity_done = 10 self.assertEqual(sum(move2.stock_valuation_layer_ids.mapped('value')), -100.0) # the move actually sent 10@10 self.assertEqual(sum(self.product1.stock_valuation_layer_ids.mapped('remaining_qty')), 0) @@ -2068,7 +2067,7 @@ class TestStockValuation(SavepointCase): self.assertEqual(self.product1.quantity_svl, -5) self.assertEqual(self.product1.value_svl, -62.5) - move2.with_context(svl=True).move_line_ids.qty_done = 20 + move2.move_line_ids.qty_done = 20 self.assertEqual(self.product1.quantity_svl, 5) self.assertEqual(self.product1.value_svl, 87.5) @@ -2129,7 +2128,7 @@ class TestStockValuation(SavepointCase): move4._action_assign() move4.move_line_ids.qty_done = 10.0 move4._action_done() - move2.with_context(svl=True).move_line_ids.qty_done = 0 + move2.move_line_ids.qty_done = 0 self.assertEqual(self.product1.value_svl, -187.5) def test_average_perpetual_4(self): @@ -2268,7 +2267,7 @@ class TestStockValuation(SavepointCase): self.assertAlmostEqual(self.product1.quantity_svl, 15) self.assertAlmostEqual(self.product1.value_svl, 250) - move1.move_line_ids.with_context(svl=True).qty_done = 15 + move1.move_line_ids.qty_done = 15 self.assertAlmostEqual(self.product1.standard_price, 14.0) self.assertAlmostEqual(len(move1.stock_valuation_layer_ids), 2) @@ -2365,7 +2364,7 @@ class TestStockValuation(SavepointCase): self.assertEqual(move2_valuation_aml.debit, 0) self.assertEqual(move2_valuation_aml.credit, 200) - move2.with_context(svl=True).quantity_done = 10.0 + move2.quantity_done = 10.0 valuation_aml = self._get_stock_valuation_move_lines() move2_valuation_aml = valuation_aml[-1] @@ -2373,7 +2372,7 @@ class TestStockValuation(SavepointCase): self.assertEqual(move2_valuation_aml.debit, 100) self.assertEqual(move2_valuation_aml.credit, 0) - move2.with_context(svl=True).quantity_done = 11.0 + move2.quantity_done = 11.0 valuation_aml = self._get_stock_valuation_move_lines() move2_valuation_aml = valuation_aml[-1] @@ -3139,7 +3138,7 @@ class TestStockValuation(SavepointCase): self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date8)).value_svl, 712.5) # edit the done quantity of move1, decrease it - move1.with_context(svl=True).quantity_done = 5 + move1.quantity_done = 5 # the change is only visible right now self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).quantity_svl, 10) @@ -3151,7 +3150,7 @@ class TestStockValuation(SavepointCase): self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date2)).value_svl, 100) # edit move 4, send 15 instead of 20 - move4.with_context(svl=True).quantity_done = 15 + move4.quantity_done = 15 # -(20*5) + (5*7.5) self.assertEqual(sum(move4.stock_valuation_layer_ids.mapped('value')), -62.5) @@ -3278,7 +3277,7 @@ class TestStockValuation(SavepointCase): self.assertEqual(self.product1.value_svl, 1275) # Edit the quantity done of move1, increase it. - move1.with_context(svl=True).quantity_done = 20 + move1.quantity_done = 20 self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).quantity_svl, 10) self.assertEqual(self.product1.with_context(to_date=Datetime.to_string(date1)).value_svl, 100) diff --git a/addons/stock_account/tests/test_stockvaluationlayer.py b/addons/stock_account/tests/test_stockvaluationlayer.py index bc34803c87b..bb5692e3e8c 100644 --- a/addons/stock_account/tests/test_stockvaluationlayer.py +++ b/addons/stock_account/tests/test_stockvaluationlayer.py @@ -170,7 +170,7 @@ class TestStockValuationStandard(TestStockValuationCommon): move1 = self._make_in_move(self.product1, 10) move2 = self._make_in_move(self.product1, 10) move3 = self._make_out_move(self.product1, 15) - self.env['stock.move.line'].with_context(svl=True).create({ + self.env['stock.move.line'].create({ 'move_id': move1.id, 'product_id': move1.product_id.id, 'qty_done': 5, @@ -221,7 +221,7 @@ class TestStockValuationStandard(TestStockValuationCommon): move1 = self._make_in_move(self.product1, 10) move2 = self._make_out_move(self.product1, 15) - self.env['stock.move.line'].with_context(svl=True).create({ + self.env['stock.move.line'].create({ 'move_id': move1.id, 'product_id': move1.product_id.id, 'qty_done': 10, @@ -249,7 +249,7 @@ class TestStockValuationStandard(TestStockValuationCommon): self.product1.product_tmpl_id.categ_id.property_valuation = 'manual_periodic' move1 = self._make_dropship_move(self.product1, 10) - move1.move_line_ids.with_context(svl=True).qty_done = 15 + move1.move_line_ids.qty_done = 15 valuation_layers = self.product1.stock_valuation_layer_ids self.assertEqual(len(valuation_layers), 4) @@ -304,7 +304,7 @@ class TestStockValuationAVCO(TestStockValuationCommon): move1 = self._make_in_move(self.product1, 10, unit_cost=10) move2 = self._make_in_move(self.product1, 10, unit_cost=20) move3 = self._make_out_move(self.product1, 15) - self.env['stock.move.line'].with_context(svl=True).create({ + self.env['stock.move.line'].create({ 'move_id': move1.id, 'product_id': move1.product_id.id, 'qty_done': 5, @@ -321,7 +321,7 @@ class TestStockValuationAVCO(TestStockValuationCommon): move1 = self._make_in_move(self.product1, 10, unit_cost=10, create_picking=True) move2 = self._make_in_move(self.product1, 10, unit_cost=20) move3 = self._make_out_move(self.product1, 15) - self.env['stock.move.line'].with_context(svl=True).create({ + self.env['stock.move.line'].create({ 'product_id': move1.product_id.id, 'qty_done': 5, 'product_uom_id': move1.product_uom.id, @@ -478,7 +478,7 @@ class TestStockValuationFIFO(TestStockValuationCommon): move1 = self._make_in_move(self.product1, 20, unit_cost=10) move2 = self._make_out_move(self.product1, 10) move3 = self._make_out_move(self.product1, 10) - move1.move_line_ids.with_context(svl=True).qty_done = 10 + move1.move_line_ids.qty_done = 10 move4 = self._make_in_move(self.product1, 20, unit_cost=15) self.assertEqual(self.product1.value_svl, 150) @@ -501,7 +501,7 @@ class TestStockValuationFIFO(TestStockValuationCommon): move3 = self._make_out_move(self.product1, 15) move4 = self._make_out_move(self.product1, 20) move5 = self._make_in_move(self.product1, 100, unit_cost=15) - move1.move_line_ids.with_context(svl=True).qty_done = 20 + move1.move_line_ids.qty_done = 20 self.assertEqual(self.product1.value_svl, 1375) self.assertEqual(self.product1.quantity_svl, 95) diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml index 3a36f18cdbe..e2815f500a2 100644 --- a/addons/stock_account/views/product_views.xml +++ b/addons/stock_account/views/product_views.xml @@ -80,23 +80,5 @@ - - Inventory Valuation - ir.actions.act_window - product.product - tree,form - form - - [('type', '=', 'product'), ('qty_available', '!=', 0)] - {'company_owned': True, 'create': False, 'edit' : False} - - - - Create a new product valuation - - If there are products, you will see its name and valuation. - - - diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml index 17cf9a232c2..f8af16cccc2 100644 --- a/addons/stock_account/views/stock_account_views.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -74,22 +74,6 @@ - - product.stock.tree.2 - product.product - - - - - - - - - - - - - product.product @@ -119,37 +103,5 @@ - - - stock.account.aml - account.move.line - - - - - - - - - - - - - - - stock.move.tree.valuation.at.date - stock.move - - - - - - - - - - - - diff --git a/addons/stock_account/wizard/stock_quantity_history.py b/addons/stock_account/wizard/stock_quantity_history.py index 59e5ab01c58..c55133462b1 100644 --- a/addons/stock_account/wizard/stock_quantity_history.py +++ b/addons/stock_account/wizard/stock_quantity_history.py @@ -10,25 +10,10 @@ class StockQuantityHistory(models.TransientModel): if not self.env.context.get('valuation'): return super(StockQuantityHistory, self).open_table() - self.env['stock.move']._run_fifo_vacuum() - + action = self.env.ref('stock_account.stock_valuation_layer_action').read()[0] if int(self.compute_at_date): - tree_view_id = self.env.ref('stock_account.view_stock_product_tree2').id - form_view_id = self.env.ref('stock.product_form_view_procurement_button').id - search_view_id = self.env.ref('stock_account.view_inventory_valuation_search').id - # We pass `to_date` in the context so that `qty_available` will be computed across - # moves until date. - action = { - 'type': 'ir.actions.act_window', - 'views': [(tree_view_id, 'tree'), (form_view_id, 'form')], - 'view_mode': 'tree,form', - 'name': _('Inventory Valuation'), - 'res_model': 'product.product', - 'domain': "[('type', '=', 'product'), ('qty_available', '!=', 0)]", - 'context': dict(self.env.context, to_date=self.date, company_owned=True, create=False, edit=False), - 'search_view_id': search_view_id - } + action['domain'] = [('create_date', '<=', self.date)] return action else: - return self.env.ref('stock_account.product_valuation_action').read()[0] + return action
- Create a new product valuation -
- If there are products, you will see its name and valuation. -