190 lines
11 KiB
Python
190 lines
11 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from odoo import fields, models
|
||
|
from odoo.tools import float_compare, float_is_zero
|
||
|
from odoo.tools.misc import groupby
|
||
|
|
||
|
|
||
|
class AccountMove(models.Model):
|
||
|
_inherit = 'account.move'
|
||
|
|
||
|
def _stock_account_prepare_anglo_saxon_in_lines_vals(self):
|
||
|
''' Prepare values used to create the journal items (account.move.line) corresponding to the price difference
|
||
|
lines for vendor bills. It only concerns the quantities that have been delivered before the bill
|
||
|
Example:
|
||
|
Buy a product having a cost of 9 and a supplier price of 10 and being a storable product and having a perpetual
|
||
|
valuation in FIFO. Deliver the product and then post the bill. The vendor bill's journal entries looks like:
|
||
|
Account | Debit | Credit
|
||
|
---------------------------------------------------------------
|
||
|
101120 Stock Interim Account (Received) | 10.0 |
|
||
|
---------------------------------------------------------------
|
||
|
101100 Account Payable | | 10.0
|
||
|
---------------------------------------------------------------
|
||
|
This method computes values used to make two additional journal items:
|
||
|
---------------------------------------------------------------
|
||
|
101120 Stock Interim Account (Received) | | 1.0
|
||
|
---------------------------------------------------------------
|
||
|
xxxxxx Expenses | 1.0 |
|
||
|
---------------------------------------------------------------
|
||
|
:return: A list of Python dictionary to be passed to env['account.move.line'].create.
|
||
|
'''
|
||
|
lines_vals_list = []
|
||
|
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
|
||
|
|
||
|
for move in self:
|
||
|
if move.move_type not in ('in_invoice', 'in_refund', 'in_receipt') or not move.company_id.anglo_saxon_accounting:
|
||
|
continue
|
||
|
|
||
|
move = move.with_company(move.company_id)
|
||
|
for line in move.invoice_line_ids:
|
||
|
# Filter out lines being not eligible for price difference.
|
||
|
# Moreover, this function is used for standard cost method only.
|
||
|
if line.product_id.type != 'product' or line.product_id.valuation != 'real_time' or line.product_id.cost_method != 'standard':
|
||
|
continue
|
||
|
|
||
|
# Retrieve accounts needed to generate the price difference.
|
||
|
debit_expense_account = line._get_price_diff_account()
|
||
|
if not debit_expense_account:
|
||
|
continue
|
||
|
# Retrieve stock valuation moves.
|
||
|
valuation_stock_moves = self.env['stock.move'].search([
|
||
|
('purchase_line_id', '=', line.purchase_line_id.id),
|
||
|
('state', '=', 'done'),
|
||
|
('product_qty', '!=', 0.0),
|
||
|
]) if line.purchase_line_id else self.env['stock.move']
|
||
|
|
||
|
if line.product_id.cost_method != 'standard' and line.purchase_line_id:
|
||
|
if move.move_type == 'in_refund':
|
||
|
valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_out())
|
||
|
else:
|
||
|
valuation_stock_moves = valuation_stock_moves.filtered(lambda stock_move: stock_move._is_in())
|
||
|
|
||
|
if not valuation_stock_moves:
|
||
|
continue
|
||
|
|
||
|
valuation_price_unit_total, valuation_total_qty = valuation_stock_moves._get_valuation_price_and_qty(line, move.currency_id)
|
||
|
valuation_price_unit = valuation_price_unit_total / valuation_total_qty
|
||
|
valuation_price_unit = line.product_id.uom_id._compute_price(valuation_price_unit, line.product_uom_id)
|
||
|
else:
|
||
|
# Valuation_price unit is always expressed in invoice currency, so that it can always be computed with the good rate
|
||
|
price_unit = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id)
|
||
|
price_unit = -price_unit if line.move_id.move_type == 'in_refund' else price_unit
|
||
|
valuation_date = valuation_stock_moves and max(valuation_stock_moves.mapped('date')) or move.date
|
||
|
valuation_price_unit = line.company_currency_id._convert(
|
||
|
price_unit, move.currency_id,
|
||
|
move.company_id, valuation_date, round=False
|
||
|
)
|
||
|
|
||
|
|
||
|
price_unit = line._get_gross_unit_price()
|
||
|
|
||
|
price_unit_val_dif = price_unit - valuation_price_unit
|
||
|
# If there are some valued moves, we only consider their quantity already used
|
||
|
if line.product_id.cost_method == 'standard':
|
||
|
relevant_qty = line.quantity
|
||
|
else:
|
||
|
relevant_qty = line._get_out_and_not_invoiced_qty(valuation_stock_moves)
|
||
|
price_subtotal = relevant_qty * price_unit_val_dif
|
||
|
|
||
|
# We consider there is a price difference if the subtotal is not zero. In case a
|
||
|
# discount has been applied, we can't round the price unit anymore, and hence we
|
||
|
# can't compare them.
|
||
|
if (
|
||
|
not move.currency_id.is_zero(price_subtotal)
|
||
|
and float_compare(line["price_unit"], line.price_unit, precision_digits=price_unit_prec) == 0
|
||
|
):
|
||
|
|
||
|
# Add price difference account line.
|
||
|
vals = {
|
||
|
'name': line.name[:64],
|
||
|
'move_id': move.id,
|
||
|
'partner_id': line.partner_id.id or move.commercial_partner_id.id,
|
||
|
'currency_id': line.currency_id.id,
|
||
|
'product_id': line.product_id.id,
|
||
|
'product_uom_id': line.product_uom_id.id,
|
||
|
'quantity': relevant_qty,
|
||
|
'price_unit': price_unit_val_dif,
|
||
|
'price_subtotal': relevant_qty * price_unit_val_dif,
|
||
|
'amount_currency': relevant_qty * price_unit_val_dif * line.move_id.direction_sign,
|
||
|
'balance': line.currency_id._convert(
|
||
|
relevant_qty * price_unit_val_dif * line.move_id.direction_sign,
|
||
|
line.company_currency_id,
|
||
|
line.company_id, fields.Date.today(),
|
||
|
),
|
||
|
'account_id': debit_expense_account.id,
|
||
|
'analytic_distribution': line.analytic_distribution,
|
||
|
'display_type': 'cogs',
|
||
|
}
|
||
|
lines_vals_list.append(vals)
|
||
|
|
||
|
# Correct the amount of the current line.
|
||
|
vals = {
|
||
|
'name': line.name[:64],
|
||
|
'move_id': move.id,
|
||
|
'partner_id': line.partner_id.id or move.commercial_partner_id.id,
|
||
|
'currency_id': line.currency_id.id,
|
||
|
'product_id': line.product_id.id,
|
||
|
'product_uom_id': line.product_uom_id.id,
|
||
|
'quantity': relevant_qty,
|
||
|
'price_unit': -price_unit_val_dif,
|
||
|
'price_subtotal': relevant_qty * -price_unit_val_dif,
|
||
|
'amount_currency': relevant_qty * -price_unit_val_dif * line.move_id.direction_sign,
|
||
|
'balance': line.currency_id._convert(
|
||
|
relevant_qty * -price_unit_val_dif * line.move_id.direction_sign,
|
||
|
line.company_currency_id,
|
||
|
line.company_id, fields.Date.today(),
|
||
|
),
|
||
|
'account_id': line.account_id.id,
|
||
|
'analytic_distribution': line.analytic_distribution,
|
||
|
'display_type': 'cogs',
|
||
|
}
|
||
|
lines_vals_list.append(vals)
|
||
|
return lines_vals_list
|
||
|
|
||
|
def _post(self, soft=True):
|
||
|
if not self._context.get('move_reverse_cancel'):
|
||
|
self.env['account.move.line'].create(self._stock_account_prepare_anglo_saxon_in_lines_vals())
|
||
|
|
||
|
# Create correction layer and impact accounts 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:
|
||
|
svls, _amls = valued_lines._apply_price_difference()
|
||
|
stock_valuation_layers |= svls
|
||
|
|
||
|
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})
|
||
|
|
||
|
posted = super(AccountMove, self.with_context(skip_cogs_reconciliation=True))._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 stock_valuation_layers:
|
||
|
stock_valuation_layers._validate_accounting_entries()
|
||
|
|
||
|
self._stock_account_anglo_saxon_reconcile_valuation()
|
||
|
|
||
|
return posted
|
||
|
|
||
|
def _stock_account_get_last_step_stock_moves(self):
|
||
|
""" Overridden from stock_account.
|
||
|
Returns the stock moves associated to this invoice."""
|
||
|
rslt = super(AccountMove, self)._stock_account_get_last_step_stock_moves()
|
||
|
for invoice in self.filtered(lambda x: x.move_type == 'in_invoice'):
|
||
|
rslt += invoice.mapped('invoice_line_ids.purchase_line_id.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'supplier')
|
||
|
for invoice in self.filtered(lambda x: x.move_type == 'in_refund'):
|
||
|
rslt += invoice.mapped('invoice_line_ids.purchase_line_id.move_ids').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'supplier')
|
||
|
return rslt
|