Skip to content

Commit a154932

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

File tree

1 file changed

+311
-24
lines changed

1 file changed

+311
-24
lines changed

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

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

35322

36323
@openupgrade.migrate()
37324
def migrate(env, version):
38-
fill_stock_valuation_layer(env)
325+
generate_stock_valuation_layer(env)
39326
openupgrade.delete_records_safely_by_xml_id(
40327
env, [
41328
"stock_account.default_cost_method",

0 commit comments

Comments
 (0)