1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo.tools.float_utils import float_compare, float_is_zero
from collections import defaultdict
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def _get_valued_in_moves(self):
self.ensure_one()
return self.purchase_line_id.move_ids.filtered(
lambda m: m.state == 'done' and m.product_qty != 0)
def _get_out_and_not_invoiced_qty(self, in_moves):
self.ensure_one()
if not in_moves:
return 0
aml_qty = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
invoiced_qty = sum(line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id)
for line in self.purchase_line_id.invoice_lines - self)
layers = in_moves.stock_valuation_layer_ids
layers_qty = sum(layers.mapped('quantity'))
out_qty = layers_qty - sum(layers.mapped('remaining_qty'))
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 _apply_price_difference(self):
svl_vals_list = []
aml_vals_list = []
for line in self:
line = line.with_company(line.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_valued_in_moves().stock_valuation_layer_ids.filtered(lambda svl: svl.product_id == line.product_id and not svl.stock_valuation_layer_id)
if not layers:
continue
new_svl_vals_list, new_aml_vals_list = line._generate_price_difference_vals(layers)
svl_vals_list += new_svl_vals_list
aml_vals_list += new_aml_vals_list
return self.env['stock.valuation.layer'].sudo().create(svl_vals_list), self.env['account.move.line'].sudo().create(aml_vals_list)
def _generate_price_difference_vals(self, layers):
"""
The method will determine which layers are impacted by the AML (`self`) and, in case of a price difference, it
will then return the values of the new AMLs and SVLs
"""
self.ensure_one()
po_line = self.purchase_line_id
product_uom = self.product_id.uom_id
# `history` is a list of tuples: (time, aml, layer)
# aml and layer will never be both defined
# we use this to get an order between posted AML and layers
history = [(layer.create_date, False, layer) for layer in layers]
am_state_field = self.env['ir.model.fields'].search([('model', '=', 'account.move'), ('name', '=', 'state')], limit=1)
for aml in po_line.invoice_lines:
move = aml.move_id
if move.state != 'posted':
continue
state_trackings = move.message_ids.tracking_value_ids.filtered(lambda t: t.field_id == am_state_field).sorted('id')
time = state_trackings[-1:].create_date or move.create_date # `or` in case it has been created in posted state
history.append((time, aml, False))
# Sort history based on the datetime. In case of equality, the prority is given to SVLs, then to IDs.
# That way, we ensure a deterministic behaviour
history.sort(key=lambda item: (item[0], bool(item[1]), (item[1] or item[2]).id))
# the next dict is a matrix [layer L, invoice I] where each cell gives two info:
# [initial qty of L invoiced by I, remaining invoiced qty]
# the second info is usefull in case of a refund
layers_and_invoices_qties = defaultdict(lambda: [0, 0])
# the next dict will also provide two info:
# [total qty to invoice, remaining qty to invoice]
# we need the total qty to invoice, so we will be able to deduce the invoiced qty before `self`
qty_to_invoice_per_layer = defaultdict(lambda: [0, 0])
# Replay the whole history: we want to know what are the links between each layer and each invoice,
# and then the links between `self` and the layers
history.append((False, self, False)) # time was only usefull for the sorting
for _time, aml, layer in history:
if layer:
total_layer_qty_to_invoice = abs(layer.quantity)
initial_layer = layer.stock_move_id.origin_returned_move_id.stock_valuation_layer_ids
if initial_layer:
# `layer` is a return. We will cancel the qty to invoice of the returned layer
# /!\ we will cancel the qty not yet invoiced only
initial_layer_remaining_qty = qty_to_invoice_per_layer[initial_layer][1]
common_qty = min(initial_layer_remaining_qty, total_layer_qty_to_invoice)
qty_to_invoice_per_layer[initial_layer][0] -= common_qty
qty_to_invoice_per_layer[initial_layer][1] -= common_qty
total_layer_qty_to_invoice = max(0, total_layer_qty_to_invoice - common_qty)
if float_compare(total_layer_qty_to_invoice, 0, precision_rounding=product_uom.rounding) > 0:
qty_to_invoice_per_layer[layer] = [total_layer_qty_to_invoice, total_layer_qty_to_invoice]
else:
invoice = aml.move_id
impacted_invoice = False
aml_qty = aml.product_uom_id._compute_quantity(aml.quantity, product_uom)
if aml.is_refund:
reversed_invoice = aml.move_id.reversed_entry_id
if reversed_invoice:
sign = -1
impacted_invoice = reversed_invoice
# it's a refund, therefore we can only consume the quantities invoiced by
# the initial invoice (`reversed_invoice`)
layers_to_consume = []
for layer in layers:
remaining_invoiced_qty = layers_and_invoices_qties[(layer, reversed_invoice)][1]
layers_to_consume.append((layer, remaining_invoiced_qty))
else:
# the refund has been generated because of a stock return, let's find and use it
sign = 1
layers_to_consume = []
for layer in qty_to_invoice_per_layer:
if layer.stock_move_id._is_out():
layers_to_consume.append((layer, qty_to_invoice_per_layer[layer][1]))
else:
# classic case, we are billing a received quantity so let's use the incoming SVLs
sign = 1
layers_to_consume = []
for layer in qty_to_invoice_per_layer:
if layer.stock_move_id._is_in():
layers_to_consume.append((layer, qty_to_invoice_per_layer[layer][1]))
while float_compare(aml_qty, 0, precision_rounding=product_uom.rounding) > 0 and layers_to_consume:
layer, total_layer_qty_to_invoice = layers_to_consume[0]
layers_to_consume = layers_to_consume[1:]
if float_is_zero(total_layer_qty_to_invoice, precision_rounding=product_uom.rounding):
continue
common_qty = min(aml_qty, total_layer_qty_to_invoice)
aml_qty -= common_qty
qty_to_invoice_per_layer[layer][1] -= sign * common_qty
layers_and_invoices_qties[(layer, invoice)] = [common_qty, common_qty]
layers_and_invoices_qties[(layer, impacted_invoice)][1] -= common_qty
# Now we know what layers does `self` use, let's check if we have to create a pdiff SVL
# (or cancel such an SVL in case of a refund)
invoice = self.move_id
svl_vals_list = []
aml_vals_list = []
for layer in layers:
# use the link between `self` and `layer` (i.e. the qty of `layer` billed by `self`)
invoicing_layer_qty = layers_and_invoices_qties[(layer, invoice)][1]
if float_is_zero(invoicing_layer_qty, precision_rounding=product_uom.rounding):
continue
# We will only consider the total quantity to invoice of the layer because we don't
# want to invoice a part of the layer that has not been invoiced and that has been
# returned in the meantime
total_layer_qty_to_invoice = qty_to_invoice_per_layer[layer][0]
remaining_qty = layer.remaining_qty
out_layer_qty = total_layer_qty_to_invoice - remaining_qty
if self.is_refund:
sign = -1
reversed_invoice = invoice.reversed_entry_id
if not reversed_invoice:
# this is a refund for a returned quantity, we don't have anything to do
continue
initial_invoiced_qty = layers_and_invoices_qties[(layer, reversed_invoice)][0]
initial_pdiff_svl = layer.stock_valuation_layer_ids.filtered(lambda svl: svl.account_move_line_id.move_id == reversed_invoice)
if not initial_pdiff_svl or float_is_zero(initial_invoiced_qty, precision_rounding=product_uom.rounding):
continue
# We have an already-out quantity: we must skip the part already invoiced. So, first,
# let's compute the already invoiced quantity...
previously_invoiced_qty = 0
for item in history:
previous_aml = item[1]
if not previous_aml or previous_aml.is_refund:
continue
previous_invoice = previous_aml.move_id
if previous_invoice == reversed_invoice:
break
previously_invoiced_qty += layers_and_invoices_qties[(layer, previous_invoice,)][1]
# ... Second, skip it:
out_qty_to_invoice = max(0, out_layer_qty - previously_invoiced_qty)
qty_to_correct = max(0, invoicing_layer_qty - out_qty_to_invoice)
if out_qty_to_invoice:
# In case the out qty is different from the one posted by the initial bill, we should compensate
# this quantity with debit/credit between stock_in and expense, but we are reversing an initial
# invoice and don't want to do more than the original one
out_qty_to_invoice = 0
aml = initial_pdiff_svl.account_move_line_id
parent_layer = initial_pdiff_svl.stock_valuation_layer_id
layer_price_unit = parent_layer._get_layer_price_unit()
else:
sign = 1
# get the invoiced qty of the layer without considering `self`
invoiced_layer_qty = total_layer_qty_to_invoice - qty_to_invoice_per_layer[layer][1] - invoicing_layer_qty
remaining_out_qty_to_invoice = max(0, out_layer_qty - invoiced_layer_qty)
out_qty_to_invoice = min(remaining_out_qty_to_invoice, invoicing_layer_qty)
qty_to_correct = invoicing_layer_qty - out_qty_to_invoice
layer_price_unit = layer._get_layer_price_unit()
returned_move = layer.stock_move_id.origin_returned_move_id
if returned_move and returned_move._is_out() and returned_move._is_returned(valued_type='out'):
# Odd case! The user receives a product, then returns it. The returns are processed as classic
# output, so the value of the returned product can be different from the initial one. The user
# then receives again the returned product (that's where we are here) -> the SVL is based on
# the returned one, the accounting entries are already compensated, and we don't want to impact
# the stock valuation. So, let's fake the layer price unit with the POL one as everything is
# already ok
layer_price_unit = po_line._get_gross_price_unit()
aml = self
aml_gross_price_unit = aml._get_gross_unit_price()
# convert from aml currency to company currency
aml_price_unit = aml_gross_price_unit / aml.currency_rate
aml_price_unit = aml.product_uom_id._compute_price(aml_price_unit, product_uom)
unit_valuation_difference = aml_price_unit - layer_price_unit
# Generate the AML values for the already out quantities
# convert from company currency to aml currency
unit_valuation_difference_curr = unit_valuation_difference * self.currency_rate
unit_valuation_difference_curr = product_uom._compute_price(unit_valuation_difference_curr, self.product_uom_id)
out_qty_to_invoice = product_uom._compute_quantity(out_qty_to_invoice, self.product_uom_id)
if not float_is_zero(unit_valuation_difference_curr * out_qty_to_invoice, precision_rounding=self.currency_id.rounding):
aml_vals_list += self._prepare_pdiff_aml_vals(out_qty_to_invoice, unit_valuation_difference_curr)
# Generate the SVL values for the on hand quantities (and impact the parent layer)
po_pu_curr = po_line.currency_id._convert(po_line.price_unit, self.currency_id, self.company_id, self.move_id.invoice_date or self.date or fields.Date.context_today(self), round=False)
price_difference_curr = po_pu_curr - aml_gross_price_unit
if not float_is_zero(unit_valuation_difference * qty_to_correct, precision_rounding=self.company_id.currency_id.rounding):
svl_vals = self._prepare_pdiff_svl_vals(layer, sign * qty_to_correct, unit_valuation_difference, price_difference_curr)
layer.remaining_value += svl_vals['value']
svl_vals_list.append(svl_vals)
return svl_vals_list, aml_vals_list
def _prepare_pdiff_aml_vals(self, qty, unit_valuation_difference):
self.ensure_one()
vals_list = []
sign = self.move_id.direction_sign
expense_account = self.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=self.move_id.fiscal_position_id)['expense']
if not expense_account:
return vals_list
for price, account in [
(unit_valuation_difference, expense_account),
(-unit_valuation_difference, self.account_id),
]:
vals_list.append({
'name': self.name[:64],
'move_id': self.move_id.id,
'partner_id': self.partner_id.id or self.move_id.commercial_partner_id.id,
'currency_id': self.currency_id.id,
'product_id': self.product_id.id,
'product_uom_id': self.product_uom_id.id,
'balance': self.company_id.currency_id.round((qty * price * sign) / self.currency_rate),
'account_id': account.id,
'analytic_distribution': self.analytic_distribution,
'display_type': 'cogs',
})
return vals_list
def _prepare_pdiff_svl_vals(self, corrected_layer, quantity, unit_cost, pdiff):
self.ensure_one()
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,
}
return {
**self.product_id._prepare_in_svl_vals(quantity, unit_cost, corrected_layer.lot_id),
**common_svl_vals,
'stock_valuation_layer_id': corrected_layer.id,
'price_diff_value': self.currency_id.round(pdiff * quantity),
}
def _get_price_unit_val_dif_and_relevant_qty(self):
self.ensure_one()
# Retrieve stock valuation moves.
valuation_stock_moves = self.env['stock.move'].search([
('purchase_line_id', '=', self.purchase_line_id.id),
('state', '=', 'done'),
('product_qty', '!=', 0.0),
]) if self.purchase_line_id else self.env['stock.move']
if self.product_id.cost_method != 'standard' and self.purchase_line_id:
if self.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:
return 0, 0
valuation_price_unit_total, valuation_total_qty = valuation_stock_moves._get_valuation_price_and_qty(self, self.move_id.currency_id)
valuation_price_unit = valuation_price_unit_total / valuation_total_qty
valuation_price_unit = self.product_id.uom_id._compute_price(valuation_price_unit, self.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 = self.product_id.uom_id._compute_price(self.product_id.standard_price, self.product_uom_id)
price_unit = -price_unit if self.move_id.move_type == 'in_refund' else price_unit
valuation_date = valuation_stock_moves and max(valuation_stock_moves.mapped('date')) or self.date
valuation_price_unit = self.company_currency_id._convert(
price_unit, self.currency_id,
self.company_id, valuation_date, round=False
)
price_unit = self._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 self.product_id.cost_method == 'standard':
relevant_qty = self.quantity
else:
relevant_qty = self._get_out_and_not_invoiced_qty(valuation_stock_moves)
return price_unit_val_dif, relevant_qty
|