diff --git a/sponsorship_compassion/i18n/de.po b/sponsorship_compassion/i18n/de.po
index 44ca8612f..ffde6086c 100644
--- a/sponsorship_compassion/i18n/de.po
+++ b/sponsorship_compassion/i18n/de.po
@@ -1001,7 +1001,7 @@ 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"
+msgid "Number of child sponsorships"
msgstr "Anzahl der Patenschaften"
#. module: sponsorship_compassion
diff --git a/sponsorship_compassion/i18n/fr_CH.po b/sponsorship_compassion/i18n/fr_CH.po
index f778477e4..0cb78996d 100644
--- a/sponsorship_compassion/i18n/fr_CH.po
+++ b/sponsorship_compassion/i18n/fr_CH.po
@@ -1004,7 +1004,7 @@ 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"
+msgid "Number of child sponsorships"
msgstr "Nombre de parrainages"
#. module: sponsorship_compassion
diff --git a/sponsorship_compassion/i18n/it.po b/sponsorship_compassion/i18n/it.po
index b55d9eddd..e26e621c4 100644
--- a/sponsorship_compassion/i18n/it.po
+++ b/sponsorship_compassion/i18n/it.po
@@ -1003,7 +1003,7 @@ 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"
+msgid "Number of child sponsorships"
msgstr "Numero di sponsorizzazioni"
#. module: sponsorship_compassion
diff --git a/sponsorship_compassion/models/contracts_report.py b/sponsorship_compassion/models/contracts_report.py
index d54386399..15cd3a463 100644
--- a/sponsorship_compassion/models/contracts_report.py
+++ b/sponsorship_compassion/models/contracts_report.py
@@ -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",
help="Count only the sponsorships who "
"are fully managed or those who are "
@@ -182,6 +182,35 @@ def get_nb_gift(_partner):
sr_total_gift += get_nb_gift(member)
partner.sr_total_gift = sr_total_gift
+ def open_donation_details(self):
+ self.ensure_one()
+ return {
+ "type": "ir.actions.act_window",
+ "name": "Donations details",
+ "res_model": "account.move.line",
+ "views": [
+ [
+ self.env.ref(
+ "sponsorship_compassion.view_invoice_line_partner_tree"
+ ).id,
+ "list",
+ ]
+ ],
+ "context": self.with_context(
+ search_default_group_product=1,
+ ).env.context,
+ "domain": [
+ "|",
+ ("partner_id", "=", self.id),
+ ("partner_id.church_id", "=", self.id),
+ ("move_id.invoice_category", "in", ["gift", "sponsorship", "fund"]),
+ ("move_id.move_type", "=", "out_invoice"),
+ ("payment_state", "=", "paid"),
+ ("last_payment", "<", self.end_period),
+ ("last_payment", ">=", self.start_period),
+ ],
+ }
+
def open_sponsorship_report(self):
action = {
"type": "ir.actions.act_window",
diff --git a/sponsorship_compassion/views/contracts_report_view.xml b/sponsorship_compassion/views/contracts_report_view.xml
index 0dbb36bd8..76ee77a28 100644
--- a/sponsorship_compassion/views/contracts_report_view.xml
+++ b/sponsorship_compassion/views/contracts_report_view.xml
@@ -6,6 +6,16 @@
service
True
+ 62.0
diff --git a/survival_sponsorship_compassion/models/__init__.py b/survival_sponsorship_compassion/models/__init__.py
index a64dd3248..b4fc3c587 100644
--- a/survival_sponsorship_compassion/models/__init__.py
+++ b/survival_sponsorship_compassion/models/__init__.py
@@ -5,3 +5,4 @@
from . import res_config_settings
from . import recurring_contract_line
from . import res_partner
+from . import contracts_report
diff --git a/survival_sponsorship_compassion/models/contracts_report.py b/survival_sponsorship_compassion/models/contracts_report.py
new file mode 100644
index 000000000..06e384b84
--- /dev/null
+++ b/survival_sponsorship_compassion/models/contracts_report.py
@@ -0,0 +1,186 @@
+##############################################################################
+#
+# ______ Releasing children from poverty _
+# / ____/___ ____ ___ ____ ____ ___________(_)___ ____
+# / / / __ \/ __ `__ \/ __ \/ __ `/ ___/ ___/ / __ \/ __ \
+# / /___/ /_/ / / / / / / /_/ / /_/ (__ |__ ) / /_/ / / / /
+# \____/\____/_/ /_/ /_/ .___/\__,_/____/____/_/\____/_/ /_/
+# /_/
+# in Jesus' name
+#
+# Copyright (C) 2024 Compassion CH (http://www.compassion.ch)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from odoo import fields, models
+
+
+# For more readability we have split "res.partner" by functionality
+# pylint: disable=R7980
+class PartnerSponsorshipReport(models.Model):
+ _inherit = "res.partner"
+
+ 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):
+ """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
+ )
+
+ 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
+
+ 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
+
+ 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()
+ }
diff --git a/survival_sponsorship_compassion/models/res_partner.py b/survival_sponsorship_compassion/models/res_partner.py
index 164fcc501..90b55eed6 100644
--- a/survival_sponsorship_compassion/models/res_partner.py
+++ b/survival_sponsorship_compassion/models/res_partner.py
@@ -1,9 +1,80 @@
-from odoo import models
+from odoo import _, fields, models
+# For more readability we have split "res.partner" by functionality
+# pylint: disable=R8180
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):
+ churches = self.filtered("is_church")
+ all_ids = set(self.ids)
+ church_member_map = {c.id: c.member_ids.ids for c in churches}
+ for members in church_member_map.values():
+ all_ids.update(members)
+
+ 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,
+ )
+
+ 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 open_survival_sponsorships(self):
+ self.ensure_one()
+ return {
+ "name": _("Survival Sponsorships"),
+ "type": "ir.actions.act_window",
+ "res_model": "recurring.contract",
+ "view_mode": "list,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",
+ }
+
def _compute_related_contracts(self):
res = super()._compute_related_contracts()
contract_obj = self.env["recurring.contract"]
diff --git a/survival_sponsorship_compassion/views/contracts_report_view.xml b/survival_sponsorship_compassion/views/contracts_report_view.xml
new file mode 100644
index 000000000..48ee19593
--- /dev/null
+++ b/survival_sponsorship_compassion/views/contracts_report_view.xml
@@ -0,0 +1,30 @@
+
+
+
+
diff --git a/survival_sponsorship_compassion/views/res_partner_view.xml b/survival_sponsorship_compassion/views/res_partner_view.xml
new file mode 100644
index 000000000..f675f89fe
--- /dev/null
+++ b/survival_sponsorship_compassion/views/res_partner_view.xml
@@ -0,0 +1,33 @@
+
+
+
+ res.partner.survival.sponsorships.form
+ res.partner
+
+
+
+
+
+
+
+
+
+