[FIX] account_stock, purchase_stock: prevent AttributeError when confirm vendor BILL/refund with FIFO/AVCO

AttributeError "account.move.line" object has no attribute "purchase_line_id"
occurs in `stock_account` when we confirm vendor BILL or REFUND with product
costing method "first in first out" or "average cost".

With the recent reflacted changes in commit[1], the `purchase_line_id`
field is used and it belongs to `purchase`, but it is not installed and does
not depend, so it causes an error.

The above issue is solved by removing some code in commit[1] from
`stock_account` and adding it to `purchase_stock`.

committ[1] - https://github.com/odoo/odoo/pull/99411/commits/e9ce88a9372843abef7cf8fc94c4dbe5f16c5fa3#diff-e6134a1a5a13058e35f86426a96db0acac44553fa3b0ca26716390f7b19fc96cR318

sentry-3975529425

closes odoo/odoo#121070

X-original-commit: 23ba3c6684e388bcfe317e7ab9f4c9008622e2b4
Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
Signed-off-by: Ansari Mahamadasif (maan) <maan@odoo.com>
This commit is contained in:
Mahamadasif Ansari
2023-05-11 12:01:55 +02:00
parent 3eb39b5913
commit 212adc34ee
3 changed files with 126 additions and 124 deletions
@@ -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.
@@ -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
-122
View File
@@ -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'