Skip to content

[19.0][ADD] project_profitability_threshold_alert: Notify project followers and project manager about costs exceeding based on a set threshold#1660

Draft
nihelgabsi-acsone wants to merge 6 commits intoOCA:19.0from
acsone:19.0-project_budget_threshold_alert-nga
Draft

[19.0][ADD] project_profitability_threshold_alert: Notify project followers and project manager about costs exceeding based on a set threshold#1660
nihelgabsi-acsone wants to merge 6 commits intoOCA:19.0from
acsone:19.0-project_budget_threshold_alert-nga

Conversation

@nihelgabsi-acsone
Copy link
Copy Markdown

@nihelgabsi-acsone nihelgabsi-acsone commented Jan 22, 2026

Notify followers and project manager about costs exceeding based on a set threshold

… and project manager about budget exceeding based on a set threshold
@nihelgabsi-acsone nihelgabsi-acsone marked this pull request as draft January 22, 2026 08:31
@nihelgabsi-acsone nihelgabsi-acsone changed the title [19.0][ADD] project_budget_threshold_alert : Notify project followers… [19.0][ADD] project_budget_threshold_alert : Notify project followers and project manager about budget exceeding based on a set threshold Jan 22, 2026
@nihelgabsi-acsone nihelgabsi-acsone force-pushed the 19.0-project_budget_threshold_alert-nga branch 2 times, most recently from 050db67 to be238cd Compare January 28, 2026 14:34
@nihelgabsi-acsone nihelgabsi-acsone changed the title [19.0][ADD] project_budget_threshold_alert : Notify project followers and project manager about budget exceeding based on a set threshold [19.0][ADD] project_profitability_threshold_alert: Notify project followers and project manager about costs exceeding based on a set threshold Jan 28, 2026
@nihelgabsi-acsone nihelgabsi-acsone force-pushed the 19.0-project_budget_threshold_alert-nga branch from be238cd to 5a29fe3 Compare January 28, 2026 15:42
def _cron_find_costs_threshold_exceeded(self):
projects = self.env["project.project"].search([("active", "=", True)])
for project in projects:
project._compute_is_cost_exceeded()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary ? The compute is triggered by its depends.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to execute _get_profitability_items() to get updated data related to project costs and revenues

projects = self.env["project.project"].search([("active", "=", True)])
for project in projects:
project._compute_is_cost_exceeded()
if project.is_cost_exceeded and not project.is_notfication_sent:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those fields should be included in search criteria (add a dedicated method for the domain like _get_costs_threshold_notification_domain())

email_layout_xmlid=None,
)

@api.depends("costs_threshold", "company_id.project_costs_threshold")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, no need of company_id.project_cost_threshold

internal_user_followers += manager
return internal_user_followers

@api.onchange("costs_threshold")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set is_notification_sent as computed / store=True depending on costs_threshold to avoid onchanges.

- Rename module to be more generic
- Add costs vs revenues threshold
@nihelgabsi-acsone nihelgabsi-acsone force-pushed the 19.0-project_budget_threshold_alert-nga branch from 5a29fe3 to 1653c40 Compare January 29, 2026 09:19
@rousseldenis rousseldenis force-pushed the 19.0-project_budget_threshold_alert-nga branch from 305f5bc to 600cc54 Compare January 30, 2026 17:53
Copy link
Copy Markdown
Contributor

@alexey-pelykh alexey-pelykh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice module, the margin threshold alert with cron + activity + email is well thought out. Small typo: "Threashold" should be "Threshold" in the mail template subject and body.

License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="project_minimum_margin_exceeded_template" model="mail.template">
<field name="name">Project: Minimum Margin threashold exceeded</field>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: threasholdthreshold (in the template name and subject line)

Copy link
Copy Markdown
Contributor

@alexey-pelykh alexey-pelykh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: project_minimum_margin_threshold_alert (Draft)

This PR is marked as draft/WIP, so this is an early-stage review to help guide development. The module concept -- alerting project stakeholders when margin thresholds are breached -- is valuable and well-structured. Below are findings to address before this moves out of draft.


PR Title vs Module Name Mismatch

The PR title references project_profitability_threshold_alert but the module is named project_minimum_margin_threshold_alert. The title should be updated to match.


Critical / High Priority

1. Typo in field name: notfication instead of notification

The field is_margin_threshold_exceeded_notfication_sent has a typo that propagates through the codebase (model, views, filters, i18n). Since this is a stored+indexed field, the typo will be baked into the database column name. This should be fixed now before any deployment.

2. _cron_margin_threshold_exceeded searches ALL projects then marks ALL as notified

At line 46 of project_project.py, the cron does projects = self.env["project.project"].search([]) then at the end sets projects.is_margin_threshold_exceeded_notfication_sent = True on ALL projects -- even those that were NOT notified (filtered out by the domain). This incorrectly marks projects that don't meet the notification criteria as "sent", preventing future notifications if their status changes.

3. _get_profitability_values() does not exist in standard Odoo project.project

The _update_is_margin_threshold_exceeded method calls project._get_profitability_values() which returns (margin_values, _show) and then reads margin_values.get("expected_percentage", "0"). However, the standard Odoo project module provides _get_profitability_items() (which the tests mock), not _get_profitability_values(). This will raise an AttributeError at runtime. The tests pass because they mock _get_profitability_items but the actual code calls a different method. The test mocking and the runtime code are out of sync.

4. Missing ir.model.access (security rules)

The __manifest__.py does not list any security/ir.model.access.csv file. While the module inherits existing models (project.project, res.users, res.config.settings), OCA guidelines typically require this file to be present. Verify whether any new model access rules or record rules are needed.


Moderate Priority

5. Typo: "Threashold" in mail template

mail_template.xml has "threashold" (should be "threshold") in both the template name and subject line. This was noted in a previous review but appears to still be present.

6. Incomplete help texts in res_config_settings.py

  • project_margin_threshold_create_activity help: "...when project margin threshold (minimum) is ." -- sentence is incomplete ("is" what?).
  • project_margin_threshold_send_email help: "...sending of emailsto internal users..." -- missing space between "emails" and "to".

7. _default_margin_threshold uses @property with float() on potentially falsy value

If the config parameter is not set, get_param() returns False. Calling float(False) returns 0.0 which works, but this is fragile. Consider using get_param(..., default=0.0) for clarity.

8. Missing __all__ or explicit exports consideration

The import from odoo.addons.base.models.res_users import ResUsers in project_project.py is used only as a type hint in _get_internal_users_for_margin_threshold. This direct import of a specific model class is unusual in Odoo modules and couples to internal structure. Consider using self.env["res.users"] return type or standard Odoo patterns instead.

9. Cron defined without noupdate="1"

data/cron.xml does not use noupdate="1". This means module updates will reset any user customization of the cron schedule. Consider wrapping it in <odoo noupdate="1">.

10. .pot file has extra entries not in .po

The .pot file contains "margin Notifications Management" (lowercase 'm') and "margin notifications" (lowercase) which differ from the .po file entries ("Margin Notifications Management" and "Margin Notifications"). This suggests the view XML was updated but translations were not regenerated.


Minor / Suggestions

11. Test naming is misleading

test_margin_not_exceeded_15 actually asserts assertTrue(self.project.is_margin_threshold_exceeded), contradicting the method name which says "not exceeded".

12. Consider adding installable: True to manifest

While it defaults to True, OCA convention is to be explicit.

13. fr.po is missing newline at end of file


Review posted via CorporateHub OCA review campaign

tracking=True,
)
is_margin_threshold_exceeded_notfication_sent = fields.Boolean(
compute="_compute_is_margin_threshold_exceeded_notfication_sent",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: notfication should be notification. Since this is a stored + indexed field, this typo will become the database column name and propagate everywhere (views, filters, translations). Better to fix it now before any deployment.

Suggested change
compute="_compute_is_margin_threshold_exceeded_notfication_sent",
is_margin_threshold_exceeded_notification_sent = fields.Boolean(

- Notify users
"""
projects = self.env["project.project"].search([]) # pylint: disable=no-search-all
projects._update_is_margin_threshold_exceeded()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line sets is_margin_threshold_exceeded_notfication_sent = True on ALL searched projects, not just the ones that were actually notified in the loop above. Projects that were filtered out (did not match _get_margin_threshold_to_notify_domain()) will also be incorrectly marked as "sent".

Consider moving this inside the loop to only mark projects that were actually notified:

for project in projects.filtered_domain(...):
    # ... notification logic ...
    project.is_margin_threshold_exceeded_notfication_sent = True

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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls _get_profitability_values() but the tests mock _get_profitability_items(). In standard Odoo 19.0, project.project has _get_profitability_items() which returns a dict (not a tuple). The method _get_profitability_values() does not exist by default.

The test passes because the mock is applied to a different method than the one actually called at runtime. This will likely cause an AttributeError in production.

Please verify which method provides the margin/profitability data you need and align both the code and tests.

)
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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete help text: "...when project margin threshold (minimum) is ." -- the sentence ends with "is ." which appears truncated. Also missing space: "activityfor" should be "activity for".

<odoo>
<record id="project_minimum_margin_exceeded_template" model="mail.template">
<field name="name">Project: Minimum Margin threashold exceeded</field>
<field name="model_id" ref="project.model_project_project" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "threashold" should be "threshold" in both the template name and the subject line (line 8).

<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2026 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider wrapping this in <odoo noupdate="1"> so that module updates do not overwrite any user customizations to the cron schedule (interval, active status, etc.).

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method is named test_margin_not_exceeded_15 but the assertion is self.assertTrue(self.project.is_margin_threshold_exceeded) -- the name says "not exceeded" but the test verifies it IS exceeded. Consider renaming to test_margin_exceeded_15 for clarity.

# 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This direct import of ResUsers from odoo.addons.base.models.res_users is unusual in Odoo modules and couples to an internal module path. It is only used as a type annotation for _get_internal_users_for_margin_threshold. Consider removing this import and using standard Odoo recordset patterns instead (the method can just return self.env["res.users"] typed recordsets without the explicit import).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants