|
| 1 | +# Copyright 2020 ForgeFlow <http://www.forgeflow.com> |
| 2 | +# Copyright 2020 Andrii Skrypka |
| 3 | +# Copyright 2021 Tecnativa - Carlos Dauden |
| 4 | +# Copyright 2021 Tecnativa - Sergio Teruel |
| 5 | +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
| 6 | + |
| 7 | +import logging |
| 8 | + |
| 9 | +from openupgradelib import openupgrade |
| 10 | +from odoo import _ |
| 11 | +from odoo.tools.float_utils import float_compare, float_is_zero, float_round |
| 12 | +from odoo.addons.base.models.ir_model import query_insert |
| 13 | + |
| 14 | +_logger = logging.getLogger(__name__) |
| 15 | + |
| 16 | +# Declare global variant to avoid that it is passed between methods |
| 17 | +precision_price = 0 |
| 18 | + |
| 19 | + |
| 20 | +def _prepare_common_svl_vals(move, product): |
| 21 | + return { |
| 22 | + "create_uid": move["write_uid"], |
| 23 | + "create_date": move["date"], |
| 24 | + "write_uid": move["write_uid"], |
| 25 | + "write_date": move["date"], |
| 26 | + "stock_move_id": move["id"], |
| 27 | + "company_id": move["company_id"], |
| 28 | + "product_id": move["product_id"], |
| 29 | + "description": move["reference"] and "%s - %s" % (move["reference"], product.name) or product.name, |
| 30 | + "value": 0.0, |
| 31 | + "unit_cost": 0.0, |
| 32 | + "remaining_qty": 0.0, |
| 33 | + "remaining_value": 0.0, |
| 34 | + "quantity": 0.0, |
| 35 | + "old_product_price_history_id": None, |
| 36 | + "account_move_id": move["account_move_id"], |
| 37 | + } |
| 38 | + |
| 39 | + |
| 40 | +def _prepare_in_svl_vals(move, quantity, unit_cost, product, is_dropship): |
| 41 | + vals = _prepare_common_svl_vals(move, product) |
| 42 | + vals.update({ |
| 43 | + "value": float_round(unit_cost * quantity, precision_digits=precision_price), |
| 44 | + "unit_cost": unit_cost, |
| 45 | + "quantity": quantity, |
| 46 | + }) |
| 47 | + if product.cost_method in ("average", "fifo") and not is_dropship: |
| 48 | + vals["remaining_qty"] = quantity |
| 49 | + vals["remaining_value"] = vals["value"] |
| 50 | + return vals |
| 51 | + |
| 52 | + |
| 53 | +def _prepare_out_svl_vals(move, quantity, unit_cost, product): |
| 54 | + # Quantity is negative for out valuation layers. |
| 55 | + quantity = -quantity |
| 56 | + vals = _prepare_common_svl_vals(move, product) |
| 57 | + vals.update({ |
| 58 | + "value": float_round(unit_cost * quantity, precision_digits=precision_price), |
| 59 | + "unit_cost": unit_cost, |
| 60 | + "quantity": quantity, |
| 61 | + "remaining_qty": 0.0, |
| 62 | + "remaining_value": 0.0, |
| 63 | + }) |
| 64 | + return vals |
| 65 | + |
| 66 | + |
| 67 | +def _prepare_man_svl_vals(price_history_rec, previous_price, quantity, company, product): |
| 68 | + diff = price_history_rec["cost"] - previous_price |
| 69 | + value = float_round(diff * quantity, precision_digits=precision_price) |
| 70 | + svl_vals = { |
| 71 | + "create_uid": price_history_rec["write_uid"], |
| 72 | + "create_date": price_history_rec["datetime"], |
| 73 | + "write_uid": price_history_rec["write_uid"], |
| 74 | + "write_date": price_history_rec["datetime"], |
| 75 | + "stock_move_id": None, |
| 76 | + "company_id": company.id, |
| 77 | + "product_id": product.id, |
| 78 | + "description": _("Product value manually modified (from %s to %s)" |
| 79 | + ) % (previous_price, price_history_rec["cost"]), |
| 80 | + "value": value, |
| 81 | + "unit_cost": 0.0, |
| 82 | + "remaining_qty": 0.0, |
| 83 | + "remaining_value": 0.0, |
| 84 | + "quantity": 0.0, |
| 85 | + "old_product_price_history_id": price_history_rec["id"], |
| 86 | + "account_move_id": price_history_rec["account_move_id"], |
| 87 | + } |
| 88 | + return svl_vals |
| 89 | + |
| 90 | + |
| 91 | +def get_product_price_history(env, company_id, product_id): |
| 92 | + env.cr.execute(""" |
| 93 | + WITH account_move_rel AS ( |
| 94 | + SELECT id, create_date |
| 95 | + FROM ( |
| 96 | + SELECT id, create_date, COUNT(*) OVER(PARTITION BY create_date) AS qty |
| 97 | + FROM account_move |
| 98 | + WHERE stock_move_id IS NULL |
| 99 | + ) foo |
| 100 | + WHERE qty = 1 |
| 101 | + ) |
| 102 | + SELECT pph.id, pph.company_id, pph.product_id, pph.datetime, pph.cost, rel.id AS account_move_id, |
| 103 | + pph.create_uid, pph.create_date, pph.write_uid, pph.write_date |
| 104 | + FROM product_price_history pph |
| 105 | + LEFT JOIN account_move_rel rel ON rel.create_date = pph.create_date |
| 106 | + WHERE pph.company_id = %s AND pph.product_id = %s |
| 107 | + ORDER BY pph.datetime, pph.id |
| 108 | + """, (company_id, product_id)) |
| 109 | + return env.cr.dictfetchall() |
| 110 | + |
| 111 | + |
| 112 | +def get_stock_moves(env, company_id, product_id): |
| 113 | + env.cr.execute(""" |
| 114 | + WITH account_move_rel AS ( |
| 115 | + SELECT id, stock_move_id |
| 116 | + FROM ( |
| 117 | + SELECT id, stock_move_id, COUNT(*) OVER(PARTITION BY stock_move_id) AS qty |
| 118 | + FROM account_move |
| 119 | + WHERE stock_move_id IS NOT NULL |
| 120 | + ) foo |
| 121 | + WHERE qty = 1 |
| 122 | + ) |
| 123 | + SELECT sm.id, sm.company_id, sm.product_id, sm.date, sm.product_qty, sm.reference, |
| 124 | + COALESCE(sm.price_unit, 0.0) AS price_unit, rel.id AS account_move_id, |
| 125 | + sm.create_uid, sm.create_date, sm.write_uid, sm.write_date, |
| 126 | + CASE WHEN (sl.usage <> 'internal' AND (sl.usage <> 'transit' OR sl.company_id <> sm.company_id)) |
| 127 | + AND (sld.usage = 'internal' OR (sld.usage = 'transit' AND sld.company_id = sm.company_id)) |
| 128 | + THEN 'in' |
| 129 | + WHEN (sl.usage = 'internal' OR (sl.usage = 'transit' AND sl.company_id = sm.company_id)) |
| 130 | + AND (sld.usage <> 'internal' AND (sld.usage <> 'transit' OR sld.company_id <> sm.company_id)) |
| 131 | + THEN 'out' |
| 132 | + WHEN sl.usage = 'supplier' AND sld.usage = 'customer' THEN 'dropship' |
| 133 | + WHEN sl.usage = 'customer' AND sld.usage = 'supplier' THEN 'dropship_return' |
| 134 | + ELSE 'other' |
| 135 | + END AS move_type |
| 136 | + FROM stock_move sm |
| 137 | + LEFT JOIN stock_location sl ON sl.id = sm.location_id |
| 138 | + LEFT JOIN stock_location sld ON sld.id = sm.location_dest_id |
| 139 | + LEFT JOIN account_move_rel rel ON rel.stock_move_id = sm.id |
| 140 | + WHERE sm.company_id = %s AND sm.product_id = %s AND state = 'done' |
| 141 | + ORDER BY sm.date, sm.id |
| 142 | + """, (company_id, product_id)) |
| 143 | + return env.cr.dictfetchall() |
| 144 | + |
| 145 | + |
| 146 | +@openupgrade.logging() |
| 147 | +def generate_stock_valuation_layer(env): |
| 148 | + openupgrade.logged_query( |
| 149 | + env.cr, """ |
| 150 | + ALTER TABLE stock_valuation_layer |
| 151 | + ADD COLUMN old_product_price_history_id integer""", |
| 152 | + ) |
| 153 | + company_obj = env["res.company"] |
| 154 | + product_obj = env["product.product"] |
| 155 | + # Needed to modify global variable |
| 156 | + global precision_price |
| 157 | + precision_price = env["decimal.precision"].precision_get("Product Price") |
| 158 | + precision_uom = env["decimal.precision"].precision_get( |
| 159 | + "Product Unit of Measure" |
| 160 | + ) |
| 161 | + companies = company_obj.search([]) |
| 162 | + products = product_obj.with_context(active_test=False).search([("type", "in", ("product", "consu"))]) |
| 163 | + all_svl_list = [] |
| 164 | + for product in products: |
| 165 | + for company in companies: |
| 166 | + history_lines = get_product_price_history(env, company.id, product.id) |
| 167 | + moves = get_stock_moves(env, company.id, product.id) |
| 168 | + svl_in_vals_list = [] |
| 169 | + svl_out_vals_list = [] |
| 170 | + svl_man_vals_list = [] |
| 171 | + svl_in_index = 0 |
| 172 | + h_index = 0 |
| 173 | + previous_price = 0.0 |
| 174 | + previous_qty = 0.0 |
| 175 | + for move in moves: |
| 176 | + is_dropship = True if move["move_type"] in ("dropship", "dropship_return") else False |
| 177 | + if product.cost_method in ("average", "standard"): |
| 178 | + # useless for Fifo because we have price unit in stock.move |
| 179 | + # Add manual adjusts |
| 180 | + have_qty = not float_is_zero(previous_qty, precision_digits=precision_uom) |
| 181 | + while h_index < len(history_lines) and history_lines[h_index]["datetime"] < move["date"]: |
| 182 | + price_history_rec = history_lines[h_index] |
| 183 | + if float_compare(price_history_rec["cost"], previous_price, precision_digits=precision_price): |
| 184 | + if have_qty: |
| 185 | + svl_vals = _prepare_man_svl_vals( |
| 186 | + price_history_rec, previous_price, previous_qty, company, product) |
| 187 | + svl_man_vals_list.append(svl_vals) |
| 188 | + previous_price = price_history_rec["cost"] |
| 189 | + h_index += 1 |
| 190 | + # Add in svl |
| 191 | + if move["move_type"] == "in" or is_dropship: |
| 192 | + total_qty = previous_qty + move["product_qty"] |
| 193 | + # TODO: is needed vaccum if total_qty is negative? |
| 194 | + if float_is_zero(total_qty, precision_digits=precision_uom): |
| 195 | + previous_price = move["price_unit"] |
| 196 | + else: |
| 197 | + previous_price = float_round( |
| 198 | + (previous_price * previous_qty + move["price_unit"] * move["product_qty"]) / total_qty, |
| 199 | + precision_digits=precision_price) |
| 200 | + svl_vals = _prepare_in_svl_vals( |
| 201 | + move, move["product_qty"], move["price_unit"], product, is_dropship) |
| 202 | + svl_in_vals_list.append(svl_vals) |
| 203 | + previous_qty = total_qty |
| 204 | + # Add out svl |
| 205 | + if move["move_type"] == "out" or is_dropship: |
| 206 | + qty = move["product_qty"] |
| 207 | + if product.cost_method in ("average", "fifo") and not is_dropship: |
| 208 | + # Reduce remaininig qty in svl of type "in" |
| 209 | + while qty > 0 and svl_in_index < len(svl_in_vals_list): |
| 210 | + if svl_in_vals_list[svl_in_index]["remaining_qty"] >= qty: |
| 211 | + candidate_cost = (svl_in_vals_list[svl_in_index]["remaining_value"] / |
| 212 | + svl_in_vals_list[svl_in_index]["remaining_qty"]) |
| 213 | + svl_in_vals_list[svl_in_index]["remaining_qty"] -= qty |
| 214 | + svl_in_vals_list[svl_in_index]["remaining_value"] = float_round( |
| 215 | + candidate_cost * svl_in_vals_list[svl_in_index]["remaining_qty"], |
| 216 | + precision_digits=precision_price) |
| 217 | + qty = 0 |
| 218 | + else: |
| 219 | + qty -= svl_in_vals_list[svl_in_index]["remaining_qty"] |
| 220 | + svl_in_vals_list[svl_in_index]["remaining_qty"] = 0.0 |
| 221 | + svl_in_vals_list[svl_in_index]["remaining_value"] = 0.0 |
| 222 | + svl_in_index += 1 |
| 223 | + if product.cost_method == 'fifo': |
| 224 | + svl_vals = _prepare_out_svl_vals( |
| 225 | + move, move["product_qty"], move["price_unit"], product) |
| 226 | + else: |
| 227 | + svl_vals = _prepare_out_svl_vals( |
| 228 | + move, move["product_qty"], previous_price, product) |
| 229 | + svl_out_vals_list.append(svl_vals) |
| 230 | + previous_qty -= move["product_qty"] |
| 231 | + # Add manual adjusts after last move |
| 232 | + if product.cost_method in ("average", "standard") and not float_is_zero( |
| 233 | + previous_qty, precision_digits=precision_uom): |
| 234 | + # useless for Fifo because we have price unit on product form |
| 235 | + while h_index < len(history_lines): |
| 236 | + price_history_rec = history_lines[h_index] |
| 237 | + if float_compare(price_history_rec["cost"], previous_price, precision_digits=precision_price): |
| 238 | + svl_vals = _prepare_man_svl_vals( |
| 239 | + price_history_rec, previous_price, previous_qty, company, product) |
| 240 | + svl_man_vals_list.append(svl_vals) |
| 241 | + previous_price = price_history_rec["cost"] |
| 242 | + h_index += 1 |
| 243 | + all_svl_list.extend(svl_in_vals_list + svl_out_vals_list + svl_man_vals_list) |
| 244 | + if all_svl_list: |
| 245 | + all_svl_list = sorted(all_svl_list, key=lambda k: (k["create_date"])) |
| 246 | + _logger.info("To create {} svl records".format(len(all_svl_list))) |
| 247 | + query_insert(env.cr, "stock_valuation_layer", all_svl_list) |
| 248 | + |
| 249 | + |
| 250 | +@openupgrade.migrate() |
| 251 | +def migrate(env, version): |
| 252 | + generate_stock_valuation_layer(env) |
| 253 | + openupgrade.delete_records_safely_by_xml_id( |
| 254 | + env, [ |
| 255 | + "stock_account.default_cost_method", |
| 256 | + "stock_account.default_valuation", |
| 257 | + "stock_account.property_stock_account_input_prd", |
| 258 | + "stock_account.property_stock_account_output_prd", |
| 259 | + ] |
| 260 | + ) |
0 commit comments