diff --git a/setup/stock_quant_history/odoo/addons/stock_quant_history b/setup/stock_quant_history/odoo/addons/stock_quant_history new file mode 120000 index 0000000..a6191c9 --- /dev/null +++ b/setup/stock_quant_history/odoo/addons/stock_quant_history @@ -0,0 +1 @@ +../../../../stock_quant_history \ No newline at end of file diff --git a/setup/stock_quant_history/setup.py b/setup/stock_quant_history/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/stock_quant_history/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_quant_history/README.rst b/stock_quant_history/README.rst new file mode 100644 index 0000000..d43668a --- /dev/null +++ b/stock_quant_history/README.rst @@ -0,0 +1,134 @@ +=================== +Stock Quant History +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a81c78e86cec372373beaecfdb7620d6cf9f418a37830e40f3e7dce8b5ece712 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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--reporting-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-reporting/tree/14.0/stock_quant_history + :alt: OCA/stock-logistics-reporting +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-reporting-14-0/stock-logistics-reporting-14-0-stock_quant_history + :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-reporting&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows regenerate stock.quant as it was for a given date. + +All stock quant history re-generated for a given date are called snapshot. + +To generate the first snapshot this module assume all `stock.move.line` +are present in the database. + +Next snapshot is computed based on the previous snapshot present in the database. + + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Generate a new stock snapshot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Go to: *Inventory / Reporting / History / Stock snapshot* +* choose the date you want to re-generate stock quants +* click on Generate + +Consult stock quant for a given snapshot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Go to: *Inventory / Reporting / History / Stock snapshot* +* select the existing snapshot to open the form view +* click on smart button to display quants at that time + +Compare stock over snapshots +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Go to: *Inventory / Reporting / History / Stock snapshot* +* In tree view select at least 2 snapshots +* Click on *Action / Compare stocks* +* You'll be redirected to the stock quant tree view for selected snapshots + +or + +* Go to: *Inventory / Reporting / History / Stock quants* +* use different filters / group and views to make your analysis + +Known issues / Roadmap +====================== + +Short terms +~~~~~~~~~~~ + +* Add a companion module stock_quant_history_account + to make the glue between stock_quant_history and stock_account adding + the stock value + +Long terms +~~~~~~~~~~ + +* Add filters (by locations, by product...) while generating + tight snapshots (not reused as based snapshot) +* add owner and package fields + +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 +~~~~~~~ + +* Pierre Verkest + +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-petrus-v| image:: https://github.com/petrus-v.png?size=40px + :target: https://github.com/petrus-v + :alt: petrus-v + +Current `maintainer `__: + +|maintainer-petrus-v| + +This module is part of the `OCA/stock-logistics-reporting `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_quant_history/__init__.py b/stock_quant_history/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/stock_quant_history/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_quant_history/__manifest__.py b/stock_quant_history/__manifest__.py new file mode 100644 index 0000000..abc87eb --- /dev/null +++ b/stock_quant_history/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2024 Foodles (https://www.foodles.co/). +# @author Pierre Verkest +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Stock Quant History", + "summary": "Re-generate stock quants for given date", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "author": "Pierre Verkest , Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-reporting", + "depends": ["stock"], + "maintainers": [ + "petrus-v", + ], + "data": [ + "security/ir.model.access.csv", + "views/stock-quant-history-snapshot.xml", + "views/stock-quant-history.xml", + ], +} diff --git a/stock_quant_history/i18n/fr.po b/stock_quant_history/i18n/fr.po new file mode 100644 index 0000000..2db9280 --- /dev/null +++ b/stock_quant_history/i18n/fr.po @@ -0,0 +1,281 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_quant_history +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_form +msgid "History details" +msgstr "Stocks" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history_snapshot__previous_snapshot_id +msgid "Base snapshot used to generate this snapshot" +msgstr "Cliché de Stock de base utilisé pour la génération de ce cliché" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__company_id +msgid "Company" +msgstr "Société" + +#. module: stock_quant_history +#: model:ir.actions.server,name:stock_quant_history.action_multi_related_stock_quant_history_tree_view +msgid "Compare stocks" +msgstr "Comparer les stocks des clichés sélectionnés" + +#. module: stock_quant_history +#: model_terms:ir.actions.act_window,help:stock_quant_history.action_stock_quant_history_snapshot +msgid "Create your first snapshot!" +msgstr "Créer votre premièr cliché !" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__create_uid +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__create_date +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history_snapshot__generated_date +msgid "Date when stock.quant.history line have been created." +msgstr "Date de génération du cliché de stock." + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__product_uom_id +msgid "Default unit of measure used for all stock operations." +msgstr "" +"Unité de mesure par défaut utilisée pour toutes les opérations de stock" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__display_name +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__display_name +msgid "Display Name" +msgstr "Libellé" + +#. module: stock_quant_history +#: model:ir.model.fields.selection,name:stock_quant_history.selection__stock_quant_history_snapshot__state__draft +msgid "Draft" +msgstr "Brouillon" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_form +msgid "Generate" +msgstr "Générer" + +#. module: stock_quant_history +#: model_terms:ir.actions.act_window,help:stock_quant_history.action_stock_quant_history +msgid "Generate stock quant history from stock quant history snapshot before!" +msgstr "Génère les lignes depuis les historiques de cliché de stock !" + +#. module: stock_quant_history +#: model:ir.model.fields.selection,name:stock_quant_history.selection__stock_quant_history_snapshot__state__generated +msgid "Generated" +msgstr "Généré" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__generated_date +msgid "Generated date" +msgstr "Date de génération" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history_snapshot__stock_quant_history_ids +msgid "Generated stock quant history for current snapshot settings." +msgstr "Lignes d'historique de quantités de stock pour ce cliché" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_search +msgid "Group By" +msgstr "Groupé par" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__id +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__id +msgid "ID" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__inventory_date +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__inventory_date +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_search +msgid "Inventory date" +msgstr "Date du stock" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history____last_update +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__write_uid +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__write_uid +msgid "Last Updated by" +msgstr "Dernière modification par" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__write_date +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__write_date +msgid "Last Updated on" +msgstr "Dernière modification le" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__company_id +msgid "Let this field empty if this location is shared between companies" +msgstr "" +"Laissez ce champ vide si cet emplacement est partagé par plusieurs sociétés" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__location_id +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Location" +msgstr "Emplacement" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Location and sub-locations of" +msgstr "Emplacement et ses enfants" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Lot" +msgstr "Lot" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__lot_id +msgid "Lot/Serial Number" +msgstr "Lot/N° de série" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__name +msgid "Name" +msgstr "Nom" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__product_id +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Product" +msgstr "Produit" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__product_tmpl_id +msgid "Product Template" +msgstr "Modèle de produit" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__quantity +msgid "Quantity" +msgstr "Quantité" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__quantity +msgid "" +"Quantity of products in this quant, in the default unit of measure of the " +"product" +msgstr "" +"Quantité d'articles dans ce quant, dans l'unité de mesure par défaut de " +"l'article" + +#. module: stock_quant_history +#: model:ir.ui.menu,name:stock_quant_history.menu_action_stock_quant_history_snapshot +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Snapshot" +msgstr "Cliché" + +#. module: stock_quant_history +#: code:addons/stock_quant_history/models/stock_quant_history_snapshot.py:0 +#, python-format +msgid "Snapshot %s" +msgstr "Cliché du %s" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__previous_snapshot_id +msgid "Snapshot base" +msgstr "Cliché de base" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__snapshot_id +msgid "Snapshot settings" +msgstr "Cliché de Stock" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__snapshot_id +msgid "Snapshot settings used to generate this line" +msgstr "Configuration du cliché de stock générant cette ligne d'historique." + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__state +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_search +msgid "Status" +msgstr "État" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_search +msgid "Stock date" +msgstr "date du stock" + +#. module: stock_quant_history +#: model:ir.ui.menu,name:stock_quant_history.menu_action_stock_history +#: model:ir.ui.menu,name:stock_quant_history.menu_action_stock_quant_history +msgid "Stock history" +msgstr "Historique de stock" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__stock_quant_history_ids +msgid "Stock quant history" +msgstr "Historique de quantité de stock" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_pivot +msgid "Stock quants" +msgstr "Quantités de stock" + +#. module: stock_quant_history +#: model:ir.model,name:stock_quant_history.model_stock_quant_history +msgid "Stock quants history" +msgstr "Historique de quantité de stock" + +#. module: stock_quant_history +#: model:ir.actions.act_window,name:stock_quant_history.action_stock_quant_history +#: model:ir.actions.act_window,name:stock_quant_history.action_stock_quant_history_snapshot +msgid "Stock snapshot" +msgstr "Cliché de stock" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__inventory_date +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history_snapshot__inventory_date +msgid "" +"The date used to create stock.quant.history as it was for the given date" +msgstr "" +"Date utilisé pour créer l'historique des quantité de stock tell qu'il " +"l'était à cette date." + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__product_uom_id +msgid "Unit of Measure" +msgstr "Unité de mesure" + +#. module: stock_quant_history +#: model:ir.model,name:stock_quant_history.model_stock_quant_history_snapshot +msgid "stock.quant.history generation configuration model" +msgstr "" +"Cliché de stock: Modèle de configuration pour la génération des ligne de " +"stock.quant.history" diff --git a/stock_quant_history/i18n/stock_quant_history.pot b/stock_quant_history/i18n/stock_quant_history.pot new file mode 100644 index 0000000..8784f46 --- /dev/null +++ b/stock_quant_history/i18n/stock_quant_history.pot @@ -0,0 +1,272 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * stock_quant_history +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\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: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_form +msgid "History details" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history_snapshot__previous_snapshot_id +msgid "Base snapshot used to generate this snapshot" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__company_id +msgid "Company" +msgstr "" + +#. module: stock_quant_history +#: model:ir.actions.server,name:stock_quant_history.action_multi_related_stock_quant_history_tree_view +msgid "Compare stocks" +msgstr "" + +#. module: stock_quant_history +#: model_terms:ir.actions.act_window,help:stock_quant_history.action_stock_quant_history_snapshot +msgid "Create your first snapshot!" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__create_uid +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__create_uid +msgid "Created by" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__create_date +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__create_date +msgid "Created on" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history_snapshot__generated_date +msgid "Date when stock.quant.history line have been created." +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__product_uom_id +msgid "Default unit of measure used for all stock operations." +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__display_name +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__display_name +msgid "Display Name" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields.selection,name:stock_quant_history.selection__stock_quant_history_snapshot__state__draft +msgid "Draft" +msgstr "" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_form +msgid "Generate" +msgstr "" + +#. module: stock_quant_history +#: model_terms:ir.actions.act_window,help:stock_quant_history.action_stock_quant_history +msgid "Generate stock quant history from stock quant history snapshot before!" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields.selection,name:stock_quant_history.selection__stock_quant_history_snapshot__state__generated +msgid "Generated" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__generated_date +msgid "Generated date" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history_snapshot__stock_quant_history_ids +msgid "Generated stock quant history for current snapshot settings." +msgstr "" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_search +msgid "Group By" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__id +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__id +msgid "ID" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__inventory_date +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__inventory_date +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_search +msgid "Inventory date" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history____last_update +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot____last_update +msgid "Last Modified on" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__write_uid +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__write_date +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__write_date +msgid "Last Updated on" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__company_id +msgid "Let this field empty if this location is shared between companies" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__location_id +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Location" +msgstr "" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Location and sub-locations of" +msgstr "" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Lot" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__lot_id +msgid "Lot/Serial Number" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__name +msgid "Name" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__product_id +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Product" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__product_tmpl_id +msgid "Product Template" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__quantity +msgid "Quantity" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__quantity +msgid "" +"Quantity of products in this quant, in the default unit of measure of the " +"product" +msgstr "" + +#. module: stock_quant_history +#: model:ir.ui.menu,name:stock_quant_history.menu_action_stock_quant_history_snapshot +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +msgid "Snapshot" +msgstr "" + +#. module: stock_quant_history +#: code:addons/stock_quant_history/models/stock_quant_history_snapshot.py:0 +#, python-format +msgid "Snapshot %s" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__previous_snapshot_id +msgid "Snapshot base" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__snapshot_id +msgid "Snapshot settings" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__snapshot_id +msgid "Snapshot settings used to generate this line" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__state +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_search +msgid "Status" +msgstr "" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_search +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_snapshot_search +msgid "Stock date" +msgstr "" + +#. module: stock_quant_history +#: model:ir.ui.menu,name:stock_quant_history.menu_action_stock_history +#: model:ir.ui.menu,name:stock_quant_history.menu_action_stock_quant_history +msgid "Stock history" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history_snapshot__stock_quant_history_ids +msgid "Stock quant history" +msgstr "" + +#. module: stock_quant_history +#: model_terms:ir.ui.view,arch_db:stock_quant_history.view_stock_quant_history_pivot +msgid "Stock quants" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model,name:stock_quant_history.model_stock_quant_history +msgid "Stock quants history" +msgstr "" + +#. module: stock_quant_history +#: model:ir.actions.act_window,name:stock_quant_history.action_stock_quant_history +#: model:ir.actions.act_window,name:stock_quant_history.action_stock_quant_history_snapshot +msgid "Stock snapshot" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history__inventory_date +#: model:ir.model.fields,help:stock_quant_history.field_stock_quant_history_snapshot__inventory_date +msgid "" +"The date used to create stock.quant.history as it was for the given date" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model.fields,field_description:stock_quant_history.field_stock_quant_history__product_uom_id +msgid "Unit of Measure" +msgstr "" + +#. module: stock_quant_history +#: model:ir.model,name:stock_quant_history.model_stock_quant_history_snapshot +msgid "stock.quant.history generation configuration model" +msgstr "" diff --git a/stock_quant_history/models/__init__.py b/stock_quant_history/models/__init__.py new file mode 100644 index 0000000..5fcab14 --- /dev/null +++ b/stock_quant_history/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_quant_history_snapshot +from . import stock_quant_history diff --git a/stock_quant_history/models/stock_quant_history.py b/stock_quant_history/models/stock_quant_history.py new file mode 100644 index 0000000..7c9bd05 --- /dev/null +++ b/stock_quant_history/models/stock_quant_history.py @@ -0,0 +1,73 @@ +# Copyright 2024 Foodles (https://www.foodles.co/). +# @author Pierre Verkest +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class StockQuantHistory(models.Model): + _name = "stock.quant.history" + _description = "Stock quants history" + _order = "snapshot_id, inventory_date, product_id, lot_id, location_id" + snapshot_id = fields.Many2one( + comodel_name="stock.quant.history.snapshot", + ondelete="cascade", + required=True, + index=True, + string="Snapshot settings", + help="Snapshot settings used to generate this line", + ) + inventory_date = fields.Datetime( + related="snapshot_id.inventory_date", + index=True, + store=True, + ) + + # same fields as stock.quant + product_id = fields.Many2one( + "product.product", + "Product", + ondelete="restrict", + readonly=True, + required=True, + index=True, + check_company=True, + ) + product_tmpl_id = fields.Many2one( + "product.template", + string="Product Template", + related="product_id.product_tmpl_id", + readonly=True, + ) + product_uom_id = fields.Many2one( + "uom.uom", "Unit of Measure", readonly=True, related="product_id.uom_id" + ) + company_id = fields.Many2one( + related="location_id.company_id", string="Company", store=True, readonly=True + ) + location_id = fields.Many2one( + "stock.location", + "Location", + auto_join=True, + ondelete="restrict", + readonly=True, + required=True, + index=True, + check_company=True, + ) + lot_id = fields.Many2one( + "stock.production.lot", + "Lot/Serial Number", + index=True, + ondelete="restrict", + readonly=True, + check_company=True, + ) + quantity = fields.Float( + "Quantity", + help=( + "Quantity of products in this quant, " + "in the default unit of measure of the product" + ), + readonly=True, + ) diff --git a/stock_quant_history/models/stock_quant_history_snapshot.py b/stock_quant_history/models/stock_quant_history_snapshot.py new file mode 100644 index 0000000..f9eb140 --- /dev/null +++ b/stock_quant_history/models/stock_quant_history_snapshot.py @@ -0,0 +1,201 @@ +# Copyright 2024 Foodles (https://www.foodles.co/). +# @author Pierre Verkest +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging +from collections import defaultdict + +from odoo import _, api, fields, models, tools +from odoo.osv.expression import AND + +_logger = logging.getLogger(__name__) + + +class DefaultDict(defaultdict): + def __missing__(self, key): + self[key] = self.default_factory(*key) + return self[key] + + +class StockQuantHistorySnapshot(models.Model): + _name = "stock.quant.history.snapshot" + _description = "stock.quant.history generation configuration model" + _order = "inventory_date desc" + + name = fields.Char( + compute="_compute_name", + ) + stock_quant_history_ids = fields.One2many( + comodel_name="stock.quant.history", + inverse_name="snapshot_id", + string="Stock quant history", + help="Generated stock quant history for current snapshot settings.", + ) + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("generated", "Generated"), + ], + string="Status", + copy=False, + default="draft", + readonly=True, + required=True, + ) + + inventory_date = fields.Datetime( + string="Inventory date", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + help="The date used to create stock.quant.history as it was for the given date", + ) + generated_date = fields.Datetime( + string="Generated date", + readonly=True, + copy=False, + help="Date when stock.quant.history line have been created.", + ) + previous_snapshot_id = fields.Many2one( + comodel_name="stock.quant.history.snapshot", + string="Snapshot base", + readonly=True, + help="Base snapshot used to generate this snapshot", + ) + + @api.depends("inventory_date") + def _compute_name(self): + # Odoo enforce users to be linked to an active lang + lang = self.env["res.lang"]._lang_get(self.env.user.lang) + dt_format = lang.date_format + " " + lang.time_format + + for rec in self: + rec.name = _("Snapshot %s") % (rec.inventory_date.strftime(dt_format)) + + def action_generate_stock_quant_history(self): + for snapshot in self: + snapshot._generate_stock_quant_history() + + def _prepare_stock_move_line_filter(self, previous_quant_snapshot): + domain = [ + ("state", "=", "done"), + ("date", "<=", self.inventory_date), + ("product_id.type", "=", "product"), + ] + if previous_quant_snapshot.exists(): + domain = AND( + [domain, [("date", ">", previous_quant_snapshot.inventory_date)]] + ) + return domain + + @api.model + def _ignored_location_usage(self): + """If you overwrite or change this + list you'll probably want to regenerate all your + snapshots""" + return [ + "supplier", + "customer", + "inventory", + ] + + def _generate_stock_quant_history(self): + self.ensure_one() + self.generated_date = fields.Datetime.now() + previous_quant_snapshot = self.search( + [ + ("state", "=", "generated"), + ("inventory_date", "<=", self.inventory_date), + ], + order="inventory_date desc", + limit=1, + ) + quant_history = DefaultDict( + lambda product, lot, location: self.env["stock.quant.history"] + .sudo() + .create( + { + "snapshot_id": self.id, + "product_id": product.id, + "lot_id": lot.id, + "location_id": location.id, + "quantity": 0, + } + ) + ) + self.previous_snapshot_id = previous_quant_snapshot + + _logger.info("Processing %s from %s", self.name, self.previous_snapshot_id.name) + if previous_quant_snapshot.stock_quant_history_ids.exists(): + _logger.info( + "Duplicate %s previous stock.quant.history...", + len(previous_quant_snapshot.stock_quant_history_ids), + ) + for stock_quant_history in previous_quant_snapshot.stock_quant_history_ids: + # copy is around 3x slower than create ! + quant_copy = quant_history[ + ( + stock_quant_history.product_id, + stock_quant_history.lot_id, + stock_quant_history.location_id, + ) + ] + quant_copy.quantity = stock_quant_history.quantity + + stock_move_lines = ( + self.env["stock.move.line"] + .sudo() + .search( + self._prepare_stock_move_line_filter(previous_quant_snapshot), + ) + ) + _logger.info( + "Apply %s stock.move.line since previous snapshot", len(stock_move_lines) + ) + ignored_location_usage = self._ignored_location_usage() + for move_line in stock_move_lines: + if move_line.location_id.usage not in ignored_location_usage: + quant_history[ + (move_line.product_id, move_line.lot_id, move_line.location_id) + ].quantity = tools.float_round( + quant_history[ + (move_line.product_id, move_line.lot_id, move_line.location_id) + ].quantity + - move_line.product_uom_id._compute_quantity( + move_line.qty_done, move_line.product_id.uom_id + ), + precision_rounding=move_line.product_id.uom_id.rounding, + ) + + if move_line.location_dest_id.usage not in ignored_location_usage: + quant_history[ + (move_line.product_id, move_line.lot_id, move_line.location_dest_id) + ].quantity = tools.float_round( + quant_history[ + ( + move_line.product_id, + move_line.lot_id, + move_line.location_dest_id, + ) + ].quantity + + move_line.product_uom_id._compute_quantity( + move_line.qty_done, move_line.product_id.uom_id + ), + precision_rounding=move_line.product_id.uom_id.rounding, + ) + + # remove line with zero to save same disk space + # avoid loop with direct SQL query + _logger.info("Remove useless stock_quant_history with quantity == 0") + self.env["stock.quant.history"].flush() + self.env.cr.execute( + "DELETE FROM stock_quant_history where quantity = 0 and snapshot_id = %s", + (self.id,), + ) + self.state = "generated" + + def action_related_stock_quant_history_tree_view(self): + action = self.env["ir.actions.actions"]._for_xml_id( + "stock_quant_history.action_stock_quant_history" + ) + action["domain"] = [("snapshot_id", "in", self.ids)] + return action diff --git a/stock_quant_history/readme/CONSTRIBUTORS.rst b/stock_quant_history/readme/CONSTRIBUTORS.rst new file mode 100644 index 0000000..638be86 --- /dev/null +++ b/stock_quant_history/readme/CONSTRIBUTORS.rst @@ -0,0 +1 @@ +* Pierre Verkest diff --git a/stock_quant_history/readme/DESCRIPTION.rst b/stock_quant_history/readme/DESCRIPTION.rst new file mode 100644 index 0000000..d0e7f1b --- /dev/null +++ b/stock_quant_history/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +This module allows regenerate stock.quant as it was for a given date. + +All stock quant history re-generated for a given date are called snapshot. + +To generate the first snapshot this module assume all `stock.move.line` +are present in the database. + +Next snapshot is computed based on the previous snapshot present in the database. + diff --git a/stock_quant_history/readme/ROADMAP.rst b/stock_quant_history/readme/ROADMAP.rst new file mode 100644 index 0000000..01b552c --- /dev/null +++ b/stock_quant_history/readme/ROADMAP.rst @@ -0,0 +1,13 @@ +Short terms +~~~~~~~~~~~ + +* Add a companion module stock_quant_history_account + to make the glue between stock_quant_history and stock_account adding + the stock value + +Long terms +~~~~~~~~~~ + +* Add filters (by locations, by product...) while generating + tight snapshots (not reused as based snapshot) +* add owner and package fields diff --git a/stock_quant_history/readme/USAGE.rst b/stock_quant_history/readme/USAGE.rst new file mode 100644 index 0000000..a3e897c --- /dev/null +++ b/stock_quant_history/readme/USAGE.rst @@ -0,0 +1,26 @@ +Generate a new stock snapshot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Go to: *Inventory / Reporting / History / Stock snapshot* +* choose the date you want to re-generate stock quants +* click on Generate + +Consult stock quant for a given snapshot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Go to: *Inventory / Reporting / History / Stock snapshot* +* select the existing snapshot to open the form view +* click on smart button to display quants at that time + +Compare stock over snapshots +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Go to: *Inventory / Reporting / History / Stock snapshot* +* In tree view select at least 2 snapshots +* Click on *Action / Compare stocks* +* You'll be redirected to the stock quant tree view for selected snapshots + +or + +* Go to: *Inventory / Reporting / History / Stock quants* +* use different filters / group and views to make your analysis diff --git a/stock_quant_history/security/ir.model.access.csv b/stock_quant_history/security/ir.model.access.csv new file mode 100644 index 0000000..d5075ff --- /dev/null +++ b/stock_quant_history/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_quant_history_manager,Stock manager Access to stock.quant.history,model_stock_quant_history,stock.group_stock_manager,1,0,0,0 +access_stock_quant_history_snapshot_manager,Stock manager Access to stock.quant.history.snapshot,model_stock_quant_history_snapshot,stock.group_stock_manager,1,1,1,1 diff --git a/stock_quant_history/static/description/index.html b/stock_quant_history/static/description/index.html new file mode 100644 index 0000000..6adfb46 --- /dev/null +++ b/stock_quant_history/static/description/index.html @@ -0,0 +1,483 @@ + + + + + + +Stock Quant History + + + +
+

Stock Quant History

+ + +

Beta License: AGPL-3 OCA/stock-logistics-reporting Translate me on Weblate Try me on Runboat

+

This module allows regenerate stock.quant as it was for a given date.

+

All stock quant history re-generated for a given date are called snapshot.

+

To generate the first snapshot this module assume all stock.move.line +are present in the database.

+

Next snapshot is computed based on the previous snapshot present in the database.

+

Table of contents

+ +
+

Usage

+
+

Generate a new stock snapshot

+
    +
  • Go to: Inventory / Reporting / History / Stock snapshot
  • +
  • choose the date you want to re-generate stock quants
  • +
  • click on Generate
  • +
+
+
+

Consult stock quant for a given snapshot

+
    +
  • Go to: Inventory / Reporting / History / Stock snapshot
  • +
  • select the existing snapshot to open the form view
  • +
  • click on smart button to display quants at that time
  • +
+
+
+

Compare stock over snapshots

+
    +
  • Go to: Inventory / Reporting / History / Stock snapshot
  • +
  • In tree view select at least 2 snapshots
  • +
  • Click on Action / Compare stocks
  • +
  • You’ll be redirected to the stock quant tree view for selected snapshots
  • +
+

or

+
    +
  • Go to: Inventory / Reporting / History / Stock quants
  • +
  • use different filters / group and views to make your analysis
  • +
+
+
+
+

Known issues / Roadmap

+
+

Short terms

+
    +
  • Add a companion module stock_quant_history_account +to make the glue between stock_quant_history and stock_account adding +the stock value
  • +
+
+
+

Long terms

+
    +
  • Add filters (by locations, by product…) while generating +tight snapshots (not reused as based snapshot)
  • +
  • add owner and package fields
  • +
+
+
+
+

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

+ +
+
+

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 maintainer:

+

petrus-v

+

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

+

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

+
+
+
+ + diff --git a/stock_quant_history/tests/__init__.py b/stock_quant_history/tests/__init__.py new file mode 100644 index 0000000..2425fb5 --- /dev/null +++ b/stock_quant_history/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_quant_history diff --git a/stock_quant_history/tests/test_stock_quant_history.py b/stock_quant_history/tests/test_stock_quant_history.py new file mode 100644 index 0000000..b030d8a --- /dev/null +++ b/stock_quant_history/tests/test_stock_quant_history.py @@ -0,0 +1,456 @@ +from collections import defaultdict + +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import AccessError +from odoo.tests import SavepointCase, users + + +class TestStockQuantHistory(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, + test_queue_job_no_delay=True, # no jobs thanks + ) + ) + + cls.stock_history_now = cls.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.now(), + } + ) + + cls.stock_manager_user = cls.env["res.users"].create( + { + "name": "foo", + "login": "stock_manager", + "email": "foo@bar.com", + "lang": "en_US", + "groups_id": [ + ( + 6, + 0, + ( + cls.env.ref("base.group_user") + | cls.env.ref("stock.group_stock_manager") + ).ids, + ) + ], + } + ) + cls.warehouse = cls.env.ref("stock.warehouse0") + cls.location = cls.warehouse.lot_stock_id + cls.product = cls.env["product.product"].create( + { + "name": "test", + "type": "product", + "tracking": "lot", + } + ) + cls.lot = cls.env["stock.production.lot"].create( + { + "name": "lot test", + "product_id": cls.product.id, + "company_id": cls.warehouse.company_id.id, + } + ) + cls.product_consu = cls.env["product.product"].create( + { + "name": "test", + "type": "consu", + } + ) + + def _update_product_stock(self, qty, lot=None, location=None, uom=None): + if lot is None: + lot = self.lot + if not location: + location = self.location + if not uom: + uom = self.product.uom_id + inventory = self.env["stock.inventory"].create( + { + "name": "Test Inventory", + "product_ids": [(6, 0, self.product.ids)], + "state": "confirm", + "line_ids": [ + ( + 0, + 0, + { + "product_qty": qty, + "location_id": location.id, + "product_id": self.product.id, + "product_uom_id": uom.id, + "prod_lot_id": lot.id, + }, + ) + ], + } + ) + inventory.action_validate() + + @classmethod + def quants_quantity_group_by(cls, recordset, key): + """inspired from sale_product_pack PR: gh:oca/product-pack/pull/159""" + groups = defaultdict(lambda: 0) + for elem in recordset: + groups[key(elem)] += elem.quantity + return groups + + def assertQuantCompare(self, quants, expected_quants): + """works either with stock.quant or stock.quants.history""" + + def group_key(quant): + return quant.product_id, quant.lot_id, quant.location_id + + grouped_quants = self.quants_quantity_group_by(quants, group_key) + grouped_expected_quants = self.quants_quantity_group_by( + expected_quants, group_key + ) + errors1 = [] + errors2 = [] + ok = [] + for key, quantity in grouped_quants.items(): + if grouped_expected_quants[key] != quantity: + errors1.append( + f"got {quantity} != Expected {grouped_expected_quants[key]} for" + f"{key}: [{key[0].name}, {key[1].name}, {key[2].name}], " + ) + else: + ok.append( + f"{grouped_expected_quants[key]} for " + f"{key}: [{key[0].name}, {key[1].name}, {key[2].name}], " + f"is the same {quantity} !" + ) + + for key, quantity in grouped_expected_quants.items(): + if grouped_quants[key] != quantity: + errors2.append( + f"got {grouped_quants[key]} != Expected {quantity} for " + f"{key}: [{key[0].name}, {key[1].name}, {key[2].name}], " + ) + self.assertEqual( + len(errors1) + len(errors2), + 0, + "Following diff detected:\n" + "\n".join(errors1) + + "\n or/and \n " + + "\n".join(errors2) + + "\n\nOK records:\n" + + "\n".join(ok), + ) + + def test_compare_quant(self): + self.stock_history_now.action_generate_stock_quant_history() + self.assertQuantCompare( + self.stock_history_now.stock_quant_history_ids, + self.env["stock.quant"].search( + [ + ( + "location_id.usage", + "not in", + [ + "customer", + "inventory", + "supplier", + ], + ), + ] + ), + ) + + @users("stock_manager") + def test_unlink_snapshot_unlink_related_stock_quant_history_records(self): + # browse with current user + self.stock_history_now.action_generate_stock_quant_history() + stock_history_now = self.env["stock.quant.history.snapshot"].browse( + self.stock_history_now.id + ) + stock_quant_history_ids = stock_history_now.stock_quant_history_ids.ids + self.assertTrue( + len(stock_quant_history_ids) > 0, + ) + stock_history_now.unlink() + self.assertEqual( + self.env["stock.quant.history"].search_count( + [("id", "in", stock_quant_history_ids)] + ), + 0, + ) + + @users("stock_manager") + def test_unlink_stock_quant_history_is_forbidden(self): + # browse with current user + self.stock_history_now.action_generate_stock_quant_history() + stock_history_now = self.env["stock.quant.history.snapshot"].browse( + self.stock_history_now.id + ) + with self.assertRaisesRegex( + AccessError, r"You are not allowed to delete.*stock.quant.histor.*" + ): + stock_history_now.stock_quant_history_ids.unlink() + + @users("stock_manager") + def test_stock_manager_create(self): + stock_history_now = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("1984-06-15 11:22:32"), + } + ) + self.assertEqual( + stock_history_now.name, + # en_US format + "Snapshot 06/15/1984 11:22:32", + ) + stock_history_now.inventory_date = fields.Datetime.now() + stock_history_now.action_generate_stock_quant_history() + + @freeze_time("2024-01-01 10:11") + def test_no_lines_before_oldest_move(self): + stock_history_1970 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("1970-01-01"), + } + ) + stock_history_1970.action_generate_stock_quant_history() + self.assertEqual( + stock_history_1970.generated_date, + fields.Datetime.from_string("2024-01-01 10:11"), + ) + self.assertEqual(stock_history_1970.state, "generated") + self.assertEqual(len(stock_history_1970.stock_quant_history_ids), 0) + + def test_round_decimal_using_uom_precision(self): + + with freeze_time("2023-01-01 10:00:00"): + self._update_product_stock(10.001) + + with freeze_time("2023-01-01 20:00:00"): + self._update_product_stock(20.002) + + snapshot_10 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 10:00:00"), + } + ) + snapshot_10.action_generate_stock_quant_history() + quant_history_10 = snapshot_10.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + # force wrong rounding for testing purpose adding float in python can be tricky + # >>> 0.1 + 0.1 + 0.1 + # 0.30000000000000004 + + quant_history_10.quantity = 10.001 + snapshot_20 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 20:00:00"), + } + ) + snapshot_20.action_generate_stock_quant_history() + quant_history_20 = snapshot_20.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + self.assertEqual(quant_history_20.quantity, 20) + + def test_next_quant_history_generation(self): + + with freeze_time("2023-01-01 10:00:00"): + self._update_product_stock(10) + + with freeze_time("2023-01-01 20:00:00"): + self._update_product_stock(30) + + snapshot_10 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 10:00:00"), + } + ) + snapshot_10.action_generate_stock_quant_history() + quant_history_10 = snapshot_10.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + self.assertEqual(quant_history_10.quantity, 10) + + snapshot_15 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 15:00:00"), + } + ) + snapshot_15.action_generate_stock_quant_history() + quant_history_15 = snapshot_15.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + self.assertEqual(quant_history_15.quantity, 10) + self.assertNotEqual(quant_history_10, quant_history_15) + self.assertNotEqual( + quant_history_10.inventory_date, quant_history_15.inventory_date + ) + snapshot_20 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 20:00:00"), + } + ) + snapshot_20.action_generate_stock_quant_history() + quant_history_20 = snapshot_20.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + self.assertEqual(quant_history_20.quantity, 30) + + snapshot_now = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.now(), + } + ) + snapshot_now.action_generate_stock_quant_history() + self.assertQuantCompare( + snapshot_now.stock_quant_history_ids, + self.env["stock.quant"].search( + [ + ( + "location_id.usage", + "not in", + [ + "customer", + "inventory", + "supplier", + ], + ), + ] + ), + ) + + def test_quant_0_not_present(self): + with freeze_time("2023-01-01 10:00:00"): + self._update_product_stock(10) + + with freeze_time("2023-01-01 15:00:00"): + self._update_product_stock(0) + + with freeze_time("2023-01-01 20:00:00"): + self._update_product_stock(30) + + snapshot_10 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 10:00:00"), + } + ) + snapshot_10.action_generate_stock_quant_history() + self.assertFalse(snapshot_10.previous_snapshot_id) + quant_history_10 = snapshot_10.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + self.assertEqual(quant_history_10.quantity, 10) + + self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 12:00:00"), + } + ) + snapshot_15 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 15:00:00"), + } + ) + snapshot_15.action_generate_stock_quant_history() + self.assertEqual(snapshot_15.previous_snapshot_id, snapshot_10) + quant_history_15 = snapshot_15.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + self.assertFalse( + quant_history_15.exists(), + ) + + snapshot_20 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 20:00:00"), + } + ) + snapshot_20.action_generate_stock_quant_history() + self.assertEqual(snapshot_20.previous_snapshot_id, snapshot_15) + quant_history_20 = snapshot_20.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + self.assertEqual(quant_history_20.quantity, 30) + + def test_action_related_stock_quant_history_tree_view(self): + self.assertEqual( + self.stock_history_now.action_related_stock_quant_history_tree_view()[ + "domain" + ], + [("snapshot_id", "in", self.stock_history_now.ids)], + ) + + def test_consu_product_are_ignored(self): + + with freeze_time("2023-01-01 09:00:00"): + + # Create stock picking with consumable + picking = self.env["stock.picking"].create( + { + "location_id": self.env.ref("stock.stock_location_customers").id, + "location_dest_id": self.location.id, + "picking_type_id": self.env.ref("stock.picking_type_in").id, + } + ) + self.env["stock.move"].create( + { + "name": self.product_consu.name, + "product_id": self.product_consu.id, + "product_uom_qty": 50.000, + "product_uom": self.product_consu.uom_id.id, + "picking_id": picking.id, + "location_id": self.env.ref("stock.stock_location_customers").id, + "location_dest_id": self.location.id, + } + ) + picking.action_confirm() + picking.move_ids_without_package.quantity_done = 50.000 + picking.button_validate() + + snapshot_10 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 10:00:00"), + } + ) + snapshot_10.action_generate_stock_quant_history() + self.assertFalse(snapshot_10.stock_quant_history_ids) + + def test_different_uom(self): + + with freeze_time("2023-01-01 10:00:00"): + self._update_product_stock(10, uom=self.env.ref("uom.product_uom_dozen")) + + snapshot_10 = self.env["stock.quant.history.snapshot"].create( + { + "inventory_date": fields.Datetime.from_string("2023-01-01 10:00:00"), + } + ) + snapshot_10.action_generate_stock_quant_history() + quant_history_10 = snapshot_10.stock_quant_history_ids.filtered( + lambda quant_history, pdt=self.product, loc=self.location: quant_history.product_id + == pdt + and quant_history.location_id == loc + ) + self.assertEqual(quant_history_10.quantity, 120) diff --git a/stock_quant_history/views/stock-quant-history-snapshot.xml b/stock_quant_history/views/stock-quant-history-snapshot.xml new file mode 100644 index 0000000..d8e0f71 --- /dev/null +++ b/stock_quant_history/views/stock-quant-history-snapshot.xml @@ -0,0 +1,139 @@ + + + + + + stock.quant.history.snapshot.search + stock.quant.history.snapshot + + + + + + + + + + + + + + stock.quant.history.snapshot.list + stock.quant.history.snapshot + + + + + + + + + + + + stock.quant.history.snapshot.form + stock.quant.history.snapshot + +
+
+
+ +
+ +
+
+

+ +

+
+ + + + + + +
+
+
+
+ + + Stock snapshot + stock.quant.history.snapshot + list,form + + +

+ Create your first snapshot! +

+
+
+ + + + + + Compare stocks + + + list + code + + action = records.action_related_stock_quant_history_tree_view() + + + +
diff --git a/stock_quant_history/views/stock-quant-history.xml b/stock_quant_history/views/stock-quant-history.xml new file mode 100644 index 0000000..34be2c4 --- /dev/null +++ b/stock_quant_history/views/stock-quant-history.xml @@ -0,0 +1,109 @@ + + + + + + stock.quant.history.search + stock.quant.history + + + + + + + + + + + + + + + + + + + + + stock.quant.history.list + stock.quant.history + + + + + + + + + + + + + + + + + stock.quant.history.pivot + stock.quant.history + + + + + + + + + + + + Stock snapshot + stock.quant.history + list,pivot,graph + + +

+ Generate stock quant history from stock quant history snapshot before! +

+
+
+ + + +
diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..881bd9f --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +freezegun