Skip to content

Commit e637a3a

Browse files
kos94ok-3DMiquelRForgeFlow
authored andcommitted
[MIG] stock_account: generate Stock valuation layers according to Stock move and Product price history
1 parent ccee11e commit e637a3a

File tree

1 file changed

+313
-24
lines changed

1 file changed

+313
-24
lines changed

addons/stock_account/migrations/13.0.1.1/post-migration.py

Lines changed: 313 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,330 @@
11
# Copyright 2020 ForgeFlow <http://www.forgeflow.com>
2+
# Copyright 2020 Andrii Skrypka
23
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
34
from openupgradelib import openupgrade
5+
from odoo import _
6+
from odoo.osv import expression
7+
from odoo.tools.float_utils import float_is_zero
48

59

6-
def fill_stock_valuation_layer(env):
10+
@openupgrade.logging()
11+
def generate_stock_valuation_layer(env):
12+
""" Generate stock.valuation.layers according to stock.move and product.price.history"""
713
openupgrade.logged_query(
814
env.cr, """
915
ALTER TABLE stock_valuation_layer
1016
ADD COLUMN old_product_price_history_id integer""",
1117
)
12-
product_ids = env['product.product'].search(
13-
[('type', '=', 'product')]).ids
14-
if product_ids:
15-
openupgrade.logged_query(
16-
env.cr, """
17-
INSERT INTO stock_valuation_layer (old_product_price_history_id,
18-
company_id, product_id, quantity, unit_cost, description,
19-
create_uid, create_date, write_uid, write_date)
20-
SELECT pph.id, pph.company_id, pph.product_id, 0, pph2.cost,
21-
pt.name, pph2.create_uid, pph2.create_date, pph2.write_uid,
22-
pph2.write_date
23-
FROM (SELECT max(id) as id, company_id, product_id
24-
FROM product_price_history
25-
GROUP BY company_id, product_id
26-
ORDER BY id
27-
) pph
28-
JOIN product_price_history pph2 ON pph.id = pph2.id
29-
JOIN product_product pp ON pph.product_id = pp.id
30-
JOIN product_template pt ON pp.product_tmpl_id = pt.id
31-
WHERE pp.id IN %s""", (tuple(product_ids), ),
32-
)
33-
# NOTE: It seems to be incomplete (without the link of the stock move)
18+
std_price_update = {} # keep standard price at datetime
19+
done_moves = env['stock.move'].with_context(
20+
tz='UTC', tracking_disable=True).search([
21+
('state', '=', 'done'),
22+
], order='company_id, product_id, date ASC')
23+
for move in done_moves:
24+
diff = move.quantity_done
25+
if move._is_in() and diff > 0 or move._is_out() and diff < 0:
26+
_product_price_update_before_done(env, move, std_price_update)
27+
_create_in_svl(env, move, std_price_update)
28+
if move.product_id.cost_method in ('average', 'fifo'):
29+
_run_fifo_vacuum(env, move.product_id, move.company_id, std_price_update)
30+
elif move._is_in() and diff < 0 or move._is_out() and diff > 0:
31+
_create_out_svl(env, move, std_price_update)
32+
elif move._is_dropshipped() and diff > 0 or move._is_dropshipped_returned() and diff < 0:
33+
_create_dropshipped_svl(env, move, std_price_update)
34+
elif move._is_dropshipped() and diff < 0 or move._is_dropshipped_returned() and diff > 0:
35+
_create_dropshipped_svl(env, move, std_price_update)
36+
37+
38+
def _product_price_update_before_done(env, move, std_price_update):
39+
# adapt standard price on incomming moves if the product cost_method is 'average'
40+
if move.with_context(force_company=move.company_id.id).product_id.cost_method == 'average':
41+
product = move.product_id.with_context(force_company=move.company_id.id)
42+
value_svl, product_tot_qty_available = _compute_value_svl(env, product, move.company_id)
43+
rounding = move.product_id.uom_id.rounding
44+
valued_move_lines = move._get_in_move_lines()
45+
qty_done = 0.0
46+
for valued_move_line in valued_move_lines:
47+
qty_done += valued_move_line.product_uom_id._compute_quantity(
48+
valued_move_line.qty_done, move.product_id.uom_id)
49+
qty = qty_done
50+
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
51+
new_std_price = _get_price_unit(move, std_price_update)
52+
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
53+
float_is_zero(product_tot_qty_available + qty, precision_rounding=rounding):
54+
new_std_price = _get_price_unit(move, std_price_update)
55+
else:
56+
# Get the standard price
57+
amount_unit = move.product_id.with_context(force_company=move.company_id.id).standard_price
58+
new_std_price = (
59+
(amount_unit * product_tot_qty_available) + (_get_price_unit(move, std_price_update) * qty)
60+
) / (product_tot_qty_available + qty)
61+
std_price_update[move.company_id.id, move.product_id.id] = new_std_price
62+
63+
64+
def _create_in_svl(env, move, std_price_update):
65+
move = move.with_context(force_company=move.company_id.id)
66+
valued_move_lines = move._get_in_move_lines()
67+
valued_quantity = 0.0
68+
for valued_move_line in valued_move_lines:
69+
valued_quantity += valued_move_line.product_uom_id._compute_quantity(
70+
valued_move_line.qty_done, move.product_id.uom_id)
71+
_change_standard_price(env, move, std_price_update) # Create SVL for manual updated standard price
72+
unit_cost = abs(_get_price_unit(move, std_price_update)) # May be negative (i.e. decrease an out move).
73+
if move.product_id.cost_method == 'standard':
74+
unit_cost = std_price_update.get((move.company_id.id, move.product_id.id), move.product_id.standard_price)
75+
vals = move.product_id._prepare_in_svl_vals(valued_quantity, unit_cost)
76+
vals.update(move._prepare_common_svl_vals())
77+
vals.update({
78+
'create_date': move.date,
79+
'create_uid': move.create_uid.id or 1,
80+
'write_date': move.date,
81+
'write_uid': move.create_uid.id or 1,
82+
})
83+
_create_stock_valuation_layer(env, [vals])
84+
85+
86+
def _create_out_svl(env, move, std_price_update):
87+
move = move.with_context(force_company=move.company_id.id)
88+
valued_move_lines = move._get_out_move_lines()
89+
valued_quantity = 0.0
90+
for valued_move_line in valued_move_lines:
91+
valued_quantity += valued_move_line.product_uom_id._compute_quantity(
92+
valued_move_line.qty_done, move.product_id.uom_id)
93+
if float_is_zero(valued_quantity, precision_rounding=move.product_id.uom_id.rounding):
94+
return
95+
_change_standard_price(env, move, std_price_update)
96+
vals = move.product_id._prepare_out_svl_vals(valued_quantity, move.company_id)
97+
vals.update(move._prepare_common_svl_vals())
98+
vals.update({
99+
'create_date': move.date,
100+
'create_uid': move.create_uid.id or 1,
101+
'write_date': move.date,
102+
'write_uid': move.create_uid.id or 1,
103+
})
104+
_create_stock_valuation_layer(env, [vals])
105+
106+
107+
def _create_dropshipped_svl(env, move, std_price_update):
108+
svl_vals_list = []
109+
move = move.with_context(force_company=move.company_id.id)
110+
valued_move_lines = move.move_line_ids
111+
valued_quantity = 0.0
112+
for valued_move_line in valued_move_lines:
113+
valued_quantity += valued_move_line.product_uom_id._compute_quantity(
114+
valued_move_line.qty_done, move.product_id.uom_id)
115+
quantity = valued_quantity
116+
_change_standard_price(env, move, std_price_update)
117+
unit_cost = _get_price_unit(move, std_price_update)
118+
company = move.product_id.env.company
119+
if move.product_id.cost_method == 'standard' and (company.id, move.product_id.id) in std_price_update:
120+
unit_cost = std_price_update[(company.id, move.product_id.id)]
121+
common_vals = dict(move._prepare_common_svl_vals(),
122+
remaining_qty=0, create_date=move.date, create_uid=move.create_uid.id,
123+
write_date=move.date, write_uid=move.create_uid.id)
124+
# create the in
125+
in_vals = {
126+
'unit_cost': unit_cost,
127+
'value': unit_cost * quantity,
128+
'quantity': quantity,
129+
}
130+
in_vals.update(common_vals)
131+
svl_vals_list.append(in_vals)
132+
# create the out
133+
out_vals = {
134+
'unit_cost': unit_cost,
135+
'value': unit_cost * quantity * -1,
136+
'quantity': quantity * -1,
137+
}
138+
out_vals.update(common_vals)
139+
svl_vals_list.append(out_vals)
140+
_create_stock_valuation_layer(env, svl_vals_list)
141+
142+
143+
def _run_fifo_vacuum(env, product, company, std_price_update):
144+
product.ensure_one()
145+
if company is None:
146+
company = env.company
147+
svls_to_vacuum = env['stock.valuation.layer'].search([
148+
('product_id', '=', product.id),
149+
('remaining_qty', '<', 0),
150+
('stock_move_id', '!=', False),
151+
('company_id', '=', company.id),
152+
], order='create_date, id')
153+
for svl_to_vacuum in svls_to_vacuum:
154+
domain = [
155+
('company_id', '=', svl_to_vacuum.company_id.id),
156+
('product_id', '=', product.id),
157+
('remaining_qty', '>', 0),
158+
'|',
159+
('create_date', '>', svl_to_vacuum.create_date),
160+
'&', ('create_date', '=', svl_to_vacuum.create_date), ('id', '>', svl_to_vacuum.id)
161+
]
162+
candidates = env['stock.valuation.layer'].search(domain)
163+
if not candidates:
164+
break
165+
qty_to_take_on_candidates = abs(svl_to_vacuum.remaining_qty)
166+
qty_taken_on_candidates = 0
167+
tmp_value = 0
168+
for candidate in candidates:
169+
qty_taken_on_candidate = min(candidate.remaining_qty, qty_to_take_on_candidates)
170+
qty_taken_on_candidates += qty_taken_on_candidate
171+
candidate_unit_cost = candidate.remaining_value / candidate.remaining_qty
172+
value_taken_on_candidate = qty_taken_on_candidate * candidate_unit_cost
173+
value_taken_on_candidate = candidate.currency_id.round(value_taken_on_candidate)
174+
new_remaining_value = candidate.remaining_value - value_taken_on_candidate
175+
candidate_vals = {
176+
'remaining_qty': candidate.remaining_qty - qty_taken_on_candidate,
177+
'remaining_value': new_remaining_value
178+
}
179+
candidate.write(candidate_vals)
180+
qty_to_take_on_candidates -= qty_taken_on_candidate
181+
tmp_value += value_taken_on_candidate
182+
if float_is_zero(qty_to_take_on_candidates, precision_rounding=product.uom_id.rounding):
183+
break
184+
# Get the estimated value we will correct.
185+
remaining_value_before_vacuum = svl_to_vacuum.unit_cost * qty_taken_on_candidates
186+
new_remaining_qty = svl_to_vacuum.remaining_qty + qty_taken_on_candidates
187+
corrected_value = remaining_value_before_vacuum - tmp_value
188+
svl_to_vacuum.write({
189+
'remaining_qty': new_remaining_qty,
190+
})
191+
# Don't create a layer or an accounting entry if the corrected value is zero.
192+
if svl_to_vacuum.currency_id.is_zero(corrected_value):
193+
continue
194+
corrected_value = svl_to_vacuum.currency_id.round(corrected_value)
195+
move = svl_to_vacuum.stock_move_id
196+
vals = {
197+
'product_id': product.id,
198+
'value': corrected_value,
199+
'unit_cost': 0,
200+
'quantity': 0,
201+
'remaining_qty': 0,
202+
'stock_move_id': move.id,
203+
'company_id': move.company_id.id,
204+
'description': 'Revaluation of %s (negative inventory)' % move.picking_id.name or move.name,
205+
'stock_valuation_layer_id': svl_to_vacuum.id,
206+
'create_date': move.date,
207+
'create_uid': move.create_uid.id or 1,
208+
'write_date': move.date,
209+
'write_uid': move.create_uid.id or 1,
210+
}
211+
_create_stock_valuation_layer(env, [vals])
212+
# If some negative stock were fixed, we need to recompute the standard price.
213+
product = product.with_context(force_company=company.id)
214+
value_svl, quantity_svl = _compute_value_svl(env, product, company)
215+
if product.cost_method == 'average' \
216+
and not float_is_zero(quantity_svl, precision_rounding=product.uom_id.rounding):
217+
std_price_update[company.id, product.id] = value_svl / quantity_svl
218+
219+
220+
def _change_standard_price(env, move, std_price_update):
221+
""" Create stock.valuation.layer for manual updated standard_price from a product form"""
222+
product = move.product_id
223+
if product.cost_method in ('standard', 'average'):
224+
company = product.env.company
225+
last_svl = move.env['stock.valuation.layer'].search([
226+
('product_id', '=', product.id),
227+
('company_id', '=', company.id),
228+
], order='create_date desc, id desc', limit=1)
229+
if not last_svl:
230+
return
231+
value_svl, quantity_svl = _compute_value_svl(env, product, company)
232+
if not float_is_zero(quantity_svl, precision_rounding=product.uom_id.rounding):
233+
env.cr.execute("""
234+
SELECT id, company_id, product_id, datetime, cost, create_uid, write_uid, write_date
235+
FROM product_price_history
236+
WHERE company_id = %s AND product_id = %s AND datetime < %s AND datetime > %s
237+
ORDER BY company_id, product_id, datetime
238+
""", (company.id, product.id, move.date, last_svl.create_date))
239+
pph_data = env.cr.fetchall()
240+
price_at_date = last_svl.unit_cost
241+
for pph in pph_data:
242+
diff = pph[4] - price_at_date
243+
value = company.currency_id.round(quantity_svl * diff)
244+
if not company.currency_id.is_zero(value):
245+
vals = {
246+
'company_id': company.id,
247+
'product_id': product.id,
248+
'description': _('Product value manually modified (from %s to %s)') % (price_at_date, pph[4]),
249+
'value': value,
250+
'quantity': 0,
251+
'old_product_price_history_id': pph[0],
252+
'create_date': pph[3],
253+
'create_uid': pph[5] or 1,
254+
'write_uid': pph[6] or 1,
255+
'write_date': pph[7],
256+
}
257+
_create_stock_valuation_layer(env, [vals])
258+
std_price_update[company.id, product.id] = pph[4]
259+
260+
261+
def _get_price_unit(move, std_price_update):
262+
""" Returns the unit price to value this stock move """
263+
move.ensure_one()
264+
price_unit = move.price_unit
265+
# If the move is a return, use the original move's price unit.
266+
returned_svl = move.env['stock.valuation.layer'].search([
267+
('stock_move_id', '=', move.origin_returned_move_id.id),
268+
], order='create_date desc, id desc', limit=1)
269+
if move.origin_returned_move_id and returned_svl:
270+
price_unit = returned_svl.unit_cost
271+
if not move.company_id.currency_id.is_zero(price_unit):
272+
return price_unit
273+
return std_price_update.get(move.company_id.id, move.product_id.id) or move.product_id.standard_price
274+
275+
276+
def _create_stock_valuation_layer(env, vals_list):
277+
for vals in vals_list:
278+
if 'account_move_id' not in vals:
279+
vals['account_move_id'] = _get_related_account_move(env, vals).id or None
280+
columns = vals.keys()
281+
query = """
282+
INSERT INTO stock_valuation_layer ({})
283+
VALUES({})
284+
""".format(', '.join(columns), ", ".join(["%({})s".format(col) for col in columns]))
285+
env.cr.execute(query, vals)
286+
287+
288+
def _get_related_account_move(env, svl_vals):
289+
""" Return Account move related to Stock Valuation Layer"""
290+
domain = []
291+
if svl_vals.get('stock_move_id'):
292+
domain = expression.AND([domain, [('stock_move_id', '=', svl_vals['stock_move_id'])]])
293+
if svl_vals.get('old_product_price_history_id'):
294+
env.cr.execute("""
295+
SELECT create_date
296+
FROM product_price_history
297+
WHERE id = %s
298+
""", (svl_vals['old_product_price_history_id'],))
299+
create_date = env.cr.fetchone()[0]
300+
domain = expression.AND([domain, [('create_date', '=', create_date)]])
301+
if not domain:
302+
return env['account.move']
303+
account_moves = env['account.move'].search(domain)
304+
if len(account_moves) > 1:
305+
return env['account.move']
306+
return account_moves
307+
308+
309+
def _compute_value_svl(env, product, company):
310+
# We call this function because the product doesn't have in cache correct value
311+
# when we create SVL by SQL
312+
# use sql instead of ORM because it saves ~30% time
313+
env.cr.execute("""
314+
SELECT product_id, sum(value) AS value, sum(quantity) AS quantity
315+
FROM stock_valuation_layer
316+
WHERE product_id = %s AND company_id = %s
317+
GROUP BY product_id
318+
""", (product.id, company.id))
319+
result = env.cr.fetchone()
320+
if result:
321+
return company.currency_id.round(result[1]), result[2]
322+
return 0.0, 0.0
34323

35324

36325
@openupgrade.migrate()
37326
def migrate(env, version):
38-
fill_stock_valuation_layer(env)
327+
generate_stock_valuation_layer(env)
39328
openupgrade.delete_records_safely_by_xml_id(
40329
env, [
41330
"stock_account.default_cost_method",

0 commit comments

Comments
 (0)