diff --git a/product_cost_price_avco_sync/README.rst b/product_cost_price_avco_sync/README.rst new file mode 100644 index 000000000000..61fdaf283313 --- /dev/null +++ b/product_cost_price_avco_sync/README.rst @@ -0,0 +1,92 @@ +============================ +Product cost price avco sync +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f07176b3a7502ceb2553fc8c5613eb3680f27cd6fa3a28d492b55a6fcee661c1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-workflow/tree/16.0/product_cost_price_avco_sync + :alt: OCA/stock-logistics-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-workflow-16-0/stock-logistics-workflow-16-0-product_cost_price_avco_sync + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/stock-logistics-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to sync cost price products with average cost method from +stock moves price unit. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Carlos Dauden + * Sergio Teruel + * Carlos Roca + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-carlosdauden| image:: https://github.com/carlosdauden.png?size=40px + :target: https://github.com/carlosdauden + :alt: carlosdauden +.. |maintainer-sergio-teruel| image:: https://github.com/sergio-teruel.png?size=40px + :target: https://github.com/sergio-teruel + :alt: sergio-teruel + +Current `maintainers `__: + +|maintainer-carlosdauden| |maintainer-sergio-teruel| + +This module is part of the `OCA/stock-logistics-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_cost_price_avco_sync/__init__.py b/product_cost_price_avco_sync/__init__.py new file mode 100644 index 000000000000..3275ac2adf3d --- /dev/null +++ b/product_cost_price_avco_sync/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models diff --git a/product_cost_price_avco_sync/__manifest__.py b/product_cost_price_avco_sync/__manifest__.py new file mode 100644 index 000000000000..df6620d9a8e0 --- /dev/null +++ b/product_cost_price_avco_sync/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2020 Tecnativa - Carlos Dauden +# Copyright 2020 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Product cost price avco sync", + "summary": "Set product cost price from updated moves", + "version": "16.0.1.0.0", + "development_status": "Production/Stable", + "category": "Stock", + "website": "https://github.com/OCA/stock-logistics-workflow", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["carlosdauden", "sergio-teruel"], + "license": "AGPL-3", + "installable": True, + "depends": ["stock_account"], +} diff --git a/product_cost_price_avco_sync/i18n/es.po b/product_cost_price_avco_sync/i18n/es.po new file mode 100644 index 000000000000..b59b11098e7f --- /dev/null +++ b/product_cost_price_avco_sync/i18n/es.po @@ -0,0 +1,54 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_cost_price_avco_sync +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-10-29 10:28+0000\n" +"PO-Revision-Date: 2023-10-28 18:53+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: product_cost_price_avco_sync +#: code:addons/product_cost_price_avco_sync/models/stock_valuation_layer.py:0 +#, python-format +msgid "" +"More than one stock move line to assign the new stock valuation layer " +"quantity" +msgstr "" +"Más de una línea de movimiento de existencias para asignar la nueva cantidad " +"de capa de valoración de existencias" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_product_product +msgid "Product" +msgstr "Producto" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "Movimientos de Producto (Línea de Movimiento de Existencias)" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_valuation_layer +msgid "Stock Valuation Layer" +msgstr "Nivel de Valoración de Existencias" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_picking +msgid "Transfer" +msgstr "Transferir" + +#~ msgid "Stock Move" +#~ msgstr "Movimiento de existencias" + +#~ msgid "Purchase Order Line" +#~ msgstr "Línea pedido de compra" diff --git a/product_cost_price_avco_sync/i18n/it.po b/product_cost_price_avco_sync/i18n/it.po new file mode 100644 index 000000000000..c61cb582742c --- /dev/null +++ b/product_cost_price_avco_sync/i18n/it.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_cost_price_avco_sync +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-12-05 17:34+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: product_cost_price_avco_sync +#: code:addons/product_cost_price_avco_sync/models/stock_valuation_layer.py:0 +#, python-format +msgid "" +"More than one stock move line to assign the new stock valuation layer " +"quantity" +msgstr "" +"Più di una riga movimento magazzino da assegnare alla nuava quantità livello " +"valorizzazione magazzino" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_product_product +msgid "Product" +msgstr "Prodotto" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "Movimenti prodotto (riga movimento di magazzino)" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_valuation_layer +msgid "Stock Valuation Layer" +msgstr "Livello valutazione magazzino" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_picking +msgid "Transfer" +msgstr "Trasferimento" diff --git a/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot b/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot new file mode 100644 index 000000000000..f8c37cecd0cb --- /dev/null +++ b/product_cost_price_avco_sync/i18n/product_cost_price_avco_sync.pot @@ -0,0 +1,42 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_cost_price_avco_sync +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_cost_price_avco_sync +#: code:addons/product_cost_price_avco_sync/models/stock_valuation_layer.py:0 +#, python-format +msgid "" +"More than one stock move line to assign the new stock valuation layer " +"quantity" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_product_product +msgid "Product" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_valuation_layer +msgid "Stock Valuation Layer" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_picking +msgid "Transfer" +msgstr "" diff --git a/product_cost_price_avco_sync/i18n/pt_BR.po b/product_cost_price_avco_sync/i18n/pt_BR.po new file mode 100644 index 000000000000..e909f2ef5fd0 --- /dev/null +++ b/product_cost_price_avco_sync/i18n/pt_BR.po @@ -0,0 +1,48 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_cost_price_avco_sync +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-06-23 03:19+0000\n" +"Last-Translator: Fernando Colus \n" +"Language-Team: none\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 3.10\n" + +#. module: product_cost_price_avco_sync +#: code:addons/product_cost_price_avco_sync/models/stock_valuation_layer.py:0 +#, python-format +msgid "" +"More than one stock move line to assign the new stock valuation layer " +"quantity" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_product_product +msgid "Product" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_move_line +msgid "Product Moves (Stock Move Line)" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_valuation_layer +msgid "Stock Valuation Layer" +msgstr "" + +#. module: product_cost_price_avco_sync +#: model:ir.model,name:product_cost_price_avco_sync.model_stock_picking +msgid "Transfer" +msgstr "Transferência" + +#~ msgid "Stock Move" +#~ msgstr "Movimentação de Estoque" diff --git a/product_cost_price_avco_sync/models/__init__.py b/product_cost_price_avco_sync/models/__init__.py new file mode 100644 index 000000000000..ade41c572e39 --- /dev/null +++ b/product_cost_price_avco_sync/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import stock_move_line +from . import stock_picking +from . import stock_valuation_layer diff --git a/product_cost_price_avco_sync/models/stock_move_line.py b/product_cost_price_avco_sync/models/stock_move_line.py new file mode 100644 index 000000000000..d3d6e0809297 --- /dev/null +++ b/product_cost_price_avco_sync/models/stock_move_line.py @@ -0,0 +1,27 @@ +# Copyright 2019 Carlos Dauden - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + @api.model + def _create_correction_svl(self, move, diff): + if move.product_id.cost_method != "average" or self.env.context.get( + "new_stock_move_create", False + ): + return super()._create_correction_svl(move, diff) + for svl in move.stock_valuation_layer_ids: + # TODO: Review if is dropshipping + if move._is_out(): + svl.quantity -= diff + else: + svl.quantity += diff + + @api.model_create_multi + def create(self, vals_list): + return super( + StockMoveLine, self.with_context(new_stock_move_create=True) + ).create(vals_list) diff --git a/product_cost_price_avco_sync/models/stock_picking.py b/product_cost_price_avco_sync/models/stock_picking.py new file mode 100644 index 000000000000..ffae921fa8a9 --- /dev/null +++ b/product_cost_price_avco_sync/models/stock_picking.py @@ -0,0 +1,12 @@ +# Copyright 2019 Carlos Dauden - Tecnativa +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def action_done(self): + """Avoid AVCO cost price recomputation when validating picking""" + return super(StockPicking, self.with_context(skip_avco_sync=True)).action_done() diff --git a/product_cost_price_avco_sync/models/stock_valuation_layer.py b/product_cost_price_avco_sync/models/stock_valuation_layer.py new file mode 100644 index 000000000000..223f6a55f5f7 --- /dev/null +++ b/product_cost_price_avco_sync/models/stock_valuation_layer.py @@ -0,0 +1,435 @@ +# Copyright 2020 Tecnativa - Carlos Dauden +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import re +from collections import OrderedDict, defaultdict + +from odoo import _, api, models +from odoo.exceptions import ValidationError +from odoo.tools import float_compare, float_is_zero, float_round + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def _run_fifo_vacuum(self, company=None): + non_average_products = self.filtered(lambda r: r.cost_method != "average") + if non_average_products: + return super(ProductProduct, non_average_products)._run_fifo_vacuum( + company=company + ) + + +class StockValuationLayer(models.Model): + """Stock Valuation Layer""" + + _inherit = "stock.valuation.layer" + + @api.model + def create(self, vals): + svl = super().create(vals) + if vals.get("quantity", 0.0) > 0.0 and svl.product_id.cost_method == "average": + svl_remaining = self.sudo().search( + [ + ("company_id", "=", svl.company_id.id), + ("product_id", "=", svl.product_id.id), + ("remaining_qty", "<", 0.0), + ], + order="id", + limit=1, + ) + if svl_remaining: + svl.cost_price_avco_sync({}, {}) + return svl + + def write(self, vals): + """Update cost price avco""" + svl_previous_vals = defaultdict(dict) + if ("unit_cost" in vals or "quantity" in vals) and not self.env.context.get( + "skip_avco_sync" + ): + for svl in self: + for field_name in set(vals.keys()) & {"unit_cost", "quantity"}: + svl_previous_vals[svl.id][field_name] = svl[field_name] + res = super().write(vals) + if svl_previous_vals: + self.cost_price_avco_sync(vals, svl_previous_vals) + return res + + def get_svls_to_avco_sync(self): + self.ensure_one() + # return self.product_id.stock_valuation_layer_ids + domain = [ + ("company_id", "=", self.company_id.id), + ("product_id", "=", self.product_id.id), + ] + return ( + self.env["stock.valuation.layer"] + .sudo() + .search(domain, order="create_date, id") + ) + + def get_avco_svl_qty_unit_cost(self, line, vals): + self.ensure_one() + if self.id == line.id: + qty = vals.get("quantity", line.quantity) + unit_cost = vals.get("unit_cost", line.unit_cost) + else: + qty = self.quantity + unit_cost = self.unit_cost + return qty, unit_cost + + @api.model + def process_avco_svl_inventory( + self, svl, svl_dic, line, svl_previous_vals, previous_unit_cost + ): + high_decimal_precision = 8 + new_svl_qty = svl_dic["quantity"] + ( + svl_previous_vals[line.id]["quantity"] - line.quantity + ) + # Check if with the new difference the sign of the move changes + if (new_svl_qty < 0 and svl.stock_move_id.location_id.usage == "inventory") or ( + new_svl_qty > 0 and svl.stock_move_id.location_dest_id.usage == "inventory" + ): + location_aux = svl.stock_move_id.location_id + svl.stock_move_id.location_id = svl.stock_move_id.location_dest_id + svl.stock_move_id.location_dest_id = location_aux + svl.stock_move_id.move_line_ids.location_id = svl.stock_move_id.location_id + svl.stock_move_id.move_line_ids.location_dest_id = ( + svl.stock_move_id.location_dest_id + ) + # TODO: Split new_svl_qty in related stock move lines + if ( + float_compare( + abs(new_svl_qty), + svl.stock_move_id.quantity_done, + precision_digits=high_decimal_precision, + ) + != 0 + ): + if len(svl.stock_move_id.move_line_ids) > 1: + raise ValidationError( + _( + "More than one stock move line to assign the new " + "stock valuation layer quantity" + ) + ) + svl.stock_move_id.quantity_done = abs(new_svl_qty) + # Reasign qty variables + qty = new_svl_qty + svl_dic["quantity"] = new_svl_qty + svl_dic["unit_cost"] = previous_unit_cost + svl_dic["value"] = svl_dic["quantity"] * previous_unit_cost + if new_svl_qty > 0: + svl_dic["remaining_qty"] = new_svl_qty + else: + svl_dic["remaining_qty"] = 0.0 + svl_dic["remaining_value"] = svl_dic["unit_cost"] * svl_dic["remaining_qty"] + return qty + + @api.model + def update_avco_svl_values(self, svl_dic, unit_cost=None, remaining_qty=None): + if unit_cost is not None: + svl_dic["unit_cost"] = unit_cost + svl_dic["value"] = svl_dic["unit_cost"] * svl_dic["quantity"] + if remaining_qty is not None: + svl_dic["remaining_qty"] = remaining_qty + svl_dic["remaining_value"] = svl_dic["remaining_qty"] * svl_dic["unit_cost"] + + @api.model + def get_avco_svl_price( + self, previous_unit_cost, previous_qty, unit_cost, qty, total_qty + ): + return ( + (previous_unit_cost * previous_qty + unit_cost * qty) / total_qty + if total_qty + else unit_cost + ) + + def vacumm_avco_svl(self, qty, svls_dic, vacuum_dic): + self.ensure_one() + svl_dic = svls_dic[self] + vacuum_qty = qty + for svl_to_vacuum in filter( + lambda ln: ln["remaining_qty"] < 0 and ln["quantity"] < 0.0, + svls_dic.values(), + ): + if abs(svl_to_vacuum["remaining_qty"]) <= vacuum_qty: + vacuum_qty = vacuum_qty + svl_to_vacuum["remaining_qty"] + diff_qty = -svl_to_vacuum["remaining_qty"] + new_remaining_qty = 0.0 + else: + new_remaining_qty = svl_to_vacuum["remaining_qty"] + vacuum_qty + diff_qty = vacuum_qty + vacuum_qty = 0.0 + vacuum_dic[svl_to_vacuum["id"]].append( + (diff_qty, svls_dic[self]["unit_cost"]) + ) + x = 0.0 + for q, c in vacuum_dic[svl_to_vacuum["id"]]: + x += q * c + if new_remaining_qty: + x += abs(new_remaining_qty) * vacuum_dic[svl_to_vacuum["id"]][0][1] + new_unit_cost = x / abs(svl_to_vacuum["quantity"]) + # Update remaining in outgoing line + self.update_avco_svl_values( + svl_to_vacuum, unit_cost=new_unit_cost, remaining_qty=new_remaining_qty + ) + # Update remaining in incoming line + self.update_avco_svl_values(svl_dic, remaining_qty=vacuum_qty) + if vacuum_qty == 0.0: + break + + def update_remaining_avco_svl_in(self, svls_dic, vacuum_dic): + for svl in self: + svl_dic = svls_dic[svl] + svl_out_qty = svl_dic["quantity"] + for svl_in_remaining in filter( + lambda ln: ln["remaining_qty"] > 0, svls_dic.values() + ): + if abs(svl_out_qty) <= svl_in_remaining["remaining_qty"]: + new_remaining_qty = svl_in_remaining["remaining_qty"] + svl_out_qty + vacuum_dic[svl.id].append((svl_out_qty, svl_dic["unit_cost"])) + svl_out_qty = 0.0 + else: + svl_out_qty = svl_out_qty + svl_in_remaining["remaining_qty"] + vacuum_dic[svl.id].append( + (svl_in_remaining["remaining_qty"], svl_dic["unit_cost"]) + ) + new_remaining_qty = 0.0 + self.update_avco_svl_values( + svl_in_remaining, remaining_qty=new_remaining_qty + ) + if svl_out_qty == 0.0: + break + self.update_avco_svl_values(svl_dic, remaining_qty=svl_out_qty) + + @api.model + def process_avco_svl_manual_adjustements(self, svls_dic): + accumulated_qty = accumulated_value = 0.0 + for svl, svl_dic in svls_dic.items(): + if ( + not svl_dic["quantity"] + and not svl_dic["unit_cost"] + and not svl.stock_move_id + and svl.description + ): + match_price = re.findall(r"[+-]?[0-9]+\.[0-9]+\)$", svl.description) + if match_price: + standard_price = float(match_price[0][:-1]) + svl_dic["value"] = ( + standard_price * accumulated_qty + ) - accumulated_value + accumulated_qty = accumulated_qty + svl_dic["quantity"] + accumulated_value = accumulated_value + svl_dic["value"] + + @api.model + def update_avco_svl_modified(self, svls_dic, skip_avco_sync=True): + for svl, svl_dic in svls_dic.items(): + vals = {} + for field_name, new_value in svl_dic.items(): + if field_name == "id": + continue + # Currency decimal precision for values and high precision to others + elif field_name in ("unit_cost", "value", "remaining_value"): + prec_digits = svl.currency_id.decimal_places + else: + prec_digits = 8 + if svl[field_name] != 0.0 and float_is_zero( + new_value, precision_digits=prec_digits + ): + vals[field_name] = 0.0 + elif float_compare( + svl[field_name], + new_value, + precision_digits=prec_digits, + ): + vals[field_name] = new_value + # Write modified fields + if vals: + svl.with_context(skip_avco_sync=skip_avco_sync).write(vals) + + def _preprocess_main_svl_line(self): + """This method serves for doing any stuff before processing the SVL, and it + also allows to skip the line returning True. + """ + return False + + def _preprocess_rest_svl_to_sync(self, svls_dic, preprocess_svl_dic): + """This method serves for doing any stuff before processing subsequent SVLs that + are being synced, and it also allows to skip the line returning True. + """ + return False + + def cost_price_avco_sync(self, vals, svl_previous_vals): # noqa: C901 + dp_obj = self.env["decimal.precision"] + precision_qty = dp_obj.precision_get("Product Unit of Measure") + precision_price = dp_obj.precision_get("Product Price") + for line in self.sorted(key=lambda l: (l.create_date, l.id)): + bypass = line._preprocess_main_svl_line() + if ( + line.product_id.cost_method != "average" + or line.stock_valuation_layer_id + or bypass + ): + continue + previous_unit_cost = previous_qty = 0.0 + svls_to_avco_sync = line.with_context( + skip_avco_sync=True + ).get_svls_to_avco_sync() + vacuum_dic = defaultdict(list) + inventory_processed = False + unit_cost_processed = False + svls_dic = OrderedDict() + # SVLS that need to be written in a previous process before processing + # the other SVLS. + preprocess_svl_dic = OrderedDict() + for svl in svls_to_avco_sync: + if svl._preprocess_rest_svl_to_sync(svls_dic, preprocess_svl_dic): + continue + # Compatibility with landed cost + if svl.stock_valuation_layer_id: + linked_layer = svl.stock_valuation_layer_id + cost_to_add = svl.value + if cost_to_add and previous_qty: + previous_unit_cost += cost_to_add / previous_qty + svls_dic[linked_layer]["remaining_value"] += cost_to_add + continue + qty, unit_cost = svl.get_avco_svl_qty_unit_cost(line, vals) + svls_dic[svl] = { + "id": svl.id, + "quantity": qty, + "unit_cost": unit_cost, + "remaining_qty": qty, + "remaining_value": qty * unit_cost, + "value": svl.value, + } + svl_dic = svls_dic[svl] + f_compare = float_compare(qty, 0.0, precision_digits=precision_qty) + # Keep inventory unit_cost if not previous incoming or manual adjustment + if not unit_cost_processed: + previous_unit_cost = unit_cost + if f_compare > 0.0: + unit_cost_processed = True + # Adjust inventory IN and OUT + # Discard moves with a picking because they are not an inventory + if ( + ( + svl.stock_move_id.location_id.usage == "inventory" + or svl.stock_move_id.location_dest_id.usage == "inventory" + ) + and not svl.stock_move_id.picking_id + and not svl.stock_move_id.scrapped + ): + if ( + not inventory_processed + # Context to keep stock quantities after inventory qty update + and self.env.context.get("keep_avco_inventory", False) + ): + qty = self.process_avco_svl_inventory( + svl, + svl_dic, + line, + svl_previous_vals, + previous_unit_cost, + ) + inventory_processed = True + else: + svl.update_avco_svl_values( + svl_dic, unit_cost=previous_unit_cost + ) + # Check if adjust IN and we have moves to vacuum outs without stock + if svl_dic["quantity"] > 0.0 and previous_qty < 0.0: + svl.vacumm_avco_svl(qty, svls_dic, vacuum_dic) + elif svl_dic["quantity"] < 0.0: + svl.update_remaining_avco_svl_in(svls_dic, vacuum_dic) + previous_qty = previous_qty + qty + # Incoming line in layer + elif f_compare > 0: + total_qty = previous_qty + qty + # Return moves + if not svl.stock_move_id or svl.stock_move_id.move_orig_ids: + svl.update_avco_svl_values( + svl_dic, unit_cost=previous_unit_cost + ) + # Normal incoming moves + else: + unit_cost_processed = True + if previous_qty <= 0.0: + # Set income svl.unit_cost as previous_unit_cost + previous_unit_cost = unit_cost + else: + previous_unit_cost = svl.get_avco_svl_price( + previous_unit_cost, + previous_qty, + unit_cost, + qty, + total_qty, + ) + svl.update_avco_svl_values(svl_dic, remaining_qty=qty) + if previous_qty < 0: + # Vacuum previous product outs without stock + svl.vacumm_avco_svl(qty, svls_dic, vacuum_dic) + previous_qty = total_qty + # Outgoing line in layer + elif f_compare < 0: + # Normal OUT + svl.update_avco_svl_values( + svl_dic, + unit_cost=previous_unit_cost, + ) + previous_qty = previous_qty + qty + svl.update_remaining_avco_svl_in(svls_dic, vacuum_dic) + # Manual standard_price adjustment line in layer + elif ( + not unit_cost + and not qty + and not svl.stock_move_id + and svl.description + ): + unit_cost_processed = True + match_price = re.findall(r"[+-]?[0-9]+\.[0-9]+\)$", svl.description) + if match_price: + standard_price = float(match_price[0][:-1]) + # TODO: Review abs in previous_qty or new_diff + new_diff = standard_price - previous_unit_cost + svl_dic["value"] = new_diff * previous_qty + previous_unit_cost = standard_price + # elif previous_qty > 0.0: + # previous_unit_cost = ( + # previous_unit_cost * previous_qty + svl_dic["value"] + # ) / previous_qty + # Incoming or Outgoing moves without quantity and unit_cost + elif not qty and svl.stock_move_id: + svl_dic["value"] = 0.0 + line.update_avco_svl_modified(preprocess_svl_dic, skip_avco_sync=False) + # Reprocess svls to set manual adjust values take into account all vacuums + self.process_avco_svl_manual_adjustements(svls_dic) + # Update product standard price if it is modified + if float_compare( + previous_unit_cost, + line.product_id.with_company(line.company_id.id).standard_price, + precision_digits=precision_price, + ): + line.product_id.with_company(line.company_id.id).with_context( + disable_auto_svl=True + ).sudo().standard_price = float_round( + previous_unit_cost, precision_digits=precision_price + ) + # Update actual line value + svl_dic = svls_dic[line] + if svl_dic["quantity"] or svl_dic["unit_cost"]: + svl_dic["value"] = svl_dic["quantity"] * svl_dic["unit_cost"] + # Write changes in db + line.update_avco_svl_modified(svls_dic) + # Update unit_cost for incoming stock moves + if ( + line.stock_move_id + and line.stock_move_id._is_in() + and float_compare( + line.stock_move_id.price_unit, + line.unit_cost, + precision_digits=precision_price, + ) + ): + line.stock_move_id.price_unit = line.unit_cost diff --git a/product_cost_price_avco_sync/readme/CONTRIBUTORS.rst b/product_cost_price_avco_sync/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..51818bd0f957 --- /dev/null +++ b/product_cost_price_avco_sync/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* `Tecnativa `_: + + * Carlos Dauden + * Sergio Teruel + * Carlos Roca diff --git a/product_cost_price_avco_sync/readme/DESCRIPTION.rst b/product_cost_price_avco_sync/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..d800b605434b --- /dev/null +++ b/product_cost_price_avco_sync/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows to sync cost price products with average cost method from +stock moves price unit. diff --git a/product_cost_price_avco_sync/static/description/icon.png b/product_cost_price_avco_sync/static/description/icon.png new file mode 100644 index 000000000000..3a0328b516c4 Binary files /dev/null and b/product_cost_price_avco_sync/static/description/icon.png differ diff --git a/product_cost_price_avco_sync/static/description/index.html b/product_cost_price_avco_sync/static/description/index.html new file mode 100644 index 000000000000..6966069ffd43 --- /dev/null +++ b/product_cost_price_avco_sync/static/description/index.html @@ -0,0 +1,432 @@ + + + + + + +Product cost price avco sync + + + +
+

Product cost price avco sync

+ + +

Production/Stable License: AGPL-3 OCA/stock-logistics-workflow Translate me on Weblate Try me on Runboat

+

This module allows to sync cost price products with average cost method from +stock moves price unit.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:

    +
    +
      +
    • Carlos Dauden
    • +
    • Sergio Teruel
    • +
    • Carlos Roca
    • +
    +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

carlosdauden sergio-teruel

+

This module is part of the OCA/stock-logistics-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_cost_price_avco_sync/tests/__init__.py b/product_cost_price_avco_sync/tests/__init__.py new file mode 100644 index 000000000000..54427389eae2 --- /dev/null +++ b/product_cost_price_avco_sync/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_product_cost_price_avco_sync diff --git a/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py b/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py new file mode 100644 index 000000000000..d78dc5b42ca6 --- /dev/null +++ b/product_cost_price_avco_sync/tests/test_product_cost_price_avco_sync.py @@ -0,0 +1,492 @@ +# Copyright 2019 Tecnativa - Carlos Dauden +# Copyright 2019 Tecnativa - Sergio Teruel +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import logging +from time import sleep + +from odoo.tests.common import TransactionCase, tagged + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + +_logger = logging.getLogger(__name__) + + +@tagged("-at_install", "post_install") +class TestProductCostPriceAvcoSync(TransactionCase): + @classmethod + def setUpClass(cls): + super(TestProductCostPriceAvcoSync, cls).setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + cls.StockPicking = cls.env["stock.picking"] + cls.supplier_location = cls.env.ref("stock.stock_location_suppliers") + cls.customer_location = cls.env.ref("stock.stock_location_customers") + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.stock_location = cls.env.ref("stock.stock_location_stock") + cls.picking_type_in = cls.env.ref("stock.picking_type_in") + cls.picking_type_out = cls.env.ref("stock.picking_type_out") + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.categ_all = cls.env.ref("product.product_category_all") + cls.categ_all.property_cost_method = "average" + cls.product = cls.env["product.product"].create( + { + "name": "Product for test", + "type": "product", + "tracking": "none", + "standard_price": 1, + "categ_id": cls.categ_all.id, + } + ) + cls.picking_in = cls.env["stock.picking"].create( + { + "picking_type_id": cls.picking_type_in.id, + "location_id": cls.supplier_location.id, + "location_dest_id": cls.stock_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": "a move", + "product_id": cls.product.id, + "product_uom_qty": 10.0, + "product_uom": cls.product.uom_id.id, + "location_id": cls.supplier_location.id, + "location_dest_id": cls.stock_location.id, + }, + ) + ], + } + ) + + cls.picking_out = cls.env["stock.picking"].create( + { + "picking_type_id": cls.picking_type_out.id, + "location_id": cls.stock_location.id, + "location_dest_id": cls.customer_location.id, + "move_ids": [ + ( + 0, + 0, + { + "name": "a move", + "product_id": cls.product.id, + "product_uom_qty": 5.0, + "product_uom": cls.product.uom_id.id, + "location_id": cls.stock_location.id, + "location_dest_id": cls.customer_location.id, + }, + ) + ], + } + ) + + def _test_sync_cost_price(self): + move_in = self.picking_in.move_ids[:1] + move_in.product_uom_qty = 100 + move_in.price_unit = 5.0 + move_in.quantity_done = move_in.product_uom_qty + self.picking_in._action_done() + move_in.date = "2019-10-01 00:00:00" + # Why do we a sleep during 1 second after avery move validation? + # The cost_price_avco_sync method remove future product price history + # from 1 second before that the move date which has been upadated. + # If we do not apply sleep for test all price history have the same + # second so test crashes. + # In a real scenario, the product price history are created with more + # difference than 1 second. + sleep(1) + + picking_in_2 = self.picking_in.copy() + move_in_2 = picking_in_2.move_ids[:1] + move_in_2.product_uom_qty = 10.0 + move_in_2.quantity_done = move_in_2.product_uom_qty + picking_in_2._action_done() + move_in_2.date = "2019-10-02 00:00:00" + sleep(1) + + move_out = self.picking_out.move_ids[:1] + move_out.quantity_done = move_out.product_uom_qty + self.picking_out._action_done() + move_out.date = "2019-10-03 00:00:00" + + picking_out_2 = self.picking_out.copy() + move_out_2 = picking_out_2.move_ids[:1] + move_out_2.quantity_done = move_out_2.product_uom_qty + picking_out_2._action_done() + move_out_2.date = "2019-10-04 00:00:00" + + # Make an inventory + inventory = self.env["stock.inventory"].create( + { + "name": "Initial inventory", + "filter": "partial", + "location_id": self.warehouse.lot_stock_id.id, + "line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "product_qty": 200, + "location_id": self.warehouse.lot_stock_id.id, + }, + ) + ], + } + ) + inventory._action_done() + inventory.move_ids.date = "2019-10-05 00:00:00" + sleep(1) + + self.assertEqual(self.product.standard_price, 5.0) + move_in.price_unit = 2.0 + self.assertEqual(self.product.standard_price, 2.27) + self.assertAlmostEqual(move_out.price_unit, -2.27, 2) + self.assertAlmostEqual(move_out_2.price_unit, -2.27, 2) + + def _test_sync_cost_price_and_history(self): + company_id = self.picking_in.company_id.id + move_in = self.picking_in.move_ids[:1] + move_in.quantity_done = move_in.product_uom_qty + self.picking_in._action_done() + move_in.date = "2019-10-01 00:00:00" + + move_out = self.picking_out.move_ids[:1] + move_out.quantity_done = move_out.product_uom_qty + self.picking_out._action_done() + move_out.date = "2019-10-01 01:00:00" + + picking_in_2 = self.picking_in.copy() + move_in_2 = picking_in_2.move_ids[:1] + move_in_2.quantity_done = move_in_2.product_uom_qty + picking_in_2._action_done() + move_in_2.date = "2019-10-01 02:00:00" + + picking_out_2 = self.picking_out.copy() + move_out_2 = picking_out_2.move_ids[:1] + move_out_2.product_uom_qty = 15 + move_out_2.quantity_done = move_out_2.product_uom_qty + picking_out_2._action_done() + move_out_2.date = "2019-10-01 03:00:00" + + picking_in_3 = self.picking_in.copy() + move_in_3 = picking_in_3.move_ids[:1] + move_in_3.quantity_done = move_in_3.product_uom_qty + move_in_3.price_unit = 2.0 + picking_in_3._action_done() + move_in_3.date = "2019-10-01 04:00:00" + + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + self.assertAlmostEqual(self.product.get_history_price(company_id), 2.0, 2) + self.product.standard_price = 20.0 + self.assertAlmostEqual(self.product.get_history_price(company_id), 20.0, 2) + + move_in.price_unit = 10.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + self.assertAlmostEqual(move_out.price_unit, -10.0, 2) + self.assertAlmostEqual(move_out_2.price_unit, -4.0, 2) + self.assertAlmostEqual( + self.product.get_history_price( + company_id, move_in_3._previous_instant_date() + ), + 4.0, + 2, + ) + + move_in_3.quantity_done = 5.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + move_in_3.quantity_done = 0.0 + self.assertAlmostEqual(self.product.standard_price, 4.0, 2) + + (move_in | move_in_2 | move_in_3).write({"price_unit": 9.0}) + self.assertAlmostEqual(self.product.standard_price, 9.0, 2) + + svl_count = self.env["stock.valuation.layer"].search_count( + [("company_id", "=", company_id), ("product_id", "=", self.product.id)] + ) + self.assertEqual(svl_count, 4) # TODO: Miralo que no se si es así + + def _test_sync_cost_price_multi_moves_done_at_same_time(self): + move_in = self.picking_in.move_ids[:1] + move_in.product_uom_qty = 10 + move_in.price_unit = 10.0 + move_in.quantity_done = move_in.product_uom_qty + + picking_in_2 = self.picking_in.copy() + move_in_2 = picking_in_2.move_ids[:1] + move_in_2.product_uom_qty = 10.0 + move_in_2.price_unit = 5.0 + move_in_2.quantity_done = move_in_2.product_uom_qty + + self.env["stock.immediate.transfer"].create( + {"pick_ids": [(6, 0, (self.picking_in + picking_in_2).ids)]} + ).process() + (self.picking_in + picking_in_2)._action_done() + + self.assertEqual(self.product.standard_price, 7.5) + move_in_2.price_unit = 4.0 + self.assertEqual(self.product.standard_price, 7.0) + move_in.price_unit = 8.0 + self.assertEqual(self.product.standard_price, 6) + + move_in.price_unit = 10.0 + self.assertEqual(self.product.standard_price, 7.0) + move_in_2.price_unit = 5.0 + self.assertEqual(self.product.standard_price, 7.5) + + def _test_change_quantiy_price(self): + """Write quantity and price to zero in a stock valuation layer""" + self.picking_in.action_assign() + move_in = self.picking_in.move_ids[:1] + self.picking_in.move_line_ids.qty_done = move_in.product_uom_qty + self.picking_in._action_done() + + picking_in_2 = self.picking_in.copy() + picking_in_2.action_assign() + move_in_2 = picking_in_2.move_ids[:1] + move_in_2.product_uom_qty = 10.0 + move_in_2.quantity_done = move_in_2.product_uom_qty + picking_in_2._action_done() + move_in_2.stock_valuation_layer_ids.unit_cost = 2.0 + self.assertAlmostEqual(self.product.standard_price, 1.5, 2) + + # Change qty before price + move_in.stock_valuation_layer_ids.unit_cost = 0.0 + self.assertAlmostEqual(self.product.standard_price, 1.0, 2) + move_in.quantity_done = 0.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + + move_in.quantity_done = 10.0 + move_in.stock_valuation_layer_ids.unit_cost = 4.0 + self.assertAlmostEqual(self.product.standard_price, 3.0, 2) + + move_in.quantity_done = 0.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + move_in.stock_valuation_layer_ids.unit_cost = 0.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + + move_in.quantity_done = 10.0 + move_in.stock_valuation_layer_ids.unit_cost = 1.0 + self.product.with_context(import_file=True).standard_price = 6.0 + svl_manual = self.env["stock.valuation.layer"].search( + [("product_id", "=", self.product.id)], order="id DESC", limit=1 + ) + self.assertAlmostEqual(svl_manual.value, 90.0, 2) + move_in.stock_valuation_layer_ids.unit_cost = 0.0 + self.assertAlmostEqual(svl_manual.value, 100.0, 2) + + def create_picking(self, p_type="IN", qty=1.0, confirmed=True): + if p_type == "IN": + picking_type = self.picking_type_in + location_id = self.supplier_location + location_dest_id = self.stock_location + else: + picking_type = self.picking_type_out + location_id = self.stock_location + location_dest_id = self.customer_location + picking = ( + self.env["stock.picking"] + .with_context(tracking_disable=True) + .create( + { + "picking_type_id": picking_type.id, + "location_id": location_id.id, + "location_dest_id": location_dest_id.id, + "move_ids": [ + ( + 0, + 0, + { + "name": "a move", + "product_id": self.product.id, + "product_uom_qty": qty, + "product_uom": self.product.uom_id.id, + "location_id": location_id.id, + "location_dest_id": location_dest_id.id, + }, + ) + ], + } + ) + ) + if confirmed: + picking.action_assign() + move = picking.move_ids[:1] + picking.move_line_ids.qty_done = move.product_uom_qty + picking._action_done() + return picking, move + + def _test_change_quantiy_price_xx(self): + """Write quantity and price to zero in a stock valuation layer""" + picking_in_01, move_in_01 = self.create_picking("IN", 10) + quant = self.env["stock.quant"].search( + [ + ("location_id.usage", "=", "internal"), + ("product_id", "=", self.product.id), + ] + ) + picking_in_02, move_in_02 = self.create_picking("IN", 10) + move_in_02.stock_valuation_layer_ids.unit_cost = 2.0 + self.assertAlmostEqual(self.product.standard_price, 1.5, 2) + + # Change qty before price + move_in_01.stock_valuation_layer_ids.unit_cost = 0.0 + self.assertAlmostEqual(self.product.standard_price, 1.0, 2) + move_in_01.quantity_done = 0.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + + move_in_01.quantity_done = 10.0 + move_in_01.stock_valuation_layer_ids.unit_cost = 4.0 + self.assertAlmostEqual(self.product.standard_price, 3.0, 2) + + move_in_01.quantity_done = 0.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + move_in_01.stock_valuation_layer_ids.unit_cost = 0.0 + self.assertAlmostEqual(self.product.standard_price, 2.0, 2) + + move_in_01.quantity_done = 10.0 + move_in_01.stock_valuation_layer_ids.unit_cost = 1.0 + self.product.with_context(import_file=True).standard_price = 6.0 + svl_manual = self.env["stock.valuation.layer"].search( + [("product_id", "=", self.product.id)], order="id DESC", limit=1 + ) + self.assertAlmostEqual(svl_manual.value, 90.0, 2) + move_in_01.stock_valuation_layer_ids.unit_cost = 0.0 + self.assertAlmostEqual(svl_manual.value, 100.0, 2) + + # self.env.context.get('inventory_mode') + quant = self.env["stock.quant"].search( + [ + ("location_id.usage", "=", "internal"), + ("product_id", "=", self.product.id), + ] + ) + quant.inventory_quantity = 0 + + picking_out_01, move_out_01 = self.create_picking("OUT", qty=5.0) + + def test_change_quantiy_price_xx(self): + """Write quantity and price to zero in a stock valuation layer""" + # Case 1 + picking_in_01, move_in_01 = self.create_picking("IN", 10) + picking_in_02, move_in_02 = self.create_picking("IN", 10) + picking_out_01, move_out_01 = self.create_picking("OUT", qty=5.0) + quant = ( + self.env["stock.quant"] + .with_context(inventory_mode=True) + .search( + [ + ("location_id.usage", "=", "internal"), + ("product_id", "=", self.product.id), + ] + ) + ) + + self.print_svl( + "Before set move 1 unit cost to 2.0 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + + move_in_01.stock_valuation_layer_ids.unit_cost = 2.0 + self.print_svl( + "After set move 1 unit cost to 2.0 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + self.assertAlmostEqual(move_in_01.stock_valuation_layer_ids.value, 20, 2) + self.assertAlmostEqual(move_in_02.stock_valuation_layer_ids.value, 10, 2) + self.assertAlmostEqual(move_out_01.stock_valuation_layer_ids.value, -7.5, 2) + self.assertAlmostEqual(self.product.standard_price, 1.5, 2) + + # Case 2 + self.print_svl( + "Before update inventory_quantity Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + quant.inventory_quantity = 6 + self.print_svl( + "After set inventory_quantity to 6 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + picking_out_02, move_out_02 = self.create_picking("OUT", qty=10.0) + self.print_svl( + "After OUT 10 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + self.product.with_context(import_file=True).standard_price = 4.0 + self.print_svl( + "After force standard price to 4 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + picking_in_03, move_in_03 = self.create_picking("IN", 2) + self.print_svl( + "After IN 2 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + self.product.with_context(import_file=True).standard_price = 7.0 + self.print_svl( + "After force standard price to 7 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + picking_in_04, move_in_04 = self.create_picking("IN", 23) + self.print_svl( + "After IN 23 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + picking_out_03, move_out_03 = self.create_picking("OUT", 8) + self.print_svl( + "After OUT 8 Quant:{} Standard Price:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + # Change cost before quantity + move_in_01.stock_valuation_layer_ids.unit_cost = 0.0 + self.print_svl( + "After force unit cost to 0 in first IN move Quant:{}".format( + quant.quantity + ) + ) + # Change qty after cost + move_in_01.with_context(keep_avco_inventory=True).quantity_done = 0.0 + self.print_svl( + "After force quantity to 0 in first IN move Quant:{} Cost:{}".format( + quant.quantity, quant.product_id.standard_price + ) + ) + + def print_svl(self, char_info=""): + msg_list = ["{}".format(char_info)] + total_qty = total_value = 0.0 + for svl in self.env["stock.valuation.layer"].search( + [("product_id", "=", self.product.id)] + ): + total_qty += svl.quantity + total_value += svl.value + msg_list.append( + "Qty:{:.3f} Cost:{:.3f} Value:{:.3f} RemQty:{:.3f}" + " Totals: qty:{:.3f} val:{:.3f} avg:{:.3f} {}".format( + svl.quantity, + svl.unit_cost, + svl.value, + svl.remaining_qty, + total_qty, + total_value, + total_value / total_qty if total_qty else 0.0, + svl.description, + ) + ) + msg_list.append( + "Total qty: {:.3f} Total value: {:.3f} Cost average {:.3f}".format( + total_qty, total_value, (total_value / total_qty if total_qty else 0.0) + ) + ) + _logger.info("\n".join(msg_list)) diff --git a/setup/product_cost_price_avco_sync/odoo/addons/product_cost_price_avco_sync b/setup/product_cost_price_avco_sync/odoo/addons/product_cost_price_avco_sync new file mode 120000 index 000000000000..9458533e202b --- /dev/null +++ b/setup/product_cost_price_avco_sync/odoo/addons/product_cost_price_avco_sync @@ -0,0 +1 @@ +../../../../product_cost_price_avco_sync \ No newline at end of file diff --git a/setup/product_cost_price_avco_sync/setup.py b/setup/product_cost_price_avco_sync/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/product_cost_price_avco_sync/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)