diff --git a/addons/purchase_stock/models/account_invoice.py b/addons/purchase_stock/models/account_invoice.py index 979461cec1f..169e68e6f34 100644 --- a/addons/purchase_stock/models/account_invoice.py +++ b/addons/purchase_stock/models/account_invoice.py @@ -2,7 +2,8 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models -from odoo.tools import float_compare +from odoo.tools import float_compare, float_is_zero +from odoo.tools.misc import groupby class AccountMove(models.Model): @@ -149,7 +150,38 @@ class AccountMove(models.Model): if self._context.get('move_reverse_cancel'): return super()._post(soft) self.env['account.move.line'].create(self._stock_account_prepare_anglo_saxon_in_lines_vals()) - return super()._post(soft) + + # Create correction layer if invoice price is different + stock_valuation_layers = self.env['stock.valuation.layer'].sudo() + valued_lines = self.env['account.move.line'].sudo() + for invoice in self: + if invoice.sudo().stock_valuation_layer_ids: + continue + if invoice.move_type in ('in_invoice', 'in_refund', 'in_receipt'): + valued_lines |= invoice.invoice_line_ids.filtered( + lambda l: l.product_id and l.product_id.cost_method != 'standard') + if valued_lines: + stock_valuation_layers |= valued_lines._create_in_invoice_svl() + + for (product, company), dummy in groupby(stock_valuation_layers, key=lambda svl: (svl.product_id, svl.company_id)): + product = product.with_company(company.id) + if not float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding): + product.sudo().with_context(disable_auto_svl=True).write({'standard_price': product.value_svl / product.quantity_svl}) + + if stock_valuation_layers: + stock_valuation_layers._validate_accounting_entries() + + posted = super()._post(soft) + # The invoice reference is set during the super call + for layer in stock_valuation_layers: + description = f"{layer.account_move_line_id.move_id.display_name} - {layer.product_id.display_name}" + layer.description = description + if layer.product_id.valuation != 'real_time': + continue + layer.account_move_id.ref = description + layer.account_move_id.line_ids.write({'name': description}) + + return posted def _stock_account_get_last_step_stock_moves(self): """ Overridden from stock_account. diff --git a/addons/purchase_stock/models/account_move_line.py b/addons/purchase_stock/models/account_move_line.py index 96064adb5bc..40b0edfdcb1 100644 --- a/addons/purchase_stock/models/account_move_line.py +++ b/addons/purchase_stock/models/account_move_line.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models +from odoo.tools.float_utils import float_compare, float_is_zero class AccountMoveLine(models.Model): @@ -25,3 +26,94 @@ class AccountMoveLine(models.Model): total_out_and_not_invoiced_qty = max(0, out_qty - invoiced_qty) out_and_not_invoiced_qty = min(aml_qty, total_out_and_not_invoiced_qty) return self.product_id.uom_id._compute_quantity(out_and_not_invoiced_qty, self.product_uom_id) + + def _create_in_invoice_svl(self): + svl_vals_list = [] + for line in self: + line = line.with_company(line.company_id) + move = line.move_id.with_company(line.move_id.company_id) + po_line = line.purchase_line_id + uom = line.product_uom_id or line.product_id.uom_id + + # Don't create value for more quantity than received + quantity = po_line.qty_received - (po_line.qty_invoiced - line.quantity) + quantity = max(min(line.quantity, quantity), 0) + if float_is_zero(quantity, precision_rounding=uom.rounding): + continue + + layers = line._get_stock_valuation_layers(move) + # Retrieves SVL linked to a return. + if not layers: + continue + + price_unit = line._get_gross_unit_price() + price_unit = line.currency_id._convert(price_unit, line.company_id.currency_id, line.company_id, line.date, round=False) + price_unit = line.product_uom_id._compute_price(price_unit, line.product_id.uom_id) + layers_price_unit = line._get_stock_valuation_layers_price_unit(layers) + layers_to_correct = line._get_stock_layer_price_difference(layers, layers_price_unit, price_unit) + svl_vals_list += line._prepare_in_invoice_svl_vals(layers_to_correct) + return self.env['stock.valuation.layer'].sudo().create(svl_vals_list) + + def _get_stock_valuation_layers_price_unit(self, layers): + price_unit_by_layer = {} + for layer in layers: + price_unit_by_layer[layer] = layer.value / layer.quantity + return price_unit_by_layer + + def _get_stock_layer_price_difference(self, layers, layers_price_unit, price_unit): + self.ensure_one() + po_line = self.purchase_line_id + aml_qty = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id) + invoice_lines = po_line.invoice_lines - self + invoices_qty = 0 + for invoice_line in invoice_lines: + invoices_qty += invoice_line.product_uom_id._compute_quantity(invoice_line.quantity, invoice_line.product_id.uom_id) + qty_received = po_line.product_uom._compute_quantity(po_line.qty_received, self.product_id.uom_id) + out_qty = qty_received - sum(layers.mapped('remaining_qty')) + out_and_not_billed_qty = max(0, out_qty - invoices_qty) + total_to_correct = max(0, aml_qty - out_and_not_billed_qty) + # we also need to skip the remaining qty that is already billed + total_to_skip = max(0, invoices_qty - out_qty) + layers_to_correct = {} + for layer in layers: + if float_compare(total_to_correct, 0, precision_rounding=self.product_id.uom_id.rounding) <= 0: + break + remaining_qty = layer.remaining_qty + qty_to_skip = min(total_to_skip, remaining_qty) + remaining_qty = max(0, remaining_qty - qty_to_skip) + qty_to_correct = min(total_to_correct, remaining_qty) + total_to_skip -= qty_to_skip + total_to_correct -= qty_to_correct + unit_valuation_difference = price_unit - layers_price_unit[layer] + if float_is_zero(unit_valuation_difference * qty_to_correct, precision_rounding=self.company_id.currency_id.rounding): + continue + po_pu_curr = po_line.currency_id._convert(po_line.price_unit, self.currency_id, self.company_id, self.date, round=False) + price_difference_curr = po_pu_curr - self._get_gross_unit_price() + layers_to_correct[layer] = (qty_to_correct, unit_valuation_difference, price_difference_curr) + return layers_to_correct + + def _prepare_in_invoice_svl_vals(self, layers_correction): + svl_vals_list = [] + invoiced_qty = self.quantity + common_svl_vals = { + 'account_move_id': self.move_id.id, + 'account_move_line_id': self.id, + 'company_id': self.company_id.id, + 'product_id': self.product_id.id, + 'quantity': 0, + 'unit_cost': 0, + 'remaining_qty': 0, + 'remaining_value': 0, + 'description': self.move_id.name and '%s - %s' % (self.move_id.name, self.product_id.name) or self.product_id.name, + } + for layer, (quantity, price_difference, price_difference_curr) in layers_correction.items(): + svl_vals = self.product_id._prepare_in_svl_vals(quantity, price_difference) + diff_value_curr = self.currency_id.round(price_difference_curr * quantity) + svl_vals.update(**common_svl_vals, stock_valuation_layer_id=layer.id, price_diff_value=diff_value_curr) + svl_vals_list.append(svl_vals) + # Adds the difference into the last SVL's remaining value. + layer.remaining_value += svl_vals['value'] + if float_compare(invoiced_qty, 0, self.product_id.uom_id.rounding) <= 0: + break + + return svl_vals_list diff --git a/addons/stock_account/models/account_move.py b/addons/stock_account/models/account_move.py index bd52bc75b23..2f70779d21d 100644 --- a/addons/stock_account/models/account_move.py +++ b/addons/stock_account/models/account_move.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- from odoo import fields, models, api -from odoo.tools.float_utils import float_compare, float_is_zero -from odoo.tools.misc import groupby class AccountMove(models.Model): @@ -48,41 +46,12 @@ class AccountMove(models.Model): if self._context.get('move_reverse_cancel'): return super()._post(soft) - # Create correction layer if invoice price is different - stock_valuation_layers = self.env['stock.valuation.layer'].sudo() - valued_lines = self.env['account.move.line'].sudo() - for invoice in self: - if invoice.sudo().stock_valuation_layer_ids: - continue - if invoice.move_type in ('in_invoice', 'in_refund', 'in_receipt'): - valued_lines |= invoice.invoice_line_ids.filtered( - lambda l: l.product_id and l.product_id.cost_method != 'standard') - if valued_lines: - stock_valuation_layers |= valued_lines._create_in_invoice_svl() - - for (product, company), dummy in groupby(stock_valuation_layers, key=lambda svl: (svl.product_id, svl.company_id)): - product = product.with_company(company.id) - if not float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding): - product.sudo().with_context(disable_auto_svl=True).write({'standard_price': product.value_svl / product.quantity_svl}) - - if stock_valuation_layers: - stock_valuation_layers._validate_accounting_entries() - # Create additional COGS lines for customer invoices. self.env['account.move.line'].create(self._stock_account_prepare_anglo_saxon_out_lines_vals()) # Post entries. posted = super()._post(soft) - # The invoice reference is set during the super call - for layer in stock_valuation_layers: - description = f"{layer.account_move_line_id.move_id.display_name} - {layer.product_id.display_name}" - layer.description = description - if layer.product_id.valuation != 'real_time': - continue - layer.account_move_id.ref = description - layer.account_move_id.line_ids.write({'name': description}) - # Reconcile COGS lines in case of anglo-saxon accounting with perpetual valuation. posted._stock_account_anglo_saxon_reconcile_valuation() return posted @@ -274,33 +243,6 @@ class AccountMoveLine(models.Model): if accounts['stock_input']: line.account_id = accounts['stock_input'] - def _create_in_invoice_svl(self): - svl_vals_list = [] - for line in self: - line = line.with_company(line.company_id) - move = line.move_id.with_company(line.move_id.company_id) - po_line = line.purchase_line_id - uom = line.product_uom_id or line.product_id.uom_id - - # Don't create value for more quantity than received - quantity = po_line.qty_received - (po_line.qty_invoiced - line.quantity) - quantity = max(min(line.quantity, quantity), 0) - if float_is_zero(quantity, precision_rounding=uom.rounding): - continue - - layers = line._get_stock_valuation_layers(move) - # Retrieves SVL linked to a return. - if not layers: - continue - - price_unit = line._get_gross_unit_price() - price_unit = line.currency_id._convert(price_unit, line.company_id.currency_id, line.company_id, line.date, round=False) - price_unit = line.product_uom_id._compute_price(price_unit, line.product_id.uom_id) - layers_price_unit = line._get_stock_valuation_layers_price_unit(layers) - layers_to_correct = line._get_stock_layer_price_difference(layers, layers_price_unit, price_unit) - svl_vals_list += line._prepare_in_invoice_svl_vals(layers_to_correct) - return self.env['stock.valuation.layer'].sudo().create(svl_vals_list) - def _eligible_for_cogs(self): self.ensure_one() return self.product_id.type == 'product' and self.product_id.valuation == 'real_time' @@ -327,73 +269,9 @@ class AccountMoveLine(models.Model): valued_moves = valued_moves.filtered(lambda stock_move: stock_move._is_in()) return valued_moves.stock_valuation_layer_ids - def _get_stock_valuation_layers_price_unit(self, layers): - price_unit_by_layer = {} - for layer in layers: - price_unit_by_layer[layer] = layer.value / layer.quantity - return price_unit_by_layer - - def _get_stock_layer_price_difference(self, layers, layers_price_unit, price_unit): - self.ensure_one() - po_line = self.purchase_line_id - aml_qty = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id) - invoice_lines = po_line.invoice_lines - self - invoices_qty = 0 - for invoice_line in invoice_lines: - invoices_qty += invoice_line.product_uom_id._compute_quantity(invoice_line.quantity, invoice_line.product_id.uom_id) - qty_received = po_line.product_uom._compute_quantity(po_line.qty_received, self.product_id.uom_id) - out_qty = qty_received - sum(layers.mapped('remaining_qty')) - out_and_not_billed_qty = max(0, out_qty - invoices_qty) - total_to_correct = max(0, aml_qty - out_and_not_billed_qty) - # we also need to skip the remaining qty that is already billed - total_to_skip = max(0, invoices_qty - out_qty) - layers_to_correct = {} - for layer in layers: - if float_compare(total_to_correct, 0, precision_rounding=self.product_id.uom_id.rounding) <= 0: - break - remaining_qty = layer.remaining_qty - qty_to_skip = min(total_to_skip, remaining_qty) - remaining_qty = max(0, remaining_qty - qty_to_skip) - qty_to_correct = min(total_to_correct, remaining_qty) - total_to_skip -= qty_to_skip - total_to_correct -= qty_to_correct - unit_valuation_difference = price_unit - layers_price_unit[layer] - if float_is_zero(unit_valuation_difference * qty_to_correct, precision_rounding=self.company_id.currency_id.rounding): - continue - po_pu_curr = po_line.currency_id._convert(po_line.price_unit, self.currency_id, self.company_id, self.date, round=False) - price_difference_curr = po_pu_curr - self._get_gross_unit_price() - layers_to_correct[layer] = (qty_to_correct, unit_valuation_difference, price_difference_curr) - return layers_to_correct - def _get_valued_in_moves(self): return self.env['stock.move'] - def _prepare_in_invoice_svl_vals(self, layers_correction): - svl_vals_list = [] - invoiced_qty = self.quantity - common_svl_vals = { - 'account_move_id': self.move_id.id, - 'account_move_line_id': self.id, - 'company_id': self.company_id.id, - 'product_id': self.product_id.id, - 'quantity': 0, - 'unit_cost': 0, - 'remaining_qty': 0, - 'remaining_value': 0, - 'description': self.move_id.name and '%s - %s' % (self.move_id.name, self.product_id.name) or self.product_id.name, - } - for layer, (quantity, price_difference, price_difference_curr) in layers_correction.items(): - svl_vals = self.product_id._prepare_in_svl_vals(quantity, price_difference) - diff_value_curr = self.currency_id.round(price_difference_curr * quantity) - svl_vals.update(**common_svl_vals, stock_valuation_layer_id=layer.id, price_diff_value=diff_value_curr) - svl_vals_list.append(svl_vals) - # Adds the difference into the last SVL's remaining value. - layer.remaining_value += svl_vals['value'] - if float_compare(invoiced_qty, 0, self.product_id.uom_id.rounding) <= 0: - break - - return svl_vals_list - def _can_use_stock_accounts(self): return self.product_id.type == 'product' and self.product_id.categ_id.property_valuation == 'real_time'