|
1 | 1 | # Copyright 2020 ForgeFlow <http://www.forgeflow.com> |
| 2 | +# Copyright 2020 Andrii Skrypka |
2 | 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
3 | 4 | from openupgradelib import openupgrade |
| 5 | +from odoo import _ |
| 6 | +from odoo.osv import expression |
| 7 | +from odoo.tools.float_utils import float_is_zero |
4 | 8 |
|
5 | 9 |
|
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""" |
7 | 13 | openupgrade.logged_query( |
8 | 14 | env.cr, """ |
9 | 15 | ALTER TABLE stock_valuation_layer |
10 | 16 | ADD COLUMN old_product_price_history_id integer""", |
11 | 17 | ) |
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 |
34 | 323 |
|
35 | 324 |
|
36 | 325 | @openupgrade.migrate() |
37 | 326 | def migrate(env, version): |
38 | | - fill_stock_valuation_layer(env) |
| 327 | + generate_stock_valuation_layer(env) |
39 | 328 | openupgrade.delete_records_safely_by_xml_id( |
40 | 329 | env, [ |
41 | 330 | "stock_account.default_cost_method", |
|
0 commit comments