[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:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user