Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
f797051
[T2534] FEAT add a method to get payment info into ContractGroup
SamBachmann Nov 25, 2025
645112c
[T2534] FEAT add a modal to update/add payment method
SamBachmann Nov 26, 2025
92cb3fe
[T2534] FEAT update controller to handle the groups of sponsorships
SamBachmann Nov 26, 2025
20dc677
[T2534] FEAT group sponsorships by payment methods
SamBachmann Nov 26, 2025
cd6aeaf
[T2534] REFACTOR group sponsorships by payment methods
SamBachmann Nov 27, 2025
c4c6f1c
[T2534] REFACTOR update manifest and separate templates
SamBachmann Nov 27, 2025
5d8e76b
[T2534] REFACTOR update manifest and separate templates
SamBachmann Nov 28, 2025
b81a117
[T2534] REFACTOR update contract_group.py logic
SamBachmann Nov 28, 2025
6f76b40
[T2534] REFACTOR add bvr number if it exist & code refactoring
SamBachmann Nov 28, 2025
6db5205
[T2534] FEAT add "add payment method" Button
SamBachmann Nov 28, 2025
e7609aa
[T2534] FEAT update the update/change/add payment modal to be working
SamBachmann Dec 1, 2025
591548b
[T2534] FEAT methods for change and update payments methods
SamBachmann Dec 2, 2025
2385e3e
[T2534] Backup JS
SamBachmann Dec 2, 2025
94fc396
[T2534] FEAT add style to Payment cards
SamBachmann Dec 3, 2025
4238b13
[T2534] FEAT Make possible for a user to change his payment method fo…
SamBachmann Dec 3, 2025
9a406ac
[T2534] FIX add CSS file for the payment selection modal
SamBachmann Dec 3, 2025
0f3e8bf
[T2534] FEAT Can switch between methods in update with collapsible se…
SamBachmann Dec 4, 2025
82a2f31
[T2534] FIX Use less of JS
SamBachmann Dec 5, 2025
a0a4e62
[T2534] REFACTOR clean up the js
SamBachmann Dec 5, 2025
f714acd
[T2534] FIX get all the payment methods and pass it to the my_donatio…
SamBachmann Dec 5, 2025
54c880e
[T2534] FIX refactor templates
SamBachmann Dec 5, 2025
ffe6f0b
[T2819] FEAT Better displaying for modal and collapsible sections
SamBachmann Dec 8, 2025
12726b5
[T2819] FIX Smother experience reloading the page after save a change…
SamBachmann Dec 8, 2025
a3b8cfe
[T2534] FIX selection of cards in modals
SamBachmann Dec 9, 2025
8f1b593
[T2534] FEAT add a new payment method from donation page
SamBachmann Dec 10, 2025
ffbb782
[T2534] FIX make possible the save of payment methods from checkout
SamBachmann Dec 10, 2025
40e152a
[T2534] FIX Improve create_from_transaction logic
SamBachmann Dec 11, 2025
b6a928a
[T2534] FIX Adapt icon size
SamBachmann Dec 11, 2025
fddfa9c
[T2534] FIX Retrieve all valid groups linked to a partner
SamBachmann Dec 11, 2025
b238b83
[T2534] FEAT display toast after returning from add a payment method
SamBachmann Dec 12, 2025
ca7236d
[T2534] REFACTOR refactor the code (docs unused methods)
SamBachmann Dec 12, 2025
9e04182
[T2534] REFACTOR refactor the code (docs unused methods)
SamBachmann Dec 12, 2025
19587ec
[T2534] REFACTOR UI addjusting for payment modal and payment cards
SamBachmann Dec 12, 2025
737ae51
[T2534] FIX Improve error toast displaying when returning from a paym…
SamBachmann Dec 12, 2025
e944c74
[T2534] FIX remove useless operations
SamBachmann Dec 12, 2025
ff970ac
[T2534] FEAT make the update payment method logic working
SamBachmann Dec 16, 2025
e45b176
[T2534] REFACTOR change doc in xml files
SamBachmann Dec 16, 2025
ee8a972
[T2534] FIX update payment method for a group infinite looping
SamBachmann Dec 16, 2025
e0bf524
[T2534] REFACTOR remove functionalities related to payment method acq…
SamBachmann Dec 16, 2025
c0495e9
[T2534] REFACTOR precommit formating
SamBachmann Dec 16, 2025
476a9a1
[T2534] REFACTOR remove logs
SamBachmann Dec 16, 2025
4a16e18
[T2534] FIX support LSV contract properly
SamBachmann Dec 17, 2025
a2bdf75
[T2534] FIX serialize event details for group change
SamBachmann Dec 17, 2025
5e5abb4
[T2534] FIX remove unused variable
SamBachmann Dec 17, 2025
492284a
[T2534] FIX simplify comments
SamBachmann Dec 17, 2025
f33dc42
[T2534] FIX prevent memory leak in widget end of life
SamBachmann Dec 17, 2025
bfe467f
[T2534] FIX remove ref field (as it is computed internally now)
SamBachmann Dec 17, 2025
d213c4f
[T2534] FIX add security check when changing a contract from a group …
SamBachmann Dec 17, 2025
3439281
[T2534] FIX prevent memory leak in widget
SamBachmann Dec 17, 2025
7cbac75
[T2534] REFACTOR retrieve bvr_reference from a backend function
SamBachmann Dec 17, 2025
20bbd54
[T2534] REFACTOR Change modal btn label from Save change to Save
SamBachmann Dec 17, 2025
3cb56ba
[T2534] FIX add variable in css
SamBachmann Dec 17, 2025
fc9f1a4
[T2534] FIX remove stylesheet link from xml my2_payment_method_card.xml
SamBachmann Dec 17, 2025
28fb20f
[T2534] FIX allow changing the ref of a group where it is lacking
SamBachmann Dec 17, 2025
85031f7
[T2534] FIX make the displayed string translatable
SamBachmann Dec 17, 2025
3cef023
[T2534] REFACTOR run precommit formating
SamBachmann Dec 17, 2025
757014b
[T2534] FIX remove duplicate line
SamBachmann Dec 17, 2025
a0848b8
[T2534] FIX remove duplicate line
SamBachmann Dec 17, 2025
0b30348
[T2534] FIX precommit
SamBachmann Dec 17, 2025
c5a2656
[T2534] REFACTOR move # of contract per group calculation to the backend
SamBachmann Dec 17, 2025
5df61c0
[T2534] FIX typo in Toast message
SamBachmann Dec 18, 2025
d4d3f42
[T2534] REFACTOR donation page to be able to not reload the page afte…
SamBachmann Dec 18, 2025
d5b8b83
[T2534] REFACTOR donation page
SamBachmann Dec 18, 2025
a714e73
[T2534] REFACTOR implement the optimistic UI reloading
SamBachmann Dec 19, 2025
d83648b
[T2534] REFACTOR implement the optimistic UI reloading
SamBachmann Dec 19, 2025
c83db3f
[T2846] REFACTOR save payment methods as a state and prevent JSON issues
SamBachmann Dec 22, 2025
9f6d8a1
[T2846]FIX precommit
SamBachmann Dec 22, 2025
8a10ee8
[T2846]FIX remove Json from update btn
SamBachmann Dec 22, 2025
fd0b8d5
[T2534] FIX precommit
SamBachmann Dec 23, 2025
88ceb66
[T2534] FIX translate error messages
SamBachmann Dec 29, 2025
f58dade
Merge branch '14.0-MyCompassion2.0' into T2534-Payment-Method-storing…
SamBachmann Dec 29, 2025
dea30b9
[T2853] FIX remove console.log in js
SamBachmann Dec 29, 2025
a8c4ba9
[T2853] FEAT add function to integrate postfinance checkout
SamBachmann Dec 30, 2025
b5f8a5c
[T2853] FEAT redirect to online payment btn
SamBachmann Dec 31, 2025
fede9f7
[T2853] FEAT checkout payment page implementation
SamBachmann Dec 31, 2025
d07630a
[T2853] FEAT postfinance checkout iframe integration
SamBachmann Jan 5, 2026
dc82825
[T2853] REFACTOR add a postfinance checkout method flow
SamBachmann Jan 6, 2026
a0aec53
[T2853] REFACTOR pre-commit
SamBachmann Jan 6, 2026
6b26649
[T2853] REFACTOR remove payment_mode_name check
SamBachmann Jan 7, 2026
340e0bc
[T2853] REFACTOR payment token integration
SamBachmann Jan 7, 2026
2812b05
[T2853] REFACTOR remove dirty icon find
SamBachmann Jan 7, 2026
9b003f2
[T2853] FIX res_partner: exclude partners with no payment mode in rec…
SamBachmann Jan 7, 2026
c45eef6
[T2853] FEAT Implement debug charge functionality for PostFinance int…
SamBachmann Jan 7, 2026
3713be9
[T2853] REFACTOR update modal to not let the user modify fields he sh…
SamBachmann Jan 7, 2026
42476a9
[T2853] REFACTOR clean up the code + precommit
SamBachmann Jan 8, 2026
55af7af
[T2853] REFACTOR standardize the whole payment processing logic
SamBachmann Jan 9, 2026
64ad556
[T2853] REFACTOR give a generic name to methods in compassion-website…
SamBachmann Jan 12, 2026
176ca5d
[T2853] REFACTOR clean up comment and upgrade add a new method backen…
SamBachmann Jan 12, 2026
b3bbd6a
[T2853] REFACTOR my2_donations.js: remove ajax calls
SamBachmann Jan 13, 2026
5393a6a
[T2853] REFACTOR remove test on string
SamBachmann Jan 13, 2026
ffe0290
[T2853] REFACTOR Clean up the code
SamBachmann Jan 13, 2026
03328f8
[T2853] REFACTOR precommit
SamBachmann Jan 13, 2026
7d78569
[T2853] REFACTOR add link to the doc into the code
SamBachmann Jan 13, 2026
a4af112
[T2853] REFACTOR add link to the doc into the code
SamBachmann Jan 13, 2026
ed3aad3
[T2853] REFACTOR add link to the doc into the code
SamBachmann Jan 13, 2026
78108aa
[T2853] FIX fix gender in recurring contract group to be the same as …
SamBachmann Jan 13, 2026
35808a9
[T2853] FIX avoid too general exceptions
SamBachmann Jan 13, 2026
da9f954
[T2853] FIX my2_donations.js remove obsolete code
SamBachmann Jan 13, 2026
b31c0b9
[T2853] FIX my2_donations.js get rid of alert
SamBachmann Jan 13, 2026
4fa95ed
[T2853] REFACTOR: use a map for payment methods
SamBachmann Jan 13, 2026
b3ddf38
[T2853] REFACTOR: use a map for payment methods
SamBachmann Jan 13, 2026
71578ba
[T2853] REFACTOR: Avoid useless calls to the db in icon retrival
SamBachmann Jan 14, 2026
74b2d39
[T2853] REFACTOR: precommit
SamBachmann Jan 14, 2026
9531898
[T2853] FIX: clean the return URL once the params are used
SamBachmann Jan 14, 2026
869d932
[T2853] FIX: improve comments readability
SamBachmann Jan 14, 2026
5948f72
[T2853] FIX: improve comments and code readability
SamBachmann Jan 14, 2026
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
3 changes: 3 additions & 0 deletions my_compassion/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
"templates/components/my2_giving_limits_modal.xml",
"templates/components/my2_checkout.xml",
"templates/components/my2_weather_time_container.xml",
"templates/components/my2_sponsorships_section.xml",
"templates/components/my2_payment_method_modal.xml",
"templates/components/my2_payment_method_card.xml",
# Other data the depends on the templates
"data/my2_new_sponsorship_wizard_steps.xml",
],
Expand Down
299 changes: 291 additions & 8 deletions my_compassion/controllers/my2_donations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,26 @@
#
##############################################################################

import json
import math
from collections import defaultdict
from datetime import datetime, timedelta

from werkzeug.exceptions import BadRequest, NotFound

from odoo import fields, http
import odoo
from odoo import _, fields, http
from odoo.http import request

from odoo.addons.portal.controllers.portal import CustomerPortal


# TODO: Refactor payment-related methods into a separate controller
# PLEASE NOTE:
# Before deep diving into this code note that there's a documentation
# page explaining the overall architecture and flow of this code.
# https://compassion.odoo.com/odoo/knowledge/202 and
# https://compassion.odoo.com/odoo/knowledge/205
class MyCompassionDonationsController(CustomerPortal):
@http.route(
'/my2/gifts/<model("product.template"):product>',
Expand Down Expand Up @@ -387,6 +395,10 @@ def _get_paid_invoices_amount(self, partner):
)
return number_of_paid_invoices

# -------------------------------------------------------------------------
# Methods and routes for MyCompassion2.0 Donations Page
# -------------------------------------------------------------------------

@http.route(
"/my2/donations",
type="http",
Expand All @@ -397,7 +409,14 @@ def my_donations(self, invoice_page=1, invoice_per_page=12, **kw):
partner = request.env.user.partner_id

# Active sponsorships
active_sponsorships = partner.get_portal_sponsorships("active")
active_sponsorships = partner.get_portal_sponsorships(["active", "mandate"])

# Group sponsorships by their backend Contract Group
sponsorship_groups = active_sponsorships.mapped("group_id")

# Put all payment methods into a dict of group_id -> method info
all_groups = partner.get_payment_modes()
payment_info_map = all_groups.get_payment_method_info()

# Due invoices
date_filter_up_bound = datetime.today() + timedelta(days=30)
Expand All @@ -417,16 +436,16 @@ def my_donations(self, invoice_page=1, invoice_per_page=12, **kw):
)
)

# Computing the total price of the active sponsorships grouped
# per sponsorship frequency and payment method.
# group_id groups the invoices that have the same payment method and frequency.
# Total cost calculation
tot_cost_per_frequency = defaultdict(lambda: defaultdict(float))

for sponsorship in active_sponsorships:
currency = sponsorship.pricelist_id.currency_id.name
tot_cost_per_frequency[sponsorship.group_id.month_interval][
currency
] += sponsorship.total_amount
# Ensure group exists
if sponsorship.group_id:
tot_cost_per_frequency[sponsorship.group_id.month_interval][
currency
] += sponsorship.total_amount

paid_invoices_data = self._get_paginated_paid_invoices(
partner, invoice_page, invoice_per_page
Expand All @@ -436,6 +455,9 @@ def my_donations(self, invoice_page=1, invoice_per_page=12, **kw):
values.update(
{
"active_sponsorships": active_sponsorships,
"sponsorship_groups": sponsorship_groups,
"payment_info_map": payment_info_map,
"payment_methods_json": json.dumps(payment_info_map),
"tot_cost_per_frequency": tot_cost_per_frequency,
"due_invoices": due_invoices,
"paid_invoices_subset": paid_invoices_data["paid_invoices_subset"],
Expand Down Expand Up @@ -466,6 +488,219 @@ def my_donations_history(self, invoice_page=1, invoice_per_page=12, **kw):

return {"html": html}

@http.route(
"/my2/donation/change_method_contract", type="json", auth="user", website=True
)
def change_payment_method_contract(self, contract_id, group_id, **kwargs):
"""
Changes the payment method for a specific contract.

:param contract_id: ID of the recurring.contract to update.
:param group_id: ID of an existing group to merge into
"""
partner = request.env.user.partner_id
if not contract_id or not group_id:
raise BadRequest()
# Verify that the contract belongs to the user
contract = (
request.env["recurring.contract"]
.sudo()
.search([("id", "=", int(contract_id)), ("partner_id", "=", partner.id)])
)
if not contract:
raise NotFound()

success = contract.change_contract_group(int(group_id))
if success:
# Render the updated list
values = self._prepare_sponsorship_values(partner)
html = request.env["ir.qweb"]._render(
"my_compassion.my2_sponsorships_section", values
)
return {
"success": True,
"html": html,
"payment_info_map": values["payment_info_map"],
}

return {"success": False, "error": _("Operation failed")}

@http.route(
"/my2/donation/change_method_group", type="json", auth="user", website=True
)
def change_payment_method_group(
self, group_id, new_group_id=None, new_bvr_ref=None, **kwargs
):
"""
Endpoint to update payment method for a sponsorship group.
Accepts new_group_id (to merge) or new_bvr_ref (to update ref).
"""
partner = request.env.user.partner_id

if not group_id:
raise BadRequest(_("Group ID is required."))

# Security Check: Search ensures the group belongs to the logged-in user
group = (
request.env["recurring.contract.group"]
.sudo()
.search(
[("id", "=", int(group_id)), ("partner_id", "=", partner.id)], limit=1
)
)

if not group:
raise NotFound(_("Payment group not found or access denied."))

# Call the model method to perform the logic
success = group.change_payment_method(
new_group_id=new_group_id, new_bvr_ref=new_bvr_ref
)

if success:
values = self._prepare_sponsorship_values(partner)
html = request.env["ir.qweb"]._render(
"my_compassion.my2_sponsorships_section", values
)
return {
"success": True,
"html": html,
"payment_info_map": values["payment_info_map"],
}

return {"success": False, "error": _("Operation failed")}

@http.route(
"/my2/donation/add_payment_method_group", type="json", auth="user", website=True
)
def add_payment_method_group(
self,
recurring_unit="month",
method_type="bvr",
advance_billing_months=1,
**kwargs,
):
"""
Creates a new Contract Group with manual BVR/Permanent Order details.
"""
partner = request.env.user.partner_id

# 1. Find Payment Mode
payment_mode = self._find_manual_payment_mode(method_type)
if not payment_mode:
return {
"success": False,
"error": _('Configuration Error: Payment mode for "%s" not found.')
% method_type,
}

# 2. Create the Group
try:
new_group = self._create_contract_group(
partner, payment_mode, recurring_unit, advance_billing_months
)

# Specific BVR Logic
new_bvr_ref = new_group.compute_partner_bvr_ref(partner)
if new_bvr_ref:
new_group.bvr_reference = new_bvr_ref

# 3. Return HTML
values = self._prepare_sponsorship_values(partner)
html = request.env["ir.qweb"]._render(
"my_compassion.my2_sponsorships_section", values
)

return {
"success": True,
"html": html,
"group_id": new_group.id,
"payment_info_map": values["payment_info_map"],
}

except odoo.exceptions.ValidationError as e:
return {"success": False, "error": str(e)}
except Exception:
return {"success": False, "error": _("An unexpected error occurred.")}

@http.route(
"/my2/donation/fetch_payment_methods_iframe",
type="json",
auth="user",
website=True,
)
def fetch_payment_methods_iframe(
self, recurring_unit="month", recurring_value=1, **kwargs
):
"""
Initiates a 'validation' transaction to tokenize a card/method.
Returns data for rendering the PostFinance Iframe with available methods.
"""
acquirer = self._get_payment_acquirer()

if not acquirer.exists():
return {"success": False, "error": "No payment provider found"}

# Prepare Transaction
return_url = "/my2/donations?unit={}&val={}".format(
recurring_unit, recurring_value
)

# 3. Get Integration Data (Iframe)
# This calls the method overridden in country specific module
result_data = self._prepare_iframe_redirect(acquirer, return_url)

if (
result_data
and isinstance(result_data, dict)
and result_data.get("type") == "iframe"
):
return {
"success": True,
"iframe_url": result_data["url"],
"pf_methods": result_data.get("pf_methods", []),
}

# Error if Iframe data is missing
return {"success": False, "error": "Payment interface could not be loaded."}

# -------------------------------------------------------------------------
# PRIVATE HELPERS (Rendering Data Preparation)
# -------------------------------------------------------------------------

@staticmethod
def _prepare_sponsorship_values(partner):
"""
Helper to fetch all data required for the sponsorship list view.
Returns a dict of values for QWeb rendering.
"""
# 1. Fetch Active Sponsorships
active_sponsorships = partner.get_portal_sponsorships(["active", "mandate"])

# 2. Fetch Groups
sponsorship_groups = active_sponsorships.mapped("group_id")

# 3. Calculate Totals
tot_cost_per_frequency = defaultdict(lambda: defaultdict(float))
for sponsorship in active_sponsorships:
currency = sponsorship.pricelist_id.currency_id.name
if sponsorship.group_id:
tot_cost_per_frequency[sponsorship.group_id.month_interval][
currency
] += sponsorship.total_amount

# 4. Fetch Available Methods (for modals)
all_groups = partner.get_payment_modes()
payment_info_map = all_groups.get_payment_method_info()

return {
"active_sponsorships": active_sponsorships,
"sponsorship_groups": sponsorship_groups,
"tot_cost_per_frequency": tot_cost_per_frequency,
"payment_info_map": payment_info_map,
"payment_methods_json": json.dumps(payment_info_map),
}

def _get_paginated_paid_invoices(
self, partner, invoice_page=1, invoice_per_page=12
):
Expand All @@ -489,6 +724,54 @@ def _get_paginated_paid_invoices(
"total_pages": total_pages,
}

@staticmethod
def _create_contract_group(partner, payment_mode, unit, value, token=None):
"""
Centralized method to create a recurring contract group.
Used by both Manual (BVR, etc.) and Online (Credit Card, etc.) flows.
"""
vals = {
"partner_id": partner.id,
"payment_mode_id": payment_mode.id,
"recurring_unit": unit,
"recurring_value": int(value),
"active": True,
}
if token:
vals["payment_token_id"] = token.id

return request.env["recurring.contract.group"].sudo().create(vals)

@staticmethod
def _find_manual_payment_mode(method_key):
"""
Finds a payment mode based on the frontend key (bvr/permanent).
Handles case-insensitive search and archiving.
"""
# Map frontend keys to DB names
search_map = {
"permanent_order": "Permanent Order",
"bvr": "BVR",
}
term = search_map.get(method_key)
if not term:
return None

# Search with active_test=False to find modes even if archived/hidden
domain = [("name", "=", term)]
mode = (
request.env["account.payment.mode"]
.sudo()
.with_context(active_test=False)
.search(domain, limit=1)
)

return mode

def _prepare_iframe_redirect(self, acquirer, return_url):
"""Method to be overridden by country/provider specific modules"""
return False

@staticmethod
def _get_payment_acquirer():
return (
Expand Down
18 changes: 18 additions & 0 deletions my_compassion/data/payment_options.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="payment_mode_ebill" model="account.payment.mode">
<field name="name">eBill</field>
<field name="company_id" ref="base.main_company"/>
<field name="bank_account_link">variable</field>
<field name="payment_method_id" ref="account.account_payment_method_manual_in"/>
<field name="active" eval="True"/>
</record>

<record id="account_payment_method_ebill" model="account.payment.method">
<field name="name">eBill</field>
<field name="code">ebill</field>
<field name="payment_type">inbound</field>
</record>
</data>
</odoo>
Loading