diff --git a/project_minimum_margin_threshold_alert/README.rst b/project_minimum_margin_threshold_alert/README.rst new file mode 100644 index 0000000000..86e2427588 --- /dev/null +++ b/project_minimum_margin_threshold_alert/README.rst @@ -0,0 +1,131 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +====================================== +Project Minimum Margin Threshold Alert +====================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4c63603caa206ac1496ddecbe6881c78356e0909339c8cb1775ef4d6c347b7e7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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/19.0/project_minimum_margin_threshold_alert + :alt: OCA/project +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/project-19-0/project-19-0-project_minimum_margin_threshold_alert + :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=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the Odoo Project application to support automatic +margin minimum threshold alerts. It allows companies to define a default +margin limit (for example when project costs exceed a given percentage +of revenues) and to apply it to projects that do not have a specific +alert configuration. Each project can also define its own threshold. + +When the margin threshold is reached or exceeded, the system sends +notifications to the project manager and to internal subscribers of the +project. The notification method follows the user’s notification +preferences, with an option to force the sending of an email even if the +user is configured to receive only in-app notifications. This module +helps project managers anticipate financial risks and improves internal +communication around project margin. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +**1. Configure the default minimum margin threshold** + +- Go to Settings > Project > Set the default threshold percentage + |screenshot.png| + +**2. Configure a project-specific minimum margin threshold (optional)** + +- Go to Project > Open a project > Manage notifications section > Define + a specific threshold for this project. + +**3. Manage notification recipients** + +- Define a Project Manager and followers (subscribers) for the project + These users will receive notifications when the threshold is reached. + +**4. User notification preferences** + +- Go to Settings > Open a user > Configure the user’s notification + preferences. |screenshot1.png| + +**5. Project notification preferences** + +When project costs exceed the configured percentage of revenues, the +system automatically sends (via a cron) + +- An email to manager and internal user followers if the Force Email + Notification option is enabled from the project. +- An activity of type 'Minimum Margin Exceeded' for the project manager + if the Create Activity option is enabled from the project. + +|screenshot2.png| + +.. |screenshot.png| image:: https://raw.githubusercontent.com/OCA/project/19.0/project_minimum_margin_threshold_alert/static/description/screenshot.png +.. |screenshot1.png| image:: https://raw.githubusercontent.com/OCA/project/19.0/project_minimum_margin_threshold_alert/static/description/screenshot1.png +.. |screenshot2.png| image:: https://raw.githubusercontent.com/OCA/project/19.0/project_minimum_margin_threshold_alert/static/description/screenshot2.png + +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 +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Nihel GABSI nihel.gabsi@acsone.eu (https://www.acsone.eu/) + +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. + +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_minimum_margin_threshold_alert/__init__.py b/project_minimum_margin_threshold_alert/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/project_minimum_margin_threshold_alert/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_minimum_margin_threshold_alert/__manifest__.py b/project_minimum_margin_threshold_alert/__manifest__.py new file mode 100644 index 0000000000..030b3ee967 --- /dev/null +++ b/project_minimum_margin_threshold_alert/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Project Minimum Margin Threshold Alert", + "summary": """Send notification when project costs threshold is exceeded""", + "version": "19.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/project", + "depends": ["project"], + "data": [ + "views/res_config_settings.xml", + "views/res_users.xml", + "views/project_project.xml", + "data/cron.xml", + "data/mail_template.xml", + "data/activity_type.xml", + ], + "demo": [], +} diff --git a/project_minimum_margin_threshold_alert/data/activity_type.xml b/project_minimum_margin_threshold_alert/data/activity_type.xml new file mode 100644 index 0000000000..40fa348c6f --- /dev/null +++ b/project_minimum_margin_threshold_alert/data/activity_type.xml @@ -0,0 +1,12 @@ + + + + + Minimum Margin Exceeded + Project margin threshold exceeded + fa-level-down + 100 + project.project + + diff --git a/project_minimum_margin_threshold_alert/data/cron.xml b/project_minimum_margin_threshold_alert/data/cron.xml new file mode 100644 index 0000000000..5896a06e70 --- /dev/null +++ b/project_minimum_margin_threshold_alert/data/cron.xml @@ -0,0 +1,13 @@ + + + + + Project: Manage Projects with minimum margin exceeded + + code + model._cron_margin_threshold_exceeded() + 1 + days + + diff --git a/project_minimum_margin_threshold_alert/data/mail_template.xml b/project_minimum_margin_threshold_alert/data/mail_template.xml new file mode 100644 index 0000000000..548dc11b16 --- /dev/null +++ b/project_minimum_margin_threshold_alert/data/mail_template.xml @@ -0,0 +1,36 @@ + + + + + Project: Minimum Margin threashold exceeded + + {{ object.name }}: Costs Threashold Exceeded + Automated notification sent to the project subscribers to inform about project Costs vs Revenues + threshold exceeding + + +

Hello,

+
+

+ This is an automated notification to inform you that the costs threshold for the project + + + has been exceeded. +

+

+ Please review the project and take any necessary actions. +

+
+
+

+ Best regards, +
+ +

+
+ {{ object.user_id.lang }} +
+
diff --git a/project_minimum_margin_threshold_alert/i18n/fr.po b/project_minimum_margin_threshold_alert/i18n/fr.po new file mode 100644 index 0000000000..eba3541c84 --- /dev/null +++ b/project_minimum_margin_threshold_alert/i18n/fr.po @@ -0,0 +1,274 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_minimum_margin_threshold_alert +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-02 13:23+0000\n" +"PO-Revision-Date: 2026-02-02 13:23+0000\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_minimum_margin_threshold_alert +#: model:mail.template,body_html:project_minimum_margin_threshold_alert.project_minimum_margin_exceeded_template +msgid "" +"

Hello,

\n" +"
\n" +"

\n" +" This is an automated notification to inform you that the costs threshold for the project\n" +" \n" +" \n" +" has been exceeded.\n" +"

\n" +"

\n" +" Please review the project and take any necessary actions.\n" +"

\n" +"
\n" +"
\n" +"

\n" +" Best regards,\n" +"
\n" +" \n" +"

\n" +" " +msgstr "" +"

Bonjour,

\n" +"
\n" +"

\n" +" Ceci est une notification automatique pour vous informer que le seuil des coûts du projet\n" +" \n" +" \n" +" a été dépassé.\n" +"

\n" +"

\n" +" Veuillez examiner le projet et prendre les mesures nécessaires.\n" +"

\n" +"
\n" +"
\n" +"

\n" +" Cordialement,\n" +"
\n" +" \n" +"

\n" +" " + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.res_config_settings_view_form_inherit +msgid "" +"" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_users__receive_project_margin_threshold_notification +msgid "Activate minimum margin notifications for projects" +msgstr "Activer les notifications sur les marges minimum pour les projets" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.template,description:project_minimum_margin_threshold_alert.project_minimum_margin_exceeded_template +msgid "" +"Automated notification sent to the project subscribers to inform about project Costs vs Revenues\n" +" threshold exceeding\n" +" " +msgstr "" +"Notification automatique envoyée aux abonnés du projet pour les informer du " +"dépassement du seuil des coûts par rapport aux revenus" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_project_project__force_margin_threshold_notification +msgid "Check this if you want to send emails when margin is exceeded." +msgstr "Cochez ceci si vous voulez envoyer des emails quand la marge est dépassée." + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_project_project__create_margin_threshold_activity +msgid "" +"Check this in order to create activities for users with that parameter " +"activated when margin threshold is exceeded." +msgstr "" +"Cochez ceci pour créer des activités pour les utilisateurs dont le paramètre" +"est activé afin de créer des activités quand le seuil de marge est dépassé." + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold +msgid "Check this to define a default margin" +msgstr "Cochez ceci pour définir une marge par défaut" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold_create_activity +msgid "" +"Check this to enable by default the creation of activityfor dedicated " +"project manager when project margin threshold (minimum) is ." +msgstr "" +"Cochez ceci pour activer par défaut la création d'activité pour le" +"gestionnaire de projet lorsque le seuil de marge est dépassé." + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold_send_email +msgid "" +"Check this to enable by default the sending of emailsto internal users when " +"project margin is exceeded." +msgstr "" +"Cochez ceci pour activer par défaut l'envoi des emails aux utilsateurs" +"internes quand la marge du projet est dépassée." + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model,name:project_minimum_margin_threshold_alert.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres de configuration" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__create_margin_threshold_activity +msgid "Create Margin Threshold Activity" +msgstr "Créer une activité pour le seuil d'alerte sur les marges" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__display_name +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_users__display_name +msgid "Display Name" +msgstr "Nom d'affichage" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__force_margin_threshold_notification +msgid "Force email notification sending (margin)" +msgstr "Forcer la notification par envoi d'email (marges)" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__id +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__id +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_users__id +msgid "ID" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__is_margin_threshold_exceeded +msgid "Is Margin Threshold Exceeded" +msgstr "Seuil de Marge Dépassé" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__is_margin_threshold_exceeded_notfication_sent +msgid "Is Margin Threshold Exceeded Notfication Sent" +msgstr "Notification sur les seuils d'alerte de marge envoyée" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.edit_project_inherit +msgid "Margin Notifications" +msgstr "Notifications sur les marges" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.res_config_settings_view_form_inherit +msgid "Margin Notifications Management" +msgstr "Notifications de marge" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__margin_threshold +msgid "Margin Threshold" +msgstr "Seuil de Marge" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.activity.type,name:project_minimum_margin_threshold_alert.mail_activity_type_margin_threshold +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.view_project_project_filter +msgid "Minimum Margin Exceeded" +msgstr "Marge Minimum dépassée" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.view_project_project_filter +msgid "Minimum Margin Notification Sent" +msgstr "Notification de Marge Minimum Envoyée" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.edit_project_inherit +msgid "Notifications" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_res_users__receive_project_margin_threshold_notification +msgid "Notify user about project minimum margin threshold exceeding" +msgstr "Notifier l'utilisateur des dépassements " + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.res_config_settings_view_form_inherit +msgid "" +"Percentage of project costs compared to project revenues that\n" +" triggers an alert. For example, a value of 50 means an alert will be sent when project costs\n" +" reach\n" +" 50% of the project revenues." +msgstr "" +"Pourcentage des coûts du projet par rapport aux revenus du projet qui\n" +" déclenche une alerte. Par exemple, une valeur de 50 signifie qu’une alerte sera envoyée lorsque les coûts du projet\n" +" atteignent\n" +" 50 % des revenus du projet." + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_project_project__margin_threshold +msgid "" +"Percentage of project costs compared to project revenues that triggers an " +"alert" +msgstr "" +"Pourcentage des coûts de projet comparé aux revenus qui vont lancer une alerte" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model,name:project_minimum_margin_threshold_alert.model_project_project +msgid "Project" +msgstr "Projet" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold +msgid "Project Margin Threshold" +msgstr "Seuil de Marge sur les Projets" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold_create_activity +msgid "Project Margin Threshold Create Activity" +msgstr "Créer une Activité sur les Seuils de Marge des Projets" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold_send_email +msgid "Project Margin Threshold Send Email" +msgstr "Envoyer un mail sur les seuils de Marge des Projets" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.activity.type,summary:project_minimum_margin_threshold_alert.mail_activity_type_margin_threshold +msgid "Project margin threshold exceeded" +msgstr "Le seuil de marge dépassé" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.actions.server,name:project_minimum_margin_threshold_alert.ir_cron_project_margin_exceeded_ir_actions_server +msgid "Project: Manage Projects with minimum margin exceeded" +msgstr "Projet: Gérer les Projets avec les marges dépassées" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.template,name:project_minimum_margin_threshold_alert.project_minimum_margin_exceeded_template +msgid "Project: Minimum Margin threashold exceeded" +msgstr "Projet: Dépassement du seuil de marge minimum" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.edit_project_inherit +msgid "The margin is exceeded!" +msgstr "La marge est dépassée!" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_project_project__is_margin_threshold_exceeded +msgid "This is set if margin is exceeded from threshold" +msgstr "Ceci est défini si la marge est dépassée depuis le seuil" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.edit_project_inherit +msgid "Thresholds" +msgstr "Seuils" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model,name:project_minimum_margin_threshold_alert.model_res_users +msgid "User" +msgstr "Utilisateur" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.template,subject:project_minimum_margin_threshold_alert.project_minimum_margin_exceeded_template +msgid "{{ object.name }}: Costs Threashold Exceeded" +msgstr "{{ object.name }} : Seuil des coûts dépassé" \ No newline at end of file diff --git a/project_minimum_margin_threshold_alert/i18n/project_minimum_margin_threshold_alert.pot b/project_minimum_margin_threshold_alert/i18n/project_minimum_margin_threshold_alert.pot new file mode 100644 index 0000000000..8795963f29 --- /dev/null +++ b/project_minimum_margin_threshold_alert/i18n/project_minimum_margin_threshold_alert.pot @@ -0,0 +1,242 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_minimum_margin_threshold_alert +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-02 13:14+0000\n" +"PO-Revision-Date: 2026-02-02 13:14+0000\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_minimum_margin_threshold_alert +#: model:mail.template,body_html:project_minimum_margin_threshold_alert.project_minimum_margin_exceeded_template +msgid "" +"

Hello,

\n" +"
\n" +"

\n" +" This is an automated notification to inform you that the costs threshold for the project\n" +" \n" +" \n" +" has been exceeded.\n" +"

\n" +"

\n" +" Please review the project and take any necessary actions.\n" +"

\n" +"
\n" +"
\n" +"

\n" +" Best regards,\n" +"
\n" +" \n" +"

\n" +" " +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.res_config_settings_view_form_inherit +msgid "" +"" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_users__receive_project_margin_threshold_notification +msgid "Activate minimum margin notifications for projects" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.template,description:project_minimum_margin_threshold_alert.project_minimum_margin_exceeded_template +msgid "" +"Automated notification sent to the project subscribers to inform about project Costs vs Revenues\n" +" threshold exceeding\n" +" " +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_project_project__force_margin_threshold_notification +msgid "Check this if you want to send emails when margin is exceeded." +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_project_project__create_margin_threshold_activity +msgid "" +"Check this in order to create activities for users with that parameter " +"activated when margin threshold is exceeded." +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold +msgid "Check this to define a default margin" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold_create_activity +msgid "" +"Check this to enable by default the creation of activityfor dedicated " +"project manager when project margin threshold (minimum) is ." +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold_send_email +msgid "" +"Check this to enable by default the sending of emailsto internal users when " +"project margin is exceeded." +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model,name:project_minimum_margin_threshold_alert.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__create_margin_threshold_activity +msgid "Create Margin Threshold Activity" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__display_name +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_users__display_name +msgid "Display Name" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__force_margin_threshold_notification +msgid "Force email notification sending (margin)" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__id +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__id +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_users__id +msgid "ID" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__is_margin_threshold_exceeded +msgid "Is Margin Threshold Exceeded" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__is_margin_threshold_exceeded_notfication_sent +msgid "Is Margin Threshold Exceeded Notfication Sent" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_project_project__margin_threshold +msgid "Margin Threshold" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.activity.type,name:project_minimum_margin_threshold_alert.mail_activity_type_margin_threshold +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.view_project_project_filter +msgid "Minimum Margin Exceeded" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.view_project_project_filter +msgid "Minimum Margin Notification Sent" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.edit_project_inherit +msgid "Notifications" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_res_users__receive_project_margin_threshold_notification +msgid "Notify user about project minimum margin threshold exceeding" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.res_config_settings_view_form_inherit +msgid "" +"Percentage of project costs compared to project revenues that\n" +" triggers an alert. For example, a value of 50 means an alert will be sent when project costs\n" +" reach\n" +" 50% of the project revenues." +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_project_project__margin_threshold +msgid "" +"Percentage of project costs compared to project revenues that triggers an " +"alert" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model,name:project_minimum_margin_threshold_alert.model_project_project +msgid "Project" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold +msgid "Project Margin Threshold" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold_create_activity +msgid "Project Margin Threshold Create Activity" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,field_description:project_minimum_margin_threshold_alert.field_res_config_settings__project_margin_threshold_send_email +msgid "Project Margin Threshold Send Email" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.activity.type,summary:project_minimum_margin_threshold_alert.mail_activity_type_margin_threshold +msgid "Project margin threshold exceeded" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.actions.server,name:project_minimum_margin_threshold_alert.ir_cron_project_margin_exceeded_ir_actions_server +msgid "Project: Manage Projects with minimum margin exceeded" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.template,name:project_minimum_margin_threshold_alert.project_minimum_margin_exceeded_template +msgid "Project: Minimum Margin threashold exceeded" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.edit_project_inherit +msgid "The margin is exceeded!" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model.fields,help:project_minimum_margin_threshold_alert.field_project_project__is_margin_threshold_exceeded +msgid "This is set if margin is exceeded from threshold" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.edit_project_inherit +msgid "Thresholds" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:ir.model,name:project_minimum_margin_threshold_alert.model_res_users +msgid "User" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.res_config_settings_view_form_inherit +msgid "margin Notifications Management" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model_terms:ir.ui.view,arch_db:project_minimum_margin_threshold_alert.edit_project_inherit +msgid "margin notifications" +msgstr "" + +#. module: project_minimum_margin_threshold_alert +#: model:mail.template,subject:project_minimum_margin_threshold_alert.project_minimum_margin_exceeded_template +msgid "{{ object.name }}: Costs Threashold Exceeded" +msgstr "" diff --git a/project_minimum_margin_threshold_alert/models/__init__.py b/project_minimum_margin_threshold_alert/models/__init__.py new file mode 100644 index 0000000000..8600b652c5 --- /dev/null +++ b/project_minimum_margin_threshold_alert/models/__init__.py @@ -0,0 +1,3 @@ +from . import project_project +from . import res_users +from . import res_config_settings diff --git a/project_minimum_margin_threshold_alert/models/project_project.py b/project_minimum_margin_threshold_alert/models/project_project.py new file mode 100644 index 0000000000..d58f8cfba9 --- /dev/null +++ b/project_minimum_margin_threshold_alert/models/project_project.py @@ -0,0 +1,157 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + +from odoo.addons.base.models.res_users import ResUsers + + +class ProjectProject(models.Model): + _inherit = "project.project" + + margin_threshold = fields.Float( + help="Percentage of project costs compared to project revenues " + "that triggers an alert", + default=lambda self: self._default_margin_threshold, + tracking=True, + ) + is_margin_threshold_exceeded = fields.Boolean( + help="This is set if margin is exceeded from threshold", + tracking=True, + ) + is_margin_threshold_exceeded_notfication_sent = fields.Boolean( + compute="_compute_is_margin_threshold_exceeded_notfication_sent", + store=True, + index=True, + tracking=True, + ) + force_margin_threshold_notification = fields.Boolean( + string="Force email notification sending (margin)", + default=lambda self: self._default_force_margin_threshold_notification, + help="Check this if you want to send emails when margin is exceeded.", + ) + create_margin_threshold_activity = fields.Boolean( + default=lambda self: self._default_create_margin_threshold_activity, + help="Check this in order to create activities for users " + "with that parameter activated when margin threshold is exceeded.", + ) + + @property + def _default_margin_threshold(self): + return float( + self.env["ir.config_parameter"] + .sudo() + .get_param("project_margin_threshold_alert.project_margin_threshold") + ) + + @property + def _default_force_margin_threshold_notification(self): + return ( + self.env["ir.config_parameter"] + .sudo() + .get_param( + "project_margin_threshold_alert.project_margin_threshold_send_email" + ) + ) + + @property + def _default_create_margin_threshold_activity(self): + return ( + self.env["ir.config_parameter"] + .sudo() + .get_param( + "project_margin_threshold_alert.project_margin_threshold_create_activity" + ) + ) + + def _get_margin_threshold_to_notify_domain(self) -> list: + """ + Build the domain to get projects that need to notify + partners and users about margin threshold exceed. + """ + return [ + ("is_margin_threshold_exceeded_notfication_sent", "=", False), + ("is_margin_threshold_exceeded", "=", True), + ] + + def _cron_margin_threshold_exceeded(self) -> None: + """ + This method will: + - Update the field 'is_margin_threshold_exceeded' + - Notify users + """ + projects = self.env["project.project"].search([]) # pylint: disable=no-search-all + projects._update_is_margin_threshold_exceeded() + for project in projects.filtered_domain( + self._get_margin_threshold_to_notify_domain() + ): + users_to_notify = project._get_internal_users_for_margin_threshold() + project._send_margin_threshold_notifications(users_to_notify) + + if project.force_margin_threshold_notification: + project._post_message_to_partners(users_to_notify.partner_id) + + if project.create_margin_threshold_activity: + project._create_margin_threshold_activity_for_manager(project.user_id) + projects.is_margin_threshold_exceeded_notfication_sent = True + + def _send_margin_threshold_notifications(self, users_to_notify): + self.message_notify( + partner_ids=users_to_notify.filtered( + lambda u: u.receive_project_margin_threshold_notification + ).partner_id.ids, + body=f"Cost threshold exceeded for project {self.name}!", + subject="Project Cost Threshold Exceeded", + email_layout_xmlid=None, + ) + + def _update_is_margin_threshold_exceeded(self): + """ + Update the field "is_margin_threshold_exceeded" + Do it asynchronously as the mean to compute it depends on + additionnal modules that fill in the "_get_margin_items" + method -> not possible to do it with compute/depends triggers. + """ + for project in self: + margin_values, _show = project._get_profitability_values() + margin = float(margin_values.get("expected_percentage", "0")) + # Margin is set to 0 if no costs. Ignore it + project.is_margin_threshold_exceeded = bool( + margin and ((margin / 100) < project.margin_threshold) + ) + + def _post_message_to_partners(self, partners): + template = ( + "project_minimum_margin_threshold_alert." + "project_minimum_margin_exceeded_template" + ) + self.message_post_with_source(source_ref=template, partner_ids=partners.ids) + + def _create_margin_threshold_activity_for_manager(self, manager): + if manager: + self.env["mail.activity"].create( + { + "res_model_id": self.env["ir.model"]._get_id("project.project"), + "res_id": self.id, + "user_id": manager.id, + "activity_type_id": self.env.ref( + "project_minimum_margin_threshold_alert.mail_activity_type_margin_threshold" + ).id, + } + ) + + def _get_internal_users_for_margin_threshold(self) -> ResUsers: + """ + Returns the internal followers and the project manager (user_id) + """ + internal_user_followers = ( + self.message_partner_ids.user_ids.filtered(lambda x: not x.share) + | self.user_id + ) + return internal_user_followers + + @api.depends("margin_threshold") + def _compute_is_margin_threshold_exceeded_notfication_sent(self): + """ + Threshold has been changed -> reset the notification is sent field + """ + self.is_margin_threshold_exceeded_notfication_sent = False diff --git a/project_minimum_margin_threshold_alert/models/res_config_settings.py b/project_minimum_margin_threshold_alert/models/res_config_settings.py new file mode 100644 index 0000000000..4467f89847 --- /dev/null +++ b/project_minimum_margin_threshold_alert/models/res_config_settings.py @@ -0,0 +1,23 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + project_margin_threshold = fields.Float( + config_parameter="project_margin_threshold_alert.project_margin_threshold", + help="Check this to define a default margin", + ) + project_margin_threshold_send_email = fields.Boolean( + config_parameter="project_margin_threshold_alert.project_margin_threshold_send_email", + help="Check this to enable by default the sending of emails" + "to internal users when project margin is exceeded.", + ) + project_margin_threshold_create_activity = fields.Boolean( + config_parameter="project_margin_threshold_alert.project_margin_threshold_create_activity", + help="Check this to enable by default the creation of activity" + "for dedicated project manager when project margin threshold (minimum) is .", + ) diff --git a/project_minimum_margin_threshold_alert/models/res_users.py b/project_minimum_margin_threshold_alert/models/res_users.py new file mode 100644 index 0000000000..d26fd5440c --- /dev/null +++ b/project_minimum_margin_threshold_alert/models/res_users.py @@ -0,0 +1,14 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + receive_project_margin_threshold_notification = fields.Boolean( + string="Activate minimum margin notifications for projects", + help="Notify user about project minimum margin threshold exceeding", + default=True, + ) diff --git a/project_minimum_margin_threshold_alert/pyproject.toml b/project_minimum_margin_threshold_alert/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/project_minimum_margin_threshold_alert/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/project_minimum_margin_threshold_alert/readme/CONFIGURE.md b/project_minimum_margin_threshold_alert/readme/CONFIGURE.md new file mode 100644 index 0000000000..32bfb1a5c5 --- /dev/null +++ b/project_minimum_margin_threshold_alert/readme/CONFIGURE.md @@ -0,0 +1,22 @@ +**1. Configure the default minimum margin threshold** +- Go to Settings > Project > Set the default threshold percentage +![screenshot.png](../static/description/screenshot.png) + +**2. Configure a project-specific minimum margin threshold (optional)** +- Go to Project > Open a project > Manage notifications section > Define a specific threshold for this project. + +**3. Manage notification recipients** +- Define a Project Manager and followers (subscribers) for the project +These users will receive notifications when the threshold is reached. + +**4. User notification preferences** +- Go to Settings > Open a user > Configure the user’s notification preferences. +![screenshot1.png](../static/description/screenshot1.png) + +**5. Project notification preferences** + +When project costs exceed the configured percentage of revenues, the system automatically sends (via a cron) +- An email to manager and internal user followers if the Force Email Notification option is enabled from the project. +- An activity of type 'Minimum Margin Exceeded' for the project manager if the Create Activity option is enabled from the project. + +![screenshot2.png](../static/description/screenshot2.png) diff --git a/project_minimum_margin_threshold_alert/readme/CONTRIBUTORS.md b/project_minimum_margin_threshold_alert/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..b8773baf79 --- /dev/null +++ b/project_minimum_margin_threshold_alert/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Nihel GABSI (https://www.acsone.eu/) diff --git a/project_minimum_margin_threshold_alert/readme/DESCRIPTION.md b/project_minimum_margin_threshold_alert/readme/DESCRIPTION.md new file mode 100644 index 0000000000..3b461b09ac --- /dev/null +++ b/project_minimum_margin_threshold_alert/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +This module extends the Odoo Project application to support automatic margin minimum threshold alerts. +It allows companies to define a default margin limit (for example when project costs exceed a given percentage of +revenues) and to apply it to projects that do not have a specific alert configuration. Each project can also define its +own threshold. + +When the margin threshold is reached or exceeded, the system sends notifications to the project manager and to internal subscribers +of the project. The notification method follows the user’s notification preferences, with an option to force the sending +of an email even if the user is configured to receive only in-app notifications. This module helps project managers anticipate +financial risks and improves internal communication around project margin. diff --git a/project_minimum_margin_threshold_alert/static/description/icon.png b/project_minimum_margin_threshold_alert/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/project_minimum_margin_threshold_alert/static/description/icon.png differ diff --git a/project_minimum_margin_threshold_alert/static/description/index.html b/project_minimum_margin_threshold_alert/static/description/index.html new file mode 100644 index 0000000000..e7d0de457e --- /dev/null +++ b/project_minimum_margin_threshold_alert/static/description/index.html @@ -0,0 +1,474 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Project Minimum Margin Threshold Alert

+ +

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

+

This module extends the Odoo Project application to support automatic +margin minimum threshold alerts. It allows companies to define a default +margin limit (for example when project costs exceed a given percentage +of revenues) and to apply it to projects that do not have a specific +alert configuration. Each project can also define its own threshold.

+

When the margin threshold is reached or exceeded, the system sends +notifications to the project manager and to internal subscribers of the +project. The notification method follows the user’s notification +preferences, with an option to force the sending of an email even if the +user is configured to receive only in-app notifications. This module +helps project managers anticipate financial risks and improves internal +communication around project margin.

+

Table of contents

+ +
+

Configuration

+

1. Configure the default minimum margin threshold

+
    +
  • Go to Settings > Project > Set the default threshold percentage +screenshot.png
  • +
+

2. Configure a project-specific minimum margin threshold (optional)

+
    +
  • Go to Project > Open a project > Manage notifications section > Define +a specific threshold for this project.
  • +
+

3. Manage notification recipients

+
    +
  • Define a Project Manager and followers (subscribers) for the project +These users will receive notifications when the threshold is reached.
  • +
+

4. User notification preferences

+
    +
  • Go to Settings > Open a user > Configure the user’s notification +preferences. screenshot1.png
  • +
+

5. Project notification preferences

+

When project costs exceed the configured percentage of revenues, the +system automatically sends (via a cron)

+
    +
  • An email to manager and internal user followers if the Force Email +Notification option is enabled from the project.
  • +
  • An activity of type ‘Minimum Margin Exceeded’ for the project manager +if the Create Activity option is enabled from the project.
  • +
+

screenshot2.png

+
+
+

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

+
    +
  • ACSONE SA/NV
  • +
+
+ +
+

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.

+

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_minimum_margin_threshold_alert/static/description/screenshot.png b/project_minimum_margin_threshold_alert/static/description/screenshot.png new file mode 100644 index 0000000000..f027b9fb1a Binary files /dev/null and b/project_minimum_margin_threshold_alert/static/description/screenshot.png differ diff --git a/project_minimum_margin_threshold_alert/static/description/screenshot1.png b/project_minimum_margin_threshold_alert/static/description/screenshot1.png new file mode 100644 index 0000000000..9e3cd5caf2 Binary files /dev/null and b/project_minimum_margin_threshold_alert/static/description/screenshot1.png differ diff --git a/project_minimum_margin_threshold_alert/static/description/screenshot2.png b/project_minimum_margin_threshold_alert/static/description/screenshot2.png new file mode 100644 index 0000000000..79371a5c23 Binary files /dev/null and b/project_minimum_margin_threshold_alert/static/description/screenshot2.png differ diff --git a/project_minimum_margin_threshold_alert/tests/__init__.py b/project_minimum_margin_threshold_alert/tests/__init__.py new file mode 100644 index 0000000000..b85bc85ed2 --- /dev/null +++ b/project_minimum_margin_threshold_alert/tests/__init__.py @@ -0,0 +1 @@ +from . import test_project_minimum_margin_alert diff --git a/project_minimum_margin_threshold_alert/tests/test_project_minimum_margin_alert.py b/project_minimum_margin_threshold_alert/tests/test_project_minimum_margin_alert.py new file mode 100644 index 0000000000..0e81769bd9 --- /dev/null +++ b/project_minimum_margin_threshold_alert/tests/test_project_minimum_margin_alert.py @@ -0,0 +1,223 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from unittest import mock + +from odoo.addons.base.tests.common import BaseCommon +from odoo.addons.mail.models.mail_thread import MailThread +from odoo.addons.project.models.project_project import ProjectProject + + +class TestProjectmarginAlert(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env.user.group_ids += cls.env.ref("project.group_project_manager") + + cls.user_a = cls.env["res.users"].create( + {"name": "User A", "login": "testa@test.com"} + ) + cls.user_a.group_ids |= cls.env.ref("project.group_project_user") + + cls.partner = cls.env["res.partner"].create( + {"name": "Georges", "email": "georges@project-margin.com"} + ) + + cls.analytic_plan = cls.env["account.analytic.plan"].create( + { + "name": "Plan A", + } + ) + cls.analytic_account = cls.env["account.analytic.account"].create( + { + "name": "Project - AA", + "code": "AA-1234", + "plan_id": cls.analytic_plan.id, + } + ) + cls.project = cls.env["project.project"].create( + { + "name": "Project", + "partner_id": cls.partner.id, + "account_id": cls.analytic_account.id, + } + ) + cls.task = cls.env["project.task"].create( + { + "name": "Task", + "project_id": cls.project.id, + } + ) + cls.project_margin_items_empty = { + "revenues": {"data": [], "total": {"invoiced": 0.0, "to_invoice": 0.0}}, + "costs": {"data": [], "total": {"billed": 0.0, "to_bill": 0.0}}, + } + cls.project_margin_items_90 = { + "revenues": {"data": [], "total": {"invoiced": 90.0, "to_invoice": 0.0}}, + "costs": {"data": [], "total": {"billed": -100.0, "to_bill": 0.0}}, + } + cls.project_margin_items_33 = { + "revenues": {"data": [], "total": {"invoiced": 100.0, "to_invoice": 0.0}}, + "costs": {"data": [], "total": {"billed": -75.0, "to_bill": 0.0}}, + } + cls.project_margin_items_15 = { + "revenues": {"data": [], "total": {"invoiced": 100.0, "to_invoice": 0.0}}, + "costs": {"data": [], "total": {"billed": -90.0, "to_bill": 0.0}}, + } + cls.project_margin_items_75 = { + "revenues": {"data": [], "total": {"invoiced": 75.0, "to_invoice": 0.0}}, + "costs": {"data": [], "total": {"billed": -100.0, "to_bill": 0.0}}, + } + cls.project_margin_items_100 = { + "revenues": {"data": [], "total": {"invoiced": 75.0, "to_invoice": 0.0}}, + "costs": {"data": [], "total": {"billed": 0, "to_bill": 0.0}}, + } + cls.foreign_currency = cls.env["res.currency"].create( + { + "name": "Chaos orb", + "symbol": "☺", + "rounding": 0.001, + "position": "after", + "currency_unit_label": "Chaos", + "currency_subunit_label": "orb", + } + ) + cls.env["res.currency.rate"].create( + { + "name": "2016-01-01", + "rate": "5.0", + "currency_id": cls.foreign_currency.id, + } + ) + + def _get_project_messages(self, project=None): + if project is None: + project = self.project + return self.env["mail.message"].search( + [("res_id", "=", project.id), ("model", "=", "project.project")] + ) + + def test_margin_not_exceeded_zero(self): + # Set threshold at 80% + # Get 0.0 values of invoiced/billed + # Threshold is not exceeded + with mock.patch.object( + ProjectProject, "_get_profitability_items" + ) as margin_mock: + margin_mock.return_value = self.project_margin_items_empty + self.project.margin_threshold = 0.2 + self.project._update_is_margin_threshold_exceeded() + self.assertFalse(self.project.is_margin_threshold_exceeded) + + def test_margin_not_exceeded_33(self): + # Set threshold at 80% + # Get 0.0 values of invoiced/billed + # Threshold is not exceeded + with mock.patch.object( + ProjectProject, "_get_profitability_items" + ) as margin_mock: + margin_mock.return_value = self.project_margin_items_33 + self.project.margin_threshold = 0.2 + self.project._update_is_margin_threshold_exceeded() + self.assertFalse(self.project.is_margin_threshold_exceeded) + + def test_margin_not_exceeded_15(self): + # Set threshold at 80% + # Get 0.0 values of invoiced/billed + # Threshold is not exceeded + with mock.patch.object( + ProjectProject, "_get_profitability_items" + ) as margin_mock: + margin_mock.return_value = self.project_margin_items_15 + self.project.margin_threshold = 0.2 + self.project._update_is_margin_threshold_exceeded() + self.assertTrue(self.project.is_margin_threshold_exceeded) + + def test_margin_not_exceeded_no_costs(self): + # Set threshold at 80% + # Get 0.0 values of invoiced/billed + # Threshold is not exceeded + with mock.patch.object( + ProjectProject, "_get_profitability_items" + ) as margin_mock: + margin_mock.return_value = self.project_margin_items_100 + self.project.margin_threshold = 0.2 + self.project._update_is_margin_threshold_exceeded() + self.assertFalse(self.project.is_margin_threshold_exceeded) + + def test_margin_exceeded(self): + # Set threshold at 80% + # Get values of invoiced 90/billed(100) + # Threshold is exceeded + with mock.patch.object( + ProjectProject, "_get_profitability_items" + ) as margin_mock: + margin_mock.return_value = self.project_margin_items_90 + self.project.margin_threshold = 0.2 + self.project._update_is_margin_threshold_exceeded() + self.assertTrue(self.project.is_margin_threshold_exceeded) + + def test_margin_exceeded_notifications(self): + # Set threshold at 80% + # Get values of invoiced 90/billed(100) + # Threshold is exceeded + # User a is follower of project + # Send notifications + self.project.message_subscribe(self.user_a.partner_id.ids) + messages_before = self._get_project_messages() + with mock.patch.object( + ProjectProject, "_get_profitability_items" + ) as margin_mock: + margin_mock.return_value = self.project_margin_items_90 + self.project.margin_threshold = 0.2 + self.project._update_is_margin_threshold_exceeded() + self.assertTrue(self.project.is_margin_threshold_exceeded) + self.project._cron_margin_threshold_exceeded() + messages_after = self._get_project_messages() + new_messages = messages_after - messages_before + self.assertEqual(self.user_a.partner_id, new_messages.notified_partner_ids) + self.assertTrue( + any("Cost threshold exceeded" in message.body for message in new_messages) + ) + + def test_margin_exceeded_activity(self): + # Set threshold at 80% + # Get values of invoiced 90/billed(100) + # Threshold is exceeded + # User a is follower of project + # Create activities + self.project.create_margin_threshold_activity = True + activities_before = self.env["mail.activity"].search( + [("user_id", "=", self.env.user.id)] + ) + with mock.patch.object( + ProjectProject, "_get_profitability_items" + ) as margin_mock: + margin_mock.return_value = self.project_margin_items_90 + self.project.margin_threshold = 0.2 + self.project._update_is_margin_threshold_exceeded() + self.assertTrue(self.project.is_margin_threshold_exceeded) + self.project._cron_margin_threshold_exceeded() + activities = ( + self.env["mail.activity"].search([("user_id", "=", self.env.user.id)]) + - activities_before + ) + self.assertEqual(len(activities), 1) + self.assertEqual(activities.display_name, "Minimum Margin Exceeded") + + def test_margin_exceeded_mail(self): + # Set threshold at 80% + # Get values of invoiced 90/billed(100) + # Threshold is exceeded + # User a is follower of project + # Send mail + self.project.force_margin_threshold_notification = True + with mock.patch.object( + ProjectProject, "_get_profitability_items" + ) as margin_mock: + margin_mock.return_value = self.project_margin_items_90 + self.project.margin_threshold = 0.2 + self.project._update_is_margin_threshold_exceeded() + self.assertTrue(self.project.is_margin_threshold_exceeded) + with mock.patch.object(MailThread, "message_post_with_source") as post_mock: + self.project._cron_margin_threshold_exceeded() + post_mock.assert_called() diff --git a/project_minimum_margin_threshold_alert/views/project_project.xml b/project_minimum_margin_threshold_alert/views/project_project.xml new file mode 100644 index 0000000000..04b746dd37 --- /dev/null +++ b/project_minimum_margin_threshold_alert/views/project_project.xml @@ -0,0 +1,54 @@ + + + + + project.project + + +
+ +
+ + + + + + + + + + + + + +
+
+ + + project.project.select + project.project + + + + + + + + + +
diff --git a/project_minimum_margin_threshold_alert/views/res_config_settings.xml b/project_minimum_margin_threshold_alert/views/res_config_settings.xml new file mode 100644 index 0000000000..157639aec2 --- /dev/null +++ b/project_minimum_margin_threshold_alert/views/res_config_settings.xml @@ -0,0 +1,56 @@ + + + + + res.config.settings + + + + + +
+
+
+ Percentage of project costs compared to project revenues that + triggers an alert. For example, a value of 50 means an alert will be sent when project costs + reach + 50% of the project revenues. +
+
+ + + + + + + + +
+
+
+
+
diff --git a/project_minimum_margin_threshold_alert/views/res_users.xml b/project_minimum_margin_threshold_alert/views/res_users.xml new file mode 100644 index 0000000000..141b400f27 --- /dev/null +++ b/project_minimum_margin_threshold_alert/views/res_users.xml @@ -0,0 +1,17 @@ + + + + + res.users + + + + + + + +