Skip to content
Merged
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
4 changes: 2 additions & 2 deletions sponsorship_compassion/i18n/de.po
Original file line number Diff line number Diff line change
Expand Up @@ -1001,8 +1001,8 @@ msgstr "Anzahl der servierten Mahlzeiten"
#. module: sponsorship_compassion
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_partner__sr_sponsorship
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_users__sr_sponsorship
msgid "Number of sponsorship"
msgstr "Anzahl der Patenschaften"
msgid "Number of child sponsorships"
msgstr "Anzahl der Kinderpatenschaften"

#. module: sponsorship_compassion
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_partner__number_sponsorships
Expand Down
4 changes: 2 additions & 2 deletions sponsorship_compassion/i18n/fr_CH.po
Original file line number Diff line number Diff line change
Expand Up @@ -1004,8 +1004,8 @@ msgstr "Nombre de repas servis"
#. module: sponsorship_compassion
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_partner__sr_sponsorship
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_users__sr_sponsorship
msgid "Number of sponsorship"
msgstr "Nombre de parrainages"
msgid "Number of child sponsorships"
msgstr "Nombre de parrainages d'enfants"

#. module: sponsorship_compassion
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_partner__number_sponsorships
Expand Down
4 changes: 2 additions & 2 deletions sponsorship_compassion/i18n/it.po
Original file line number Diff line number Diff line change
Expand Up @@ -1003,8 +1003,8 @@ msgstr "Numero di pasti serviti"
#. module: sponsorship_compassion
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_partner__sr_sponsorship
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_users__sr_sponsorship
msgid "Number of sponsorship"
msgstr "Numero di sponsorizzazioni"
msgid "Number of child sponsorships"
msgstr "Numero di sponsorizzazioni per bambini"

#. module: sponsorship_compassion
#: model:ir.model.fields,field_description:sponsorship_compassion.field_res_partner__number_sponsorships
Expand Down
2 changes: 1 addition & 1 deletion sponsorship_compassion/models/contracts_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class PartnerSponsorshipReport(models.Model):

# sr -> Sponsorship Report
sr_sponsorship = fields.Integer(
"Number of sponsorship",
"Number of child sponsorships",
compute="_compute_sr_sponsorship",
Comment on lines 33 to 35

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Translations no longer match

The field label now uses the source text Number of child sponsorships, but the updated German/French/Italian PO entries still have msgid "Number of sponsorship". Odoo matches translations by the source msgid, so those edited msgstr values are orphaned and users in those languages will see the new English label instead of the translated text. Please regenerate or update the PO entries for the new source string.

Artifacts

Repro: focused translation lookup script

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Repro: translation lookup output showing English fallback

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

help="Count only the sponsorships who "
"are fully managed or those who are "
Expand Down
5 changes: 4 additions & 1 deletion sponsorship_compassion/views/contracts_report_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
<field name="sr_nb_bible" />
<field name="sr_nb_medic_check" />
</group>
<group string="During the last 12 months">
<group
name="last12months"
string="During the last 12 months"
>
<field
name="sr_total_donation"
widget="monetary"
Expand Down
2 changes: 2 additions & 0 deletions survival_sponsorship_compassion/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"views/product_template_view.xml",
"views/recurring_contract.xml",
"views/res_config_settings_view.xml",
"views/res_partner_view.xml",
"views/contracts_report_view.xml",
"data/product_warning_automation.xml",
"data/survival_product_template.xml",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<data noupdate="1">
<record id="survival_product_template" model="product.template">
<field name="name">Survival Sponsorship</field>
<field name="list_price">62.0</field>
Comment thread
ecino marked this conversation as resolved.
<field name="type">service</field>
<field name="survival_sponsorship_sale">True</field>
</record>
Expand Down
1 change: 1 addition & 0 deletions survival_sponsorship_compassion/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from . import recurring_contract_line
from . import wordpress_configuration
from . import res_partner
from . import contracts_report
177 changes: 177 additions & 0 deletions survival_sponsorship_compassion/models/contracts_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
from odoo import fields, models


# For more readability we have split "res.partner" by functionality
# pylint: disable=R7980
Comment thread
danpa32 marked this conversation as resolved.
class PartnerSponsorshipReport(models.Model):
_inherit = "res.partner"

# sr -> Sponsorship Report
sr_survival_sponsorship_count = fields.Integer(
"Number of survival sponsorships",
compute="_compute_sponsorship_metrics",
help="Number of survival sponsorships " "for a church AND its members.",
)

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

sr_nb_moms_supported_for_a_year = fields.Float(
"Number of moms and babies supported for 1 year (all-in-all)",
compute="_compute_sponsorship_metrics",
help="Number of moms and babies supported for a year.",
)

sr_countries_current = fields.Char(
"Countries currently impacted",
compute="_compute_sponsorship_metrics",
help="List of current countries impacted "
"by the church and its members by the CSP program.",
)

sr_countries_previous = fields.Char(
"Countries previously impacted",
compute="_compute_sponsorship_metrics",
help="List of previously impacted countries "
"by the church and its members by the CSP program.",
)

def _compute_sponsorship_metrics(self):
Comment thread
danpa32 marked this conversation as resolved.
"""Orchestrator method to calculate and apply all report metrics."""
# Early exit for empty recordsets
if not self:
return

# 1. Initialize Default Values
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
Comment thread
greptile-apps[bot] marked this conversation as resolved.

# 2. Fetch Data
annual_cost_baseline = self._get_annual_cost_baseline()
partner_stats = self._fetch_sponsorship_stats()
donation_stats = self._fetch_donation_stats()
Comment thread
greptile-apps[bot] marked this conversation as resolved.

# 3. Apply calculated data to records
for partner in self:
# Apply Contract/Country Stats
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"])
)

# Apply Donation Stats (aggregating members for churches)
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
)

def _get_annual_cost_baseline(self):
"""Fetch base annual cost (for CSP only) from 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."
)

monthly_cost = survival_tmpl.list_price
return monthly_cost * 12
Comment thread
greptile-apps[bot] marked this conversation as resolved.

# This function is using raw sql and union all
# It might seem weird to not use the ORM,
# but for optimization purposes, raw SQL is better
# and lighter for the database
def _fetch_sponsorship_stats(self):
"""Execute raw SQL for contract counts and country sets."""
churches = self.filtered("is_church")
query = """
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 \
"""
self.env.cr.execute(
query, (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"])
Comment on lines +148 to +149

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Draft countries counted

This branch puts every non-active CSP contract with a csp_country into previous_countries. Draft and waiting contracts can already have a country through their contract line, but they have not impacted that country yet. When support staff prepares a CSP contract that is never paid, the sponsorship report can still show that country under “Countries previously impacted”. Please restrict this to contracts that actually had support activity, such as cancelled/terminated contracts with an activation/start marker.

Artifacts

Repro: focused harness for draft CSP previous_countries

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Repro: harness execution output showing draft country in previous_countries

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex


# Cleanup: Ensure countries are only in one set
for data in stats.values():
data["previous_countries"] -= data["current_countries"]
return stats

def _fetch_donation_stats(self):
"""Execute raw SQL for total donation amounts per partner."""
all_ids = set(self.ids)
for church in self.filtered("is_church"):
all_ids.update(church.member_ids.ids)

query = """
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'
Comment on lines +169 to +171

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Refunds stay counted

The donation total only includes paid out_invoice moves, so refunded or reversed CSP invoices are never subtracted. If a supporter pays a CHF 744 CSP invoice and that invoice is later refunded, the original paid invoice still matches this query and the report still counts one year of support. The total should net out refunds/reversals or exclude reversed invoices so sr_total_donation_for_csp and the moms-supported metric do not stay inflated.

Artifacts

Repro: database harness creating paid CSP invoice and refund then running the donation stats SQL

  • Contains supporting evidence from the run (text/x-python; charset=utf-8).

Repro: verbose harness output showing refund fixture rows and unexpected positive total

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

GROUP BY am.partner_id \
"""
self.env.cr.execute(query, (tuple(all_ids),))
Comment thread
danpa32 marked this conversation as resolved.
return {
row["partner_id"]: row["total_amount"] for row in self.env.cr.dictfetchall()
}
86 changes: 85 additions & 1 deletion survival_sponsorship_compassion/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,70 @@
from odoo import models
from odoo import _, fields, models


class ResPartner(models.Model):
_inherit = "res.partner"

survival_sponsorship_count = fields.Integer(
string="Survival sponsorship(s)",
compute="_compute_active_csp_count",
)

def _get_survival_sponsorship_data(self):
"""
Returns a dictionary mapping partner_id to their survival sponsorship metrics.
This is the single source of truth for sponsorship calculations.
"""
churches = self.filtered("is_church")
all_ids = set(self.ids)
# Pre-fetch member IDs to avoid ORM overhead in loops
church_member_map = {c.id: c.member_ids.ids for c in churches}
for members in church_member_map.values():
all_ids.update(members)

# Using your proven read_group logic
group_data = self.env["recurring.contract"].read_group(
domain=[("partner_id", "in", list(all_ids)), ("type", "=", "CSP")],
fields=["partner_id", "state", "csp_country"],
groupby=["partner_id", "state", "csp_country"],
lazy=False,
)

# Structure: {
# partner_id: {
# 'active_count': X,
# 'current_countries': set(),
# 'previous_countries': set()
# }
# }
results = {
pid: {"active_count": 0, "curr": set(), "prev": set()} for pid in all_ids
}

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"])

return results, church_member_map

def _compute_active_csp_count(self):
data, member_map = self._get_survival_sponsorship_data()
for partner in self:
count = data.get(partner.id, {}).get("active_count", 0)
if partner.is_church:
count += sum(
data.get(mid, {}).get("active_count", 0)
for mid in member_map.get(partner.id, [])
)
partner.survival_sponsorship_count = count

def _compute_related_contracts(self):
super()._compute_related_contracts()
contract_obj = self.env["recurring.contract"]
Expand Down Expand Up @@ -35,3 +96,26 @@ def _compute_related_contracts(self):
partner.other_contract_ids = partner.other_contract_ids.filtered(
lambda c: c.type != "CSP"
)

def open_survival_sponsorships(self):
self.ensure_one()

return {
"name": _("Survival Sponsorships"),
"type": "ir.actions.act_window",
"res_model": "recurring.contract",
"view_mode": "tree,form",
"domain": [
("type", "=", "CSP"),
("state", "=", "active"),
"|",
("partner_id", "=", self.id),
("partner_id.church_id", "=", self.id),
],
"context": {
"create": False,
"default_type": "CSP",
"default_partner_id": self.id,
},
"target": "current",
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
32 changes: 32 additions & 0 deletions survival_sponsorship_compassion/views/contracts_report_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record
id="view_sponsorship_report_survival_form_inherit"
model="ir.ui.view"
>
<field
name="name"
>res.partner.sponsorship.report.survival.form.inherit</field>
<field name="model">res.partner</field>
<field
name="inherit_id"
ref="sponsorship_compassion.sponsorship_report_form"
/>
<field name="arch" type="xml">

<xpath expr="//field[@name='sr_nb_girl']" position="after">
<field name="sr_survival_sponsorship_count" />
</xpath>

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

</field>
</record>
</odoo>
Loading
Loading