Skip to content

T3227 - new info about survival program for churches in odoo#2109

Open
danpa32 wants to merge 2 commits into
18.0from
T3227-New-Info-about-survival-program-for-churches-in-Odoo
Open

T3227 - new info about survival program for churches in odoo#2109
danpa32 wants to merge 2 commits into
18.0from
T3227-New-Info-about-survival-program-for-churches-in-Odoo

Conversation

@danpa32

@danpa32 danpa32 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Migrate the golden ticket to v18.0
See #2102 for more info

danpa32 and others added 2 commits July 1, 2026 11:07
…porting to v18

- Add sr_survival_* computed fields on res.partner (contracts_report.py)
- Add survival_sponsorship_count stat button and open_survival_sponsorships action
- Add contracts_report_view.xml with xpath targeting //group[@name='last12months']
- Add res_partner_view.xml with survival stat button after open_contracts
- Add list_price=62.0 to survival product template
- sponsorship_compassion: add name="last12months" to group, rename sr_sponsorship
  string to "Number of child sponsorships", restore "Sponsorship impact" stat button,
  update de/fr_CH/it translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…utton

Add back the "Details of donations" stat button and method that was dropped
during the v18 migration. Also fix FA4 icon (fa-file-text-o → fa-file-alt)
on both the donations details and sponsorship impact buttons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@danpa32 danpa32 requested review from NoeBerdoz and ecino July 1, 2026 09:13

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces survival sponsorship metrics and reporting features, including new fields, views, and actions to track CSP sponsorships, donations, and supported moms and babies. The review feedback focuses on aligning the implementation with Odoo best practices and security standards. Key recommendations include replacing raw SQL queries with ORM-compliant methods to respect multi-company record rules, avoiding unhandled exceptions in compute methods to prevent UI crashes, defining monetary fields with the correct type and widget, and adding defensive checks to prevent potential runtime errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +64 to +103
def _compute_sponsorship_metrics(self):
"""Orchestrator method to calculate and apply all report metrics."""
if not self:
return

for partner in self:
partner.sr_survival_sponsorship_count = 0
partner.sr_total_donation_for_csp = 0.0
partner.sr_countries_current = ""
partner.sr_countries_previous = ""
partner.sr_nb_moms_supported_for_a_year = 0

annual_cost_baseline = self._get_annual_cost_baseline()
partner_stats = self._fetch_sponsorship_stats()
donation_stats = self._fetch_donation_stats()

for partner in self:
stats = partner_stats.get(partner.id, {})
if stats:
partner.sr_survival_sponsorship_count = stats["count"]
if stats["current_countries"]:
partner.sr_countries_current = ", ".join(
sorted(stats["current_countries"])
)
if stats["previous_countries"]:
partner.sr_countries_previous = ", ".join(
sorted(stats["previous_countries"])
)

total_donation = donation_stats.get(partner.id, 0.0)
if partner.is_church:
total_donation += sum(
donation_stats.get(mid, 0.0) for mid in partner.member_ids.ids
)
partner.sr_total_donation_for_csp = total_donation

if annual_cost_baseline > 0:
partner.sr_nb_moms_supported_for_a_year = round(
total_donation / annual_cost_baseline, 2
)

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.

high

Instead of executing raw SQL queries to fetch sponsorship statistics, we can reuse the existing _get_survival_sponsorship_data() method defined on res.partner. Reusing the ORM-based method is much safer, respects Odoo's multi-company record rules, and is easier to maintain.

    def _compute_sponsorship_metrics(self):
        """Orchestrator method to calculate and apply all report metrics."""
        if not self:
            return

        for partner in self:
            partner.sr_survival_sponsorship_count = 0
            partner.sr_total_donation_for_csp = 0.0
            partner.sr_countries_current = ""
            partner.sr_countries_previous = ""
            partner.sr_nb_moms_supported_for_a_year = 0

        annual_cost_baseline = self._get_annual_cost_baseline()
        data, member_map = self._get_survival_sponsorship_data()
        donation_stats = self._fetch_donation_stats()

        for partner in self:
            partner_data = data.get(partner.id, {})
            count = partner_data.get("active_count", 0)
            curr_countries = set(partner_data.get("curr", []))
            prev_countries = set(partner_data.get("prev", []))

            if partner.is_church:
                for mid in member_map.get(partner.id, []):
                    member_data = data.get(mid, {})
                    count += member_data.get("active_count", 0)
                    curr_countries.update(member_data.get("curr", []))
                    prev_countries.update(member_data.get("prev", []))

            prev_countries -= curr_countries

            partner.sr_survival_sponsorship_count = count
            if curr_countries:
                partner.sr_countries_current = ", ".join(sorted(curr_countries))
            if prev_countries:
                partner.sr_countries_previous = ", ".join(sorted(prev_countries))

            total_donation = donation_stats.get(partner.id, 0.0)
            if partner.is_church:
                total_donation += sum(
                    donation_stats.get(mid, 0.0) for mid in member_map.get(partner.id, [])
                )
            partner.sr_total_donation_for_csp = total_donation

            if annual_cost_baseline > 0:
                partner.sr_nb_moms_supported_for_a_year = round(
                    total_donation / annual_cost_baseline, 2
                )

Comment on lines +105 to +117
def _get_annual_cost_baseline(self):
"""Fetch base annual cost (CSP only) from the survival product template."""
survival_tmpl = self.env.ref(
"survival_sponsorship_compassion.survival_product_template",
raise_if_not_found=False,
)
if not survival_tmpl:
raise ValueError(
"Missing required external ID: "
"'survival_sponsorship_compassion.survival_product_template'. "
"Ensure the survival product template is installed."
)
return survival_tmpl.list_price * 12

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.

high

Compute methods in Odoo should never raise unhandled exceptions like ValueError during normal execution, as this will crash the entire user interface when loading partner views if the record is missing. It is safer to return 0.0 or log a warning and fail gracefully.

    def _get_annual_cost_baseline(self):
        """Fetch base annual cost (CSP only) from the survival product template."""
        survival_tmpl = self.env.ref(
            "survival_sponsorship_compassion.survival_product_template",
            raise_if_not_found=False,
        )
        return (survival_tmpl.list_price * 12) if survival_tmpl else 0.0

Comment on lines +163 to +186
def _fetch_donation_stats(self):
"""Execute raw SQL for total CSP donation amounts per partner."""
all_ids = set(self.ids)
for church in self.filtered("is_church"):
all_ids.update(church.member_ids.ids)

self.env.cr.execute(
"""
SELECT am.partner_id,
COALESCE(SUM(aml.price_subtotal), 0) AS total_amount
FROM account_move am
JOIN account_move_line aml ON aml.move_id = am.id
JOIN recurring_contract rc ON aml.contract_id = rc.id
WHERE am.partner_id IN %s
AND am.move_type = 'out_invoice'
AND am.payment_state = 'paid'
AND rc.type = 'CSP'
GROUP BY am.partner_id
""",
(tuple(all_ids),),
)
return {
row["partner_id"]: row["total_amount"] for row in self.env.cr.dictfetchall()
}

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.

security-high high

Executing raw SQL to fetch donation statistics bypasses Odoo's multi-company record rules and security access controls. We should refactor this to use Odoo's ORM read_group on account.move.line to ensure security and multi-company compliance.

    def _fetch_donation_stats(self):
        """Fetch total CSP donation amounts per partner using ORM."""
        all_ids = set(self.ids)
        for church in self.filtered("is_church"):
            all_ids.update(church.member_ids.ids)

        domain = [
            ("partner_id", "in", list(all_ids)),
            ("move_id.move_type", "=", "out_invoice"),
            ("move_id.payment_state", "=", "paid"),
            ("contract_id.type", "=", "CSP"),
        ]
        group_data = self.env["account.move.line"].read_group(
            domain=domain,
            fields=["partner_id", "price_subtotal:sum"],
            groupby=["partner_id"],
        )
        return {
            row["partner_id"][0]: row["price_subtotal"]
            for row in group_data
            if row.get("partner_id")
        }

Comment on lines +41 to +45
sr_total_donation_for_csp = fields.Float(
"Total donation given",
compute="_compute_sponsorship_metrics",
help="Total donation given for CSP.",
)

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.

medium

The field sr_total_donation_for_csp represents a monetary amount but is defined as a Float. In Odoo, fields representing currency values should be defined as Monetary to ensure proper formatting and currency symbol display in the user interface.

Suggested change
sr_total_donation_for_csp = fields.Float(
"Total donation given",
compute="_compute_sponsorship_metrics",
help="Total donation given for CSP.",
)
sr_total_donation_for_csp = fields.Monetary(
"Total donation given",
compute="_compute_sponsorship_metrics",
help="Total donation given for CSP.",
)

Comment on lines +119 to +161
def _fetch_sponsorship_stats(self):
"""Execute raw SQL for contract counts and country sets."""
churches = self.filtered("is_church")
self.env.cr.execute(
"""
SELECT rp.id AS partner_id,
rc.id AS contract_id,
rc.state,
rc.csp_country
FROM res_partner rp
LEFT JOIN recurring_contract rc
ON rc.partner_id = rp.id AND rc.type = 'CSP'
WHERE rp.id IN %s
UNION ALL
SELECT p.church_id AS partner_id,
rc.id AS contract_id,
rc.state,
rc.csp_country
FROM res_partner p
JOIN recurring_contract rc
ON rc.partner_id = p.id AND rc.type = 'CSP'
WHERE p.church_id IN %s
""",
(tuple(self.ids), tuple(churches.ids) if churches else (0,)),
)
stats = {}
for row in self.env.cr.dictfetchall():
pid = row["partner_id"]
data = stats.setdefault(
pid,
{"count": 0, "current_countries": set(), "previous_countries": set()},
)
if row["contract_id"]:
if row["state"] == "active":
data["count"] += 1
if row["csp_country"]:
data["current_countries"].add(row["csp_country"])
elif row["csp_country"]:
data["previous_countries"].add(row["csp_country"])

for data in stats.values():
data["previous_countries"] -= data["current_countries"]
return stats

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.

medium

This method can be completely removed since the sponsorship statistics are now fetched using the ORM-compliant _get_survival_sponsorship_data() method.

Suggested change
def _fetch_sponsorship_stats(self):
"""Execute raw SQL for contract counts and country sets."""
churches = self.filtered("is_church")
self.env.cr.execute(
"""
SELECT rp.id AS partner_id,
rc.id AS contract_id,
rc.state,
rc.csp_country
FROM res_partner rp
LEFT JOIN recurring_contract rc
ON rc.partner_id = rp.id AND rc.type = 'CSP'
WHERE rp.id IN %s
UNION ALL
SELECT p.church_id AS partner_id,
rc.id AS contract_id,
rc.state,
rc.csp_country
FROM res_partner p
JOIN recurring_contract rc
ON rc.partner_id = p.id AND rc.type = 'CSP'
WHERE p.church_id IN %s
""",
(tuple(self.ids), tuple(churches.ids) if churches else (0,)),
)
stats = {}
for row in self.env.cr.dictfetchall():
pid = row["partner_id"]
data = stats.setdefault(
pid,
{"count": 0, "current_countries": set(), "previous_countries": set()},
)
if row["contract_id"]:
if row["state"] == "active":
data["count"] += 1
if row["csp_country"]:
data["current_countries"].add(row["csp_country"])
elif row["csp_country"]:
data["previous_countries"].add(row["csp_country"])
for data in stats.values():
data["previous_countries"] -= data["current_countries"]
return stats
# Deleted in favor of reusing _get_survival_sponsorship_data()

Comment on lines +32 to +41
for row in group_data:
pid = row["partner_id"][0]
if pid not in results:
continue
if row["state"] == "active":
results[pid]["active_count"] += row["__count"]
if row["csp_country"]:
results[pid]["curr"].add(row["csp_country"])
elif row["csp_country"]:
results[pid]["prev"].add(row["csp_country"])

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.

medium

If row["partner_id"] is False (e.g., due to database inconsistency or draft contracts), accessing row["partner_id"][0] will raise a TypeError. Adding a defensive check prevents potential runtime crashes.

Suggested change
for row in group_data:
pid = row["partner_id"][0]
if pid not in results:
continue
if row["state"] == "active":
results[pid]["active_count"] += row["__count"]
if row["csp_country"]:
results[pid]["curr"].add(row["csp_country"])
elif row["csp_country"]:
results[pid]["prev"].add(row["csp_country"])
for row in group_data:
if not row.get("partner_id"):
continue
pid = row["partner_id"][0]
if pid not in results:
continue
if row["state"] == "active":
results[pid]["active_count"] += row["__count"]
if row["csp_country"]:
results[pid]["curr"].add(row["csp_country"])
elif row["csp_country"]:
results[pid]["prev"].add(row["csp_country"])


<xpath expr="//group[@name='last12months']" position="after">
<group string="Supported moms and babies">
<field name="sr_total_donation_for_csp" />

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.

medium

Since sr_total_donation_for_csp is a monetary field, it should be displayed with the monetary widget and the correct currency field option to ensure proper formatting.

Suggested change
<field name="sr_total_donation_for_csp" />
<field name="sr_total_donation_for_csp" widget="monetary" options="{'currency_field': 'currency_id'}" />

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The changes are narrowly scoped to Odoo view wiring, report helper fields, partner actions, translations, and survival sponsorship metadata, with no outstanding code issues identified.

The modified areas are cohesive and align with the migration goal; no blocking behavior or unsafe contract changes were found in the reviewed diff.

T-Rex T-Rex Logs

What T-Rex did

  • Compared the survival metrics before and after the change; the after-state shows the file exists, all five fields are present, and the computed results match the expected values (count 2, total donation 2232.0, moms supported 3.0, current countries Kenya, Uganda, previous countries Rwanda).
  • Reviewed the church survival UI changes; the after-state head XML includes Sponsorship impact, Details of donations, open_survival_sponsorships, Survival sponsorships, and Supported moms and babies, but a runtime blocker prevented running the UI tests due to missing Odoo and Playwright components.
  • Verified the survival and donation action domains now select only active fixture records: survival action domain uses fixture contract IDs [10, 11], and donation action domain uses fixture move line IDs [20, 21].

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "[MIG] sponsorship_compassion: restore mi..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown

T-Rex pricing update — T-Rex was free through June 2026. Effective July 1, 2026, T-Rex adds 2 credits on top of the standard 1-credit review (3 total). T-Rex settings

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.

1 participant