diff --git a/project_consumable/README.rst b/project_consumable/README.rst new file mode 100644 index 0000000000..85fe7ae2d7 --- /dev/null +++ b/project_consumable/README.rst @@ -0,0 +1,126 @@ +================== +Project consumable +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d945aa69887faf24cdade547c25c80aaa2d02c752193444173ee867f598bed91 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fproject-lightgray.png?logo=github + :target: https://github.com/OCA/project/tree/17.0/project_consumable + :alt: OCA/project +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/project-17-0/project-17-0-project_consumable + :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/project&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allow to collect materials/consumable linked to a project +adding account analytic lines. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To be able to track consumable products (or avoid to link un-followed +products) to project, you have to configure each products to be +available in projects. + +1. Go to Product form view (ie: *Invoicing > customers > Products*) and + make sure the project_ok field is checked (by default consumable + products are available). + +.. note:: + + This module do not depends on sales or accounting modules so you may + need to install extra modules to get menu entry to products or + analytic account line views. + +Usage +===== + +Link products to a project +-------------------------- + +This can be done from different places as describe bellow. + +In facts this add ``account.analytic.line`` with proper product, +quantities and Unit of Mesure provided by users, analytic amount will be +computed based on product cost. + +- Material & Consumable Menu +- Project tab + +Review consumable amount +------------------------ + +You can analyse consumable amount per project from different places: + +- project dashboard +- Analytic account line analyse +- Project analyse + +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 + +Contributors +------------ + +- 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/project `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/project_consumable/__init__.py b/project_consumable/__init__.py new file mode 100644 index 0000000000..a952e1fe2a --- /dev/null +++ b/project_consumable/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2021-2025 - Pierre Verkest +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models +from .hooks import set_project_ok_for_consumable_products diff --git a/project_consumable/__manifest__.py b/project_consumable/__manifest__.py new file mode 100644 index 0000000000..92206d6a34 --- /dev/null +++ b/project_consumable/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2021-2025 - Pierre Verkest +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Project consumable", + "summary": "Track the use of consumables by project with analytic accounting.", + "author": "Pierre Verkest, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/project", + "category": "Project Management", + "version": "17.0.1.0.0", + "license": "AGPL-3", + "maintainers": ["petrus-v"], + "depends": [ + "account", + "hr_timesheet", + ], + "data": [ + "views/analytic_account_line.xml", + "views/analytic_account_line_report.xml", + "views/product.xml", + "views/project_views.xml", + "security/project_consumable_security.xml", + ], + "demo": [ + "demo/product-product.xml", + ], + "post_init_hook": "set_project_ok_for_consumable_products", +} diff --git a/project_consumable/demo/product-product.xml b/project_consumable/demo/product-product.xml new file mode 100644 index 0000000000..a92de9e550 --- /dev/null +++ b/project_consumable/demo/product-product.xml @@ -0,0 +1,28 @@ + + + + Coffee capsule + + + + 1 capsule + + reference + + + + 10 capsules box + + bigger + + + False + True + Coffee capsule + 0.33 + consu + + + + + diff --git a/project_consumable/hooks.py b/project_consumable/hooks.py new file mode 100644 index 0000000000..6032a12679 --- /dev/null +++ b/project_consumable/hooks.py @@ -0,0 +1,2 @@ +def set_project_ok_for_consumable_products(env): + env["product.template"].search([("type", "=", "consu")]).write({"project_ok": True}) diff --git a/project_consumable/i18n/fr.po b/project_consumable/i18n/fr.po new file mode 100644 index 0000000000..addfd4bbc0 --- /dev/null +++ b/project_consumable/i18n/fr.po @@ -0,0 +1,233 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_consumable +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\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: project_consumable +#: model:uom.uom,name:project_consumable.uom_cat_coffee_capsule_unit +msgid "1 capsule" +msgstr "1 capsule" + +#. module: project_consumable +#: model:uom.uom,name:project_consumable.uom_cat_coffee_capsule_box_10 +msgid "10 capsules box" +msgstr "Boîte de 10 capsules" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_kanban +msgid "" +"" +msgstr "" +"" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_invoice_form +msgid "Consumable" +msgstr "Consomable" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_kanban +msgid "Quantity: " +msgstr "Quantité: " + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.act_project_consumable_product_line_all +#: model:ir.ui.menu,name:project_consumable.menu_hr_product_tracking_all +msgid "All Materials" +msgstr "Tous les matériels" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_form +msgid "Analytic Entry" +msgstr "Entrée analytique" + +#. module: project_consumable +#: model:ir.model,name:project_consumable.model_account_analytic_line +msgid "Analytic Line" +msgstr "Ligne analytique" + +#. module: project_consumable +#: model:ir.model.fields,field_description:project_consumable.field_product_product__project_ok +#: model:ir.model.fields,field_description:project_consumable.field_product_template__project_ok +msgid "Available in projects" +msgstr "Consommé dans les projets" + +#. module: project_consumable +#: model:ir.ui.menu,name:project_consumable.timesheet_menu_report_consumable_by_project +msgid "By Project" +msgstr "Par projet" + +#. module: project_consumable +#: model:ir.model.fields,help:project_consumable.field_product_product__project_ok +#: model:ir.model.fields,help:project_consumable.field_product_template__project_ok +msgid "" +"Check this box to be able to link this product with analytic line and " +"project or task. Product cost will be used and displayed in project " +"dashboard as consumable cost. So you'll be able to analyse consumable " +"products cost per project." +msgstr "" +"Sélection cette option pour permettre de liéer ce produit a une ligne " +"analytique lié à un projet ou une tâche. Le coût du produit sera utilisé et " +"impactera le compte analytique lié au projet lors de la saisie de ces " +"lignes. Vous pourrez ainsi analyser l'usage des consomables par projet." + +#. module: project_consumable +#: model:product.template,name:project_consumable.product_coffee_capsule_product_template +#: model:uom.category,name:project_consumable.uom_category_coffee_capsule +msgid "Coffee capsule" +msgstr "" + +#. module: project_consumable +#: model:ir.model.fields,field_description:project_consumable.field_project_project__consumable_count +msgid "Consumable Count" +msgstr "Nombre d'enregistrements" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.act_consumable_line_by_project +msgid "Consumables" +msgstr "Consomables" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Date" +msgstr "Date" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Group By" +msgstr "Grouper par" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_graph +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_pivot +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Material" +msgstr "Matériel" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_graph +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_pivot +msgid "Material Costs" +msgstr "Coûts des matériels" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Material by Date" +msgstr "Matériels par date" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.consumable_action_report_by_project +msgid "Material/Consumable By Project" +msgstr "Matériels par projet" + +#. module: project_consumable +#: model:ir.ui.menu,name:project_consumable.menu_timesheets_reports_consumables +msgid "Materials/Consumables" +msgstr "Matériels" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.act_project_consumable_product_line +#: model:ir.ui.menu,name:project_consumable.menu_hr_product_tracking +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "My Materials" +msgstr "Mes saisie de matériels" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.consumable_action_report_by_project +msgid "No activities found." +msgstr "Aucune activité trouvé." + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_project_consumable_product_line_all +msgid "No material found. Let's add a new one!" +msgstr "Aucun matériel enregistré." + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_project_consumable_product_line +msgid "No products found. Let's start a new one!" +msgstr "Aucun produit trouvé." + +#. module: project_consumable +#: model:ir.model.fields,help:project_consumable.field_project_project__consumable_count +msgid "Number of consumable lines collected." +msgstr "Nombre de lignes de consommation saisies." + +#. module: project_consumable +#: model:ir.model,name:project_consumable.model_product_template +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Product" +msgstr "Produit" + +#. module: project_consumable +#: model:ir.model,name:project_consumable.model_project_project +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Project" +msgstr "Projet" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.material_action_project +msgid "Project's Materials" +msgstr "Matériels du projet" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_consumable_line_by_project +msgid "Record a new activity" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_tree +msgid "Total" +msgstr "Total" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_project_consumable_product_line +msgid "" +"Track consumed products by projects every day and analysis cost per projects." +msgstr "" +"Suivez vos produits consommés par projet au jour le jour pour analyser leurs coûts." + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_consumable_line_by_project +msgid "" +"Track your consumable by projects every day and invoice those materials to " +"your customers." +msgstr "" +"Suivez vos produits consommés par projet au jour le jour pour facturer ces " +"matériels à vos clients." + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_project_consumable_product_line_all +msgid "Track your consumable products by projects every day and analyse costs." +msgstr "" +"Suivez vos consomables par projet au jour le jour pour analyser leurs coûts." + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.consumable_action_report_by_project +msgid "" +"Track your consumable products by projects every day and analyze associated " +"cost." +msgstr "" +"Suivez vos consomables par projet au jour le jour pour analyser leurs coûts." + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "User" +msgstr "Utilisateur" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_invoice_form +msgid "records" +msgstr "enreg." diff --git a/project_consumable/i18n/project_consumable.pot b/project_consumable/i18n/project_consumable.pot new file mode 100644 index 0000000000..99166233c0 --- /dev/null +++ b/project_consumable/i18n/project_consumable.pot @@ -0,0 +1,221 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_consumable +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.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: project_consumable +#: model:uom.uom,name:project_consumable.uom_cat_coffee_capsule_unit +msgid "1 capsule" +msgstr "" + +#. module: project_consumable +#: model:uom.uom,name:project_consumable.uom_cat_coffee_capsule_box_10 +msgid "10 capsules box" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_kanban +msgid "" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_invoice_form +msgid "Consumable" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_kanban +msgid "Quantity: " +msgstr "" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.act_project_consumable_product_line_all +#: model:ir.ui.menu,name:project_consumable.menu_hr_product_tracking_all +msgid "All Materials" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_form +msgid "Analytic Entry" +msgstr "" + +#. module: project_consumable +#: model:ir.model,name:project_consumable.model_account_analytic_line +msgid "Analytic Line" +msgstr "" + +#. module: project_consumable +#: model:ir.model.fields,field_description:project_consumable.field_product_product__project_ok +#: model:ir.model.fields,field_description:project_consumable.field_product_template__project_ok +msgid "Available in projects" +msgstr "" + +#. module: project_consumable +#: model:ir.ui.menu,name:project_consumable.timesheet_menu_report_consumable_by_project +msgid "By Project" +msgstr "" + +#. module: project_consumable +#: model:ir.model.fields,help:project_consumable.field_product_product__project_ok +#: model:ir.model.fields,help:project_consumable.field_product_template__project_ok +msgid "" +"Check this box to be able to link this product with analytic line and " +"project or task. Product cost will be used and displayed in project " +"dashboard as consumable cost. So you'll be able to analyse consumable " +"products cost per project." +msgstr "" + +#. module: project_consumable +#: model:product.template,name:project_consumable.product_coffee_capsule_product_template +#: model:uom.category,name:project_consumable.uom_category_coffee_capsule +msgid "Coffee capsule" +msgstr "" + +#. module: project_consumable +#: model:ir.model.fields,field_description:project_consumable.field_project_project__consumable_count +msgid "Consumable Count" +msgstr "" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.act_consumable_line_by_project +msgid "Consumables" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Date" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Group By" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_graph +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_pivot +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Material" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_graph +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_pivot +msgid "Material Costs" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Material by Date" +msgstr "" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.consumable_action_report_by_project +msgid "Material/Consumable By Project" +msgstr "" + +#. module: project_consumable +#: model:ir.ui.menu,name:project_consumable.menu_timesheets_reports_consumables +msgid "Materials/Consumables" +msgstr "" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.act_project_consumable_product_line +#: model:ir.ui.menu,name:project_consumable.menu_hr_product_tracking +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "My Materials" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.consumable_action_report_by_project +msgid "No activities found." +msgstr "" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_project_consumable_product_line_all +msgid "No material found. Let's add a new one!" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_project_consumable_product_line +msgid "No products found. Let's start a new one!" +msgstr "" + +#. module: project_consumable +#: model:ir.model.fields,help:project_consumable.field_project_project__consumable_count +msgid "Number of consumable lines collected." +msgstr "" + +#. module: project_consumable +#: model:ir.model,name:project_consumable.model_product_template +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Product" +msgstr "" + +#. module: project_consumable +#: model:ir.model,name:project_consumable.model_project_project +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "Project" +msgstr "" + +#. module: project_consumable +#: model:ir.actions.act_window,name:project_consumable.material_action_project +msgid "Project's Materials" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_consumable_line_by_project +msgid "Record a new activity" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_tree +msgid "Total" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_project_consumable_product_line +msgid "" +"Track consumed products by projects every day and analysis cost per " +"projects." +msgstr "" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_consumable_line_by_project +msgid "" +"Track your consumable by projects every day and invoice those materials to " +"your customers." +msgstr "" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.act_project_consumable_product_line_all +msgid "" +"Track your consumable products by projects every day and analyse costs." +msgstr "" + +#. module: project_consumable +#: model_terms:ir.actions.act_window,help:project_consumable.consumable_action_report_by_project +msgid "" +"Track your consumable products by projects every day and analyze associated " +"cost." +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_consumable_product_line_search +msgid "User" +msgstr "" + +#. module: project_consumable +#: model_terms:ir.ui.view,arch_db:project_consumable.project_invoice_form +msgid "records" +msgstr "" diff --git a/project_consumable/models/__init__.py b/project_consumable/models/__init__.py new file mode 100644 index 0000000000..6dabf604c0 --- /dev/null +++ b/project_consumable/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2021-2025 - Pierre Verkest +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import account_analytic_line +from . import product_template +from . import project_project diff --git a/project_consumable/models/account_analytic_line.py b/project_consumable/models/account_analytic_line.py new file mode 100644 index 0000000000..e263ce0521 --- /dev/null +++ b/project_consumable/models/account_analytic_line.py @@ -0,0 +1,123 @@ +# Copyright 2021-2025 - Pierre Verkest +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + consumable_project_id = fields.Many2one( + "project.project", + domain='[("allow_consumables", "=", True)]', + string="Project (consumable)", + ) + + def _consumable_preprocess_create(self, vals_list): + for vals in vals_list: + if not vals.get("name") and "consumable_project_id" in vals: + vals["name"] = "/" + return vals_list + + @api.model_create_multi + def create(self, vals_list): + vals_list = self._consumable_preprocess(vals_list) + vals_list = self._consumable_preprocess_create(vals_list) + + lines = super().create(vals_list) + + for line, values in zip(lines, vals_list, strict=False): + if line.consumable_project_id: + line._consumable_postprocess(values) + return lines + + def write(self, values): + values = self._consumable_preprocess([values])[0] + result = super().write(values) + # applied only for timesheet + self.filtered(lambda t: t.consumable_project_id)._consumable_postprocess(values) + return result + + def _consumable_preprocess(self, vals_list): + """Deduce other field values from the one given. + Override this to compute on the fly some field that can not be computed fields. + :param values: dict values for `create`or `write`. + """ + for vals in vals_list: + if all(v in vals for v in ["product_id", "consumable_project_id"]): + if "product_uom_id" not in vals: + product = ( + self.env["product.product"].sudo().browse(vals["product_id"]) + ) + vals["product_uom_id"] = product.uom_id.id + if not vals.get("account_id") and "consumable_project_id" in vals: + account = ( + self.env["project.project"] + .browse(vals["consumable_project_id"]) + .analytic_account_id + ) + if not account or not account.active: + raise ValidationError( + _( + "Materials must be created on a project " + "with an active analytic account." + ) + ) + vals["account_id"] = account.id + return vals_list + + def _consumable_postprocess(self, values): + sudo_self = self.sudo() + values_to_write = self._consumable_postprocess_values(values) + for consumable in sudo_self: + if values_to_write[consumable.id]: + consumable.write(values_to_write[consumable.id]) + return values + + def _consumable_postprocess_values(self, values): + """Get the addionnal values to write on record + :param dict values: values for the model's fields, as a dictionary:: + {'field_name': field_value, ...} + :return: a dictionary mapping each record id to its corresponding + dictionary values to write (may be empty). + """ + result = {id_: {} for id_ in self.ids} + sudo_self = self.sudo() + + if any( + field_name in values + for field_name in [ + "unit_amount", + "product_id", + "product_uom_id", + "date", + ] + ): + for material in sudo_self: + if material.consumable_project_id and material.product_id: + cost = material.product_id.standard_price or 0.0 + qty = material.unit_amount + if ( + material.product_uom_id + and material.product_id.uom_id + and material.product_uom_id != material.product_id.uom_id + ): + qty = material.product_uom_id._compute_quantity( + qty, + material.product_id.uom_id, + ) + amount = -1 * qty * cost + amount_converted = material.product_id.currency_id._convert( + amount, + material.account_id.currency_id, + self.env.company, + material.date, + ) + result[material.id].update( + { + "amount": amount_converted, + } + ) + return result diff --git a/project_consumable/models/product_template.py b/project_consumable/models/product_template.py new file mode 100644 index 0000000000..bcb48aebf1 --- /dev/null +++ b/project_consumable/models/product_template.py @@ -0,0 +1,25 @@ +# Copyright 2021-2025 - Pierre Verkest +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + project_ok = fields.Boolean( + string="Available in projects", + help="Check this box to be able to link this product with " + "analytic line and project or task. Product cost will be used " + "and displayed in project dashboard as consumable cost. " + "So you'll be able to analyse consumable products cost per project.", + ) + + @api.onchange("type") + def _onchange_type(self): + res = super()._onchange_type() + if self.type == "consu": + self.project_ok = True + else: + self.project_ok = False + return res diff --git a/project_consumable/models/project_project.py b/project_consumable/models/project_project.py new file mode 100644 index 0000000000..105a7ef541 --- /dev/null +++ b/project_consumable/models/project_project.py @@ -0,0 +1,90 @@ +# Copyright 2021-2025 - Pierre Verkest +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, _lt, api, fields, models +from odoo.exceptions import ValidationError + + +class Project(models.Model): + _inherit = "project.project" + + company_currency_id = fields.Many2one( + string="Company Currency", + related="company_id.currency_id", + readonly=True, + ) + allow_consumables = fields.Boolean( + "Consumable", default=False, help="Project allowed while collecting consumable" + ) + consumable_ids = fields.One2many( + "account.analytic.line", "consumable_project_id", "Associated Consumables" + ) + consumable_count = fields.Integer( + compute="_compute_consumable_total_price", + compute_sudo=True, + help="Number of consumable lines collected.", + ) + consumable_total_price = fields.Monetary( + compute="_compute_consumable_total_price", + help="Total price of all consumables recorded in the project.", + compute_sudo=True, + currency_field="company_currency_id", + ) + + @api.constrains("allow_consumables", "analytic_account_id") + def _check_allow_consumables(self): + for project in self: + if project.allow_consumables and not project.analytic_account_id: + raise ValidationError( + _("You cannot use consumables without an analytic account.") + ) + + @api.depends( + "consumable_ids", + "consumable_ids.amount", + "consumable_ids.consumable_project_id", + ) + def _compute_consumable_total_price(self): + consumables_read_group = self.env["account.analytic.line"]._read_group( + [("consumable_project_id", "in", self.ids)], + ["consumable_project_id"], + ["consumable_project_id:count", "amount:sum"], + ) + self.consumable_total_price = 0 + self.consumable_count = 0 + for project, count, amount_sum in consumables_read_group: + project.consumable_total_price = amount_sum + project.consumable_count = count + + def action_project_consumable(self): + action = self.env["ir.actions.act_window"]._for_xml_id( + "project_consumable.consumable_action_report_by_project" + ) + action["display_name"] = _("%(name)s's Materials", name=self.name) + action["domain"] = [("consumable_project_id", "in", self.ids)] + return action + + def _get_stat_buttons(self): + buttons = super()._get_stat_buttons() + if not self.allow_consumables or not self.env.user.has_group( + "project.group_project_manager" + ): + return buttons + + buttons.append( + { + "icon": "copy", + "text": _lt("Materials"), + "number": _lt( + "%(amount)s € (%(count)s)", + amount=self.consumable_total_price, + count=self.consumable_count, + ), + "action_type": "object", + "action": "action_project_consumable", + "show": True, + "sequence": 6, + } + ) + return buttons diff --git a/project_consumable/pyproject.toml b/project_consumable/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/project_consumable/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/project_consumable/readme/CONFIGURE.md b/project_consumable/readme/CONFIGURE.md new file mode 100644 index 0000000000..bac9ccf72f --- /dev/null +++ b/project_consumable/readme/CONFIGURE.md @@ -0,0 +1,12 @@ +To be able to track consumable products (or avoid to link un-followed +products) to project, you have to configure each products to be +available in projects. + +1. Go to Product form view (ie: *Invoicing \> customers \> Products*) + and make sure the project_ok field is checked (by default consumable + products are available). + +> [!NOTE] +> This module do not depends on sales or accounting modules so you may +> need to install extra modules to get menu entry to products or +> analytic account line views. diff --git a/project_consumable/readme/CONTRIBUTORS.md b/project_consumable/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..2b71f93ab0 --- /dev/null +++ b/project_consumable/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Pierre Verkest \<\> diff --git a/project_consumable/readme/DESCRIPTION.md b/project_consumable/readme/DESCRIPTION.md new file mode 100644 index 0000000000..7260c005c1 --- /dev/null +++ b/project_consumable/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module allow to collect materials/consumable linked to a +project adding account analytic lines. diff --git a/project_consumable/readme/USAGE.md b/project_consumable/readme/USAGE.md new file mode 100644 index 0000000000..03ae31aa07 --- /dev/null +++ b/project_consumable/readme/USAGE.md @@ -0,0 +1,18 @@ +## Link products to a project + +This can be done from different places as describe bellow. + +In facts this add `account.analytic.line` with proper product, +quantities and Unit of Mesure provided by users, analytic amount will be +computed based on product cost. + +* Material & Consumable Menu +* Project tab + +## Review consumable amount + +You can analyse consumable amount per project from different places: + +* project dashboard +* Analytic account line analyse +* Project analyse diff --git a/project_consumable/security/project_consumable_security.xml b/project_consumable/security/project_consumable_security.xml new file mode 100644 index 0000000000..347ae869ea --- /dev/null +++ b/project_consumable/security/project_consumable_security.xml @@ -0,0 +1,29 @@ + + + + + + account.analytic.line.consumable.user + + [ + ('user_id', '=', user.id), + ('consumable_project_id', '!=', False), + ] + + + + + account.analytic.line.consumable.manager + + [('consumable_project_id', '!=', False),] + + + diff --git a/project_consumable/static/description/icon.png b/project_consumable/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/project_consumable/static/description/icon.png differ diff --git a/project_consumable/static/description/index.html b/project_consumable/static/description/index.html new file mode 100644 index 0000000000..e0ac296b36 --- /dev/null +++ b/project_consumable/static/description/index.html @@ -0,0 +1,472 @@ + + + + + +Project consumable + + + +
+

Project consumable

+ + +

Beta License: AGPL-3 OCA/project Translate me on Weblate Try me on Runboat

+

This module allow to collect materials/consumable linked to a project +adding account analytic lines.

+

Table of contents

+ +
+

Configuration

+

To be able to track consumable products (or avoid to link un-followed +products) to project, you have to configure each products to be +available in projects.

+
    +
  1. Go to Product form view (ie: Invoicing > customers > Products) and +make sure the project_ok field is checked (by default consumable +products are available).
  2. +
+
+

Note

+

This module do not depends on sales or accounting modules so you may +need to install extra modules to get menu entry to products or +analytic account line views.

+
+
+
+

Usage

+ +
+

Review consumable amount

+

You can analyse consumable amount per project from different places:

+
    +
  • project dashboard
  • +
  • Analytic account line analyse
  • +
  • Project analyse
  • +
+
+
+
+

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
  • +
+
+
+

Contributors

+ +
+
+

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/project project on GitHub.

+

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

+
+
+
+ + diff --git a/project_consumable/tests/__init__.py b/project_consumable/tests/__init__.py new file mode 100644 index 0000000000..9c49fcabe8 --- /dev/null +++ b/project_consumable/tests/__init__.py @@ -0,0 +1 @@ +from . import test_project_consumable diff --git a/project_consumable/tests/test_project_consumable.py b/project_consumable/tests/test_project_consumable.py new file mode 100644 index 0000000000..5199ea0772 --- /dev/null +++ b/project_consumable/tests/test_project_consumable.py @@ -0,0 +1,228 @@ +# Copyright 2021-2025 - Pierre Verkest +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from datetime import date + +from odoo.exceptions import AccessError, ValidationError +from odoo.tests import TransactionCase, new_test_user, users + + +class TestProjectConsumable(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product = cls.env.ref("project_consumable.product_coffee_capsule") + default_plan_id = cls.env["account.analytic.plan"].search([], limit=1) + cls.analytic_account = cls.env["account.analytic.account"].create( + { + "name": "Test", + "plan_id": default_plan_id.id, + "company_id": cls.env.company.id, + } + ) + cls.project = cls.env["project.project"].create( + { + "name": "Test", + "allow_consumables": True, + "analytic_account_id": cls.analytic_account.id, + } + ) + cls.user_demo = cls.env.ref("base.user_demo") + cls.employee = cls.user_demo.employee_id + cls.project_user = new_test_user( + cls.env, + login="test.project.user", + groups="project.group_project_user,hr_timesheet.group_hr_timesheet_user", + ) + cls.project_manager = new_test_user( + cls.env, + login="test.project.manager", + groups="project.group_project_manager,hr_timesheet.group_timesheet_manager", + ) + + def test_onchange_product_type_project_ok_to_be_true(self): + self.product.project_ok = False + self.product.type = "consu" + self.product.product_tmpl_id._onchange_type() + self.assertTrue(self.product.project_ok) + + def test_onchange_product_type_project_ok_to_be_false(self): + self.product.project_ok = True + self.product.type = "service" + self.product.product_tmpl_id._onchange_type() + self.assertFalse(self.product.project_ok) + + def _prepare_consumable_line_data(self, **kwargs): + data = { + "name": "collect test material", + "consumable_project_id": self.project.id, + "account_id": None, + "product_id": self.product.id, + "unit_amount": 6, + "product_uom_id": self.product.uom_id.id, + "task_id": None, + "amount": None, + "date": None, + "partner_id": None, + } + data.update(**kwargs) + return {k: v for k, v in data.items() if v is not None} + + @users("test.project.user") + def test_user_create(self): + account_analytic_line = self.env["account.analytic.line"].create( + self._prepare_consumable_line_data() + ) + self.assertEqual(account_analytic_line.user_id.id, self.project_user.id) + + @users("test.project.user") + def test_user_create_for_someone_else_failed(self): + with self.assertRaises(AccessError): + self.env["account.analytic.line"].create( + self._prepare_consumable_line_data(user_id=self.project_manager.id) + ) + + @users("test.project.manager") + def test_manager_create(self): + account_analytic_line = self.env["account.analytic.line"].create( + self._prepare_consumable_line_data(user_id=self.project_user.id) + ) + self.assertEqual(account_analytic_line.user_id.id, self.project_user.id) + + @users("demo") + def test_user_id(self): + account_analytic_line = self.env["account.analytic.line"].create( + self._prepare_consumable_line_data(user_id=None) + ) + self.assertEqual(account_analytic_line.user_id.id, self.env.user.id) + + def test_name(self): + account_analytic_line = self.env["account.analytic.line"].create( + self._prepare_consumable_line_data(name=None) + ) + self.assertEqual(account_analytic_line.name, "/") + + def test_date(self): + account_analytic_line = self.env["account.analytic.line"].create( + self._prepare_consumable_line_data(date=None) + ) + self.assertEqual(account_analytic_line.date, date.today()) + + def test_analytic_account_set_from_project(self): + account_analytic_line = self.env["account.analytic.line"].create( + self._prepare_consumable_line_data(account_id=None) + ) + self.assertEqual( + account_analytic_line.account_id.id, self.project.analytic_account_id.id + ) + + def test_consumable_amount_force_uom(self): + account_analytic_line = self.env["account.analytic.line"].create( + self._prepare_consumable_line_data( + unit_amount=7, + product_uom_id=self.env.ref( + "project_consumable.uom_cat_coffee_capsule_box_10" + ).id, + ) + ) + self.assertEqual( + account_analytic_line.amount, + # -1: cost are negative + # 7: product quantity + # 10: coffee box + # 0.33 product unit cost + -1 * 7 * 10 * 0.33, + ) + + def test_consumable_amount_default_product_uom(self): + # self.product.standard_price = 0.33 + account_analytic_line = self.env["account.analytic.line"].create( + self._prepare_consumable_line_data(unit_amount=6, product_uom_id=None) + ) + self.assertEqual( + account_analytic_line.amount, + # -1: cost are negative + # 7: product quantity + # 0.33 product unit cost + -1 * 6 * 0.33, + ) + self.assertEqual( + account_analytic_line.product_uom_id.id, + self.env.ref("project_consumable.uom_cat_coffee_capsule_unit").id, + ) + + @users("demo") + def test_timesheet(self): + """Ensure we don't break timesheets behaviors""" + account_analytic_line = self.env["account.analytic.line"].create( + { + "name": "test timesheet", + "project_id": self.project.id, + "unit_amount": 3, + "employee_id": self.employee.id, + } + ) + self.assertEqual( + account_analytic_line.account_id.id, self.project.analytic_account_id.id + ) + self.assertEqual( + account_analytic_line.product_uom_id.id, + self.env.ref("uom.product_uom_hour").id, + ) + timesheet_cost = 75 + self.assertEqual(account_analytic_line.amount, -timesheet_cost * 3) + + def test_consumable_count(self): + self.env["account.analytic.line"].create( + self._prepare_consumable_line_data( + unit_amount=7, + product_uom_id=self.env.ref( + "project_consumable.uom_cat_coffee_capsule_box_10" + ).id, + ) + ) + self.assertEqual(self.project.consumable_count, 1) + + def test_project_with_inactive_analytic_account_raise(self): + self.project.analytic_account_id.active = False + with self.assertRaisesRegex( + ValidationError, + r"Materials must be created on a project with " + r"an active analytic account", + ): + self.env["account.analytic.line"].create( + self._prepare_consumable_line_data( + unit_amount=7, + product_uom_id=self.env.ref( + "project_consumable.uom_cat_coffee_capsule_box_10" + ).id, + ) + ) + + def test_project_without_analytic_account_raise(self): + with self.assertRaisesRegex( + ValidationError, r"You cannot use consumables without an analytic account" + ): + self.project.analytic_account_id = False + + def test_action_project_consumable(self): + action = self.project.action_project_consumable() + self.assertEqual( + action["domain"], [("consumable_project_id", "in", self.project.ids)] + ) + + def test_get_stat_buttons(self): + buttons = self.project._get_stat_buttons() + self.assertTrue( + "action_project_consumable" in [button["action"] for button in buttons], + buttons, + ) + + def test_get_stat_buttons_non_project_manager(self): + user_demo = self.env.ref("base.user_demo") + self.assertFalse(user_demo.has_group("project.group_project_manager")) + buttons = self.project.with_user(user_demo)._get_stat_buttons() + self.assertTrue( + "action_project_consumable" not in [button["action"] for button in buttons], + buttons, + ) diff --git a/project_consumable/views/analytic_account_line.xml b/project_consumable/views/analytic_account_line.xml new file mode 100644 index 0000000000..f06626166c --- /dev/null +++ b/project_consumable/views/analytic_account_line.xml @@ -0,0 +1,431 @@ + + + + + account.analytic.line.tree.project_consumable + account.analytic.line + + + + + + + + + + + + + + + + + account.analytic.line.tree.project_consumable.with.user + account.analytic.line + + primary + 10 + + + False + 1 + many2one_avatar_user + + + + + + account.analytic.line.project_consumable.pivot + account.analytic.line + + + + + + + + + + + + account.analytic.line.project_consumable.graph + account.analytic.line + + + + + + + + + + + account.analytic.line.project_consumable.form + account.analytic.line + 1 + + +
+ + + + + + + + + + + + + + + + + + +
+
+
+ + + account.analytic.line.project_consumable.form.with.user + account.analytic.line + + primary + 10 + + + + + + + + + account.analytic.line.project_consumable.search + account.analytic.line + + + + + + + + + + + + + + + + + + + + + + account.analytic.line.project_consumable.search.user + account.analytic.line + + primary + 100 + + + + + + + + + account.analytic.line.project_consumable.kanban + account.analytic.line + + + + + + + + + + + + +
+
+
+ +
+
+ + + +
+
+ +
+
+ +
+
+
+ + + + + + Quantity: + +
+
+
+
+
+
+
+
+ + + + My Materials + account.analytic.line + tree,form,kanban + [('consumable_project_id', '!=', False), ('user_id', '=', uid), ('product_id', '!=', False)] + { + "search_default_week":1, + } + + +

+ No products found. Let's start a new one! +

+

+ Track consumed products by projects every day and analysis cost per projects. +

+
+
+ + + tree + + + + + + + form + + + + + + + kanban + 6 + + + + + + + + Project's Materials + account.analytic.line + [('consumable_project_id', 'in', active_ids), ('product_id', '!=', False)] + tree + + + + + All Materials + account.analytic.line + tree,form,pivot,graph,kanban + + [('consumable_project_id', '!=', False), ('product_id', '!=', False)] + { + 'search_default_week':1, + } + +

+ No material found. Let's add a new one! +

+

+ Track your consumable products by projects every day and analyse costs. +

+
+
+ + + + + tree + + + + + + + form + + + + + + + pivot + + + + + + + graph + + + + + + kanban + 7 + + + + + +
diff --git a/project_consumable/views/analytic_account_line_report.xml b/project_consumable/views/analytic_account_line_report.xml new file mode 100644 index 0000000000..3da6efb96d --- /dev/null +++ b/project_consumable/views/analytic_account_line_report.xml @@ -0,0 +1,80 @@ + + + + + + Material/Consumable By Project + account.analytic.line + [('consumable_project_id', '!=', False), ('product_id', '!=', False)] + {'search_default_groupby_project': 1} + + +

+ No activities found. +

+ Track your consumable products by projects every day and analyze associated cost. +

+
+
+ + + + pivot + + + + + + + graph + + + + + + + tree + + + + + + + form + + + + + + + + +
diff --git a/project_consumable/views/product.xml b/project_consumable/views/product.xml new file mode 100644 index 0000000000..8e35c8c125 --- /dev/null +++ b/project_consumable/views/product.xml @@ -0,0 +1,18 @@ + + + + + product.template + + + +
+ +
+
+
+
+
diff --git a/project_consumable/views/project_views.xml b/project_consumable/views/project_views.xml new file mode 100644 index 0000000000..c4e765205e --- /dev/null +++ b/project_consumable/views/project_views.xml @@ -0,0 +1,60 @@ + + + + + + Consumables + account.analytic.line + tree,form + + [('project_consumable_id', '!=', False), ('product_id', '!=', False)] + {"default_project_consumable_id": active_id, "search_default_project_consumable_id": [active_id]} + + +

+ Record a new activity +

+ Track your consumable by projects every day and invoice those materials to your customers. +

+
+
+ + + Inherit project form : Invoicing Data + project.project + + 20 + +
+ +
+
+
+ +