Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions spp_mis_demo_v2/models/mis_demo_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1411,10 +1411,12 @@ def _ensure_program_managers(self, program):
if program[field]:
continue
for mgr_obj, def_mgr_obj in mapping.items():
# Create the concrete manager implementation and link via wrapper
# Create the concrete manager implementation and link via
# wrapper. Each concrete model's default_get() supplies a
# method-specific name; we don't pass "Default" anymore —
# see #941 round 2 / item 3.
def_mgr = self.env[def_mgr_obj].create(
{
"name": "Default",
"program_id": program.id,
}
)
Expand Down
3 changes: 2 additions & 1 deletion spp_programs/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"name": "OpenSPP Programs",
"summary": "Manage programs, cycles, beneficiary enrollment, entitlements (cash and in-kind), payments, and fund tracking for social protection.",
"category": "OpenSPP/Core",
"version": "19.0.2.0.10",
"version": "19.0.2.0.11",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
Expand Down Expand Up @@ -109,6 +109,7 @@
"wizard/create_program_wizard_compliance_views.xml",
"wizard/create_program_wizard_cel_views.xml",
"wizard/enrollment_wizard_views.xml",
"wizard/exit_membership_wizard.xml",
"wizard/prepare_entitlement_confirm_wizard.xml",
],
"assets": {
Expand Down
58 changes: 58 additions & 0 deletions spp_programs/migrations/19.0.2.0.11/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Replace placeholder `name = "Default"` on default-manager records with
their method-specific labels — see #941 round 2 / item 3.

Existing programs upgraded from a prior version still carry rows whose
`name` literally reads "Default" (the value used by
`SPPProgram.create_default_managers` before the cleanup). New rows are
fine because each concrete model now seeds its own meaningful name via
`default_get`. This migration backfills the historical rows so the Edit
form shows e.g. "CEL Eligibility Criteria" instead of "Default".
"""

import logging

from psycopg2 import sql

_logger = logging.getLogger(__name__)


# (table_name, label) pairs. Tables are derived from the concrete-manager
# `_name` (dots replaced by underscores).
_DEFAULT_NAME_RENAMES = (
("spp_program_membership_manager_default", "CEL Eligibility Criteria"),
("spp_program_entitlement_manager_default", "Basic Cash"),
("spp_program_entitlement_manager_cash", "Cash Entitlement"),
("spp_program_entitlement_manager_inkind", "In-kind Entitlement"),
("spp_cycle_manager_default", "Default Cycle Schedule"),
("spp_compliance_manager_default", "CEL Compliance Criteria"),
("spp_program_payment_manager_default", "Default Payment"),
("spp_program_manager_default", "Default Program Manager"),
("spp_deduplication_manager_default", "Default Deduplication"),
)


def migrate(cr, version):
if not version:
return
for table, label in _DEFAULT_NAME_RENAMES:
# Skip tables that don't exist (modules not installed in this DB).
cr.execute(
"SELECT 1 FROM information_schema.tables WHERE table_name = %s",
(table,),
)
if not cr.fetchone():
continue
cr.execute(
sql.SQL("UPDATE {tbl} SET name = %s WHERE name = 'Default'").format(
tbl=sql.Identifier(table),
),
(label,),
)
if cr.rowcount:
_logger.info(
"Renamed %d %s rows from 'Default' to %r",
cr.rowcount,
table,
label,
)
10 changes: 10 additions & 0 deletions spp_programs/models/cel/eligibility_cel.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ class DefaultEligibilityManagerCEL(models.Model):
help="True if the local expression differs from the source template",
)

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label so the
record's actual `name` (visible on the Edit form) reads "CEL
Eligibility Criteria" instead of the placeholder "Default"."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("CEL Eligibility Criteria"))
return res

# -------------------------------------------------------------------------
# CEL Profile (computed for widget context)
# -------------------------------------------------------------------------
Expand Down
21 changes: 21 additions & 0 deletions spp_programs/models/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,27 @@ def _get_approval_definition(self):
self.ensure_one()
return self.cycle_approval_definition_id

def _notify_thread_by_email(self, message, recipients_data, **kwargs):
"""Suppress outgoing email when the parent program has email
notifications disabled. Chatter logging and in-app notifications are
unaffected — only the email dispatch is short-circuited."""
self.ensure_one()
if self.program_id and not self.program_id._should_send_email_notifications():
return
return super()._notify_thread_by_email(message, recipients_data, **kwargs)

def _create_approval_activity(self, definition, review):
"""Gate the approver-email path on the parent program's toggle.

spp.approval.mixin schedules a mail.activity for each approver on
submit; the activity dispatch sends email through the assignee's
notification preferences. Skip the scheduling entirely when the
program has email notifications turned off."""
self.ensure_one()
if self.program_id and not self.program_id._should_send_email_notifications():
return
return super()._create_approval_activity(definition, review)
Comment on lines +621 to +631
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Overriding _create_approval_activity to return None when email notifications are disabled suppresses the creation of the Odoo Activity entirely. This prevents approvers from seeing the task in their Odoo systray or on the record's chatter, which might break the in-app workflow. If the intent is only to suppress the email notification, overriding _notify_thread_by_email (as done above) is sufficient, as it is called during activity creation to send the mail. Furthermore, returning None instead of a recordset might cause tracebacks in callers expecting an activity object.


@api.onchange("start_date")
def on_start_date_change(self):
self.program_id.get_manager(constants.MANAGER_CYCLE).on_start_date_change(self)
Expand Down
43 changes: 42 additions & 1 deletion spp_programs/models/entitlement.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def _generate_code(self):

is_cash_entitlement = fields.Boolean("Cash Entitlement", default=False)
currency_id = fields.Many2one("res.currency", readonly=True, related="journal_id.currency_id")
initial_amount = fields.Monetary(required=True, currency_field="currency_id")
initial_amount = fields.Monetary(string="Amount", required=True, currency_field="currency_id")
transfer_fee = fields.Monetary(currency_field="currency_id", default=0.0)
balance = fields.Monetary(compute="_compute_balance") # in company currency
# TODO: implement transactions against this entitlement
Expand Down Expand Up @@ -390,6 +390,29 @@ def _get_approval_definition(self):
self.ensure_one()
return self.entitlement_approval_definition_id

def _notify_thread_by_email(self, message, recipients_data, **kwargs):
"""Suppress outgoing email when the parent program has email
notifications disabled. Chatter logging and in-app notifications are
unaffected — only the email dispatch is short-circuited."""
self.ensure_one()
program = self.cycle_id.program_id
if program and not program._should_send_email_notifications():
return
return super()._notify_thread_by_email(message, recipients_data, **kwargs)

def _create_approval_activity(self, definition, review):
"""Gate the approver-email path on the parent program's toggle.

spp.approval.mixin schedules a mail.activity for each approver on
submit; the activity dispatch sends email through the assignee's
notification preferences. Skip the scheduling entirely when the
program has email notifications turned off."""
self.ensure_one()
program = self.cycle_id.program_id
if program and not program._should_send_email_notifications():
return
return super()._create_approval_activity(definition, review)
Comment on lines +403 to +414
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Overriding _create_approval_activity to return None when email notifications are disabled suppresses the creation of the Odoo Activity entirely. This prevents approvers from seeing the task in their Odoo systray or on the record's chatter, which might break the in-app workflow. If the intent is only to suppress the email notification, overriding _notify_thread_by_email (as done above) is sufficient, as it is called during activity creation to send the mail. Furthermore, returning None instead of a recordset might cause tracebacks in callers expecting an activity object.


def action_submit_for_approval(self):
"""Submit entitlement for approval using standardized workflow."""
for record in self:
Expand Down Expand Up @@ -885,6 +908,24 @@ def _resolve_approval_definition(self):
self.ensure_one()
return self._get_approval_definition()

def _notify_thread_by_email(self, message, recipients_data, **kwargs):
"""Suppress outgoing email when the parent program has email
notifications disabled. Chatter logging and in-app notifications are
unaffected — only the email dispatch is short-circuited."""
self.ensure_one()
program = self.cycle_id.program_id
if program and not program._should_send_email_notifications():
return
return super()._notify_thread_by_email(message, recipients_data, **kwargs)

def _create_approval_activity(self, definition, review):
"""Gate the approver-email path on the parent program's toggle."""
self.ensure_one()
program = self.cycle_id.program_id
if program and not program._should_send_email_notifications():
return
return super()._create_approval_activity(definition, review)
Comment on lines +921 to +927
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Overriding _create_approval_activity to return None when email notifications are disabled suppresses the creation of the Odoo Activity entirely. This prevents approvers from seeing the task in their Odoo systray or on the record's chatter, which might break the in-app workflow. If the intent is only to suppress the email notification, overriding _notify_thread_by_email (as done above) is sufficient, as it is called during activity creation to send the mail. Furthermore, returning None instead of a recordset might cause tracebacks in callers expecting an activity object.


def action_submit_for_approval(self):
"""Submit in-kind entitlement for approval using standardized workflow."""
for record in self:
Expand Down
8 changes: 8 additions & 0 deletions spp_programs/models/managers/compliance_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ class DefaultComplianceManager(models.Model):
_inherit = ["spp.compliance.manager.base", "spp.manager.source.mixin"]
_description = "Default Compliance Manager"

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("CEL Compliance Criteria"))
return res

# Mode selection
compliance_cel_mode = fields.Selection(
selection=[
Expand Down
8 changes: 8 additions & 0 deletions spp_programs/models/managers/cycle_manager_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,14 @@ class DefaultCycleManager(models.Model):
]
_description = "Default Cycle Manager"

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("Default Cycle Schedule"))
return res

cycle_duration = fields.Integer(default=1, required=True, string="Recurrence")
approver_group_id = fields.Many2one(
comodel_name="res.groups",
Expand Down
10 changes: 9 additions & 1 deletion spp_programs/models/managers/deduplication_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import logging
from datetime import date

from odoo import Command, api, fields, models
from odoo import Command, _, api, fields, models

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,6 +53,14 @@ class DefaultDeduplication(models.Model):
_capability_individual = True
_capability_group = True

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("Default Deduplication"))
return res

def deduplicate_beneficiaries(self, states):
for rec in self:
duplicate_beneficiaries = []
Expand Down
8 changes: 8 additions & 0 deletions spp_programs/models/managers/entitlement_manager_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,14 @@ class DefaultCashEntitlementManager(models.Model):
# Set to True so that the UI will display the payment management components
IS_CASH_ENTITLEMENT = True

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("Basic Cash"))
return res

amount_per_cycle = fields.Monetary(
currency_field="currency_id",
aggregator="sum",
Expand Down
8 changes: 8 additions & 0 deletions spp_programs/models/managers/entitlement_manager_cash.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ class SppCashEntitlementManager(models.Model):
# Set to True so that the UI will display the payment management components
IS_CASH_ENTITLEMENT = True

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("Cash Entitlement"))
return res

# Cash Entitlement Manager
is_evaluate_one_item = fields.Boolean(default=False)
entitlement_item_ids = fields.One2many(
Expand Down
8 changes: 8 additions & 0 deletions spp_programs/models/managers/entitlement_manager_inkind.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class SPPInKindEntitlementManager(models.Model):
# Set to False so that the UI will not display the payment management components
IS_CASH_ENTITLEMENT = False

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("In-kind Entitlement"))
return res

@api.model
def _default_warehouse_id(self):
return self.env["stock.warehouse"].search([("company_id", "=", self.env.company.id)], limit=1)
Expand Down
7 changes: 6 additions & 1 deletion spp_programs/models/managers/manager_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _compute_display_name(self):
def _selection_manager_ref_id(self):
return []

def open_manager_form(self, readonly=False):
def open_manager_form(self, readonly=False, title=None):
self.ensure_one()
if self.manager_ref_id:
# Get the res_model and res_id from the manager_ref_id (reference field)
Expand All @@ -52,6 +52,11 @@ def open_manager_form(self, readonly=False):
"flags": {"mode": "readonly" if readonly else "edit"},
}
)
# Override the dialog title so the banner label (e.g. "Who
# Qualifies?") shows in the modal header instead of the model's
# _description ("Odoo" when unset).
if title:
action["name"] = title
return action

return {
Expand Down
51 changes: 51 additions & 0 deletions spp_programs/models/managers/payment_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,57 @@ class DefaultFilePaymentManager(models.Model):
MAX_PAYMENTS_FOR_SYNC_PREPARE = 200
MAX_BATCHES_FOR_SYNC_SEND = 50

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("Default Payment"))
return res

@api.model_create_multi
def create(self, vals_list):
"""Auto-create the default batch tag when needed.

The `batch_tag_ids` constraint requires at least one tag when
`create_batch=True` (the default). The legacy flow relied on
the user toggling `create_batch` in the form to fire the
onchange that creates the tag — that doesn't fire on the form's
initial open, so the program form's `+ Add` button used to
pre-create the tag itself (#952). Pre-creation leaves an
orphan batch tag in the DB if the user dismisses the dialog
with `X` (#953). Doing it here means the tag is only created
atomically with the manager record on Save.
"""
for vals in vals_list:
if not vals.get("create_batch", True) or vals.get("batch_tag_ids"):
continue
program_id = vals.get("program_id") or self.env.context.get("default_program_id")
if not program_id:
continue
program = self.env["spp.program"].browse(program_id)
tag_name = f"Default {program.name}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The tag name string is partially hardcoded in English. It should be fully translatable to support multi-language environments.

Suggested change
tag_name = f"Default {program.name}"
tag_name = _("Default %s") % program.name

BatchTag = self.env["spp.payment.batch.tag"].sudo() # nosemgrep: odoo-sudo-without-context
tag = BatchTag.search(
[
("name", "=", tag_name),
("order", "=", 1),
("max_batch_size", "=", 500),
],
limit=1,
)
if not tag:
tag = BatchTag.create(
{
"name": tag_name,
"order": 1,
"domain": [],
"max_batch_size": 500,
}
)
vals["batch_tag_ids"] = [(4, tag.id)]
return super().create(vals_list)
Comment on lines +109 to +136
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In api.model_create_multi, if vals_list contains multiple records for the same program, the search for the default batch tag might not find a tag created in a previous iteration of the same loop because the database hasn't been flushed yet. This could lead to duplicate tag creation or unique constraint violations. Consider using a local cache (dictionary) to track tags created during the execution of this method.


currency_id = fields.Many2one("res.currency", related="program_id.journal_id.currency_id", readonly=True)

create_batch = fields.Boolean("Automatically Create Batch", default=True)
Expand Down
8 changes: 8 additions & 0 deletions spp_programs/models/managers/program_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class DefaultProgramManager(models.Model):
_inherit = ["spp.base.program.manager", "spp.manager.source.mixin"]
_description = "Default Program Manager"

@api.model
def default_get(self, fields_list):
"""Default the manager name to its method-specific label."""
res = super().default_get(fields_list)
if "name" in fields_list:
res.setdefault("name", _("Default Program Manager"))
return res

number_of_cycles = fields.Integer(default=1)
copy_last_cycle_on_new_cycle = fields.Boolean(string="Copy previous cycle", default=True)

Expand Down
Loading