Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 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
192b980
[T2534] FIX remove console.log in js
SamBachmann Dec 29, 2025
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
262 changes: 254 additions & 8 deletions my_compassion/controllers/my2_donations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
#
##############################################################################

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
Expand Down Expand Up @@ -397,7 +399,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 an array
all_groups = partner.get_payment_modes()
payment_methods = [group.get_payment_method_info() for group in all_groups]

# Due invoices
date_filter_up_bound = datetime.today() + timedelta(days=30)
Expand All @@ -417,16 +426,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 +445,9 @@ def my_donations(self, invoice_page=1, invoice_per_page=12, **kw):
values.update(
{
"active_sponsorships": active_sponsorships,
"sponsorship_groups": sponsorship_groups,
"payment_methods": payment_methods,
"payment_methods_json": json.dumps(payment_methods),
"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 +478,240 @@ def my_donations_history(self, invoice_page=1, invoice_per_page=12, **kw):

return {"html": html}

@http.route(
"/my2/donations/get_payment_methods_sponsor",
type="json",
auth="user",
website=True,
)
def get_payment_methods_sponsor(self, **kwargs):
"""
Returns a list of payment methods (saved tokens and acquirers) for the current
user.
"""
partner = request.env.user.partner_id
groups = partner.get_payment_modes()
payment_methods = []

for group in groups:
info = group.get_payment_method_info()
if not info:
continue
method = dict(info)
method["group_id"] = group.id

payment_methods.append(method)

return payment_methods

@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_methods": values["payment_methods"],
}

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_methods": values["payment_methods"],
}

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

# Configuration for supported manual payment methods
# Key: frontend 'value' from the select input
# Value: 'name' (or partial name) to search for in account.payment.mode
_payment_mode_map = {
"permanent_order": "Permanent Order",
"bvr": "BVR",
}

@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. Resolve Payment Mode Search Term
mode_search_term = self._payment_mode_map.get(method_type)

if not mode_search_term:
return {
"success": False,
"error": _("Invalid payment method type selected."),
}

# 2. Find the Payment Mode
# Exact match attempt
payment_mode = (
request.env["account.payment.mode"]
.sudo()
.search([("name", "=", mode_search_term)], limit=1)
)

# Fallback: Loose search (ilike) if exact match fails
if not payment_mode:
payment_mode = (
request.env["account.payment.mode"]
.sudo()
.search([("name", "ilike", mode_search_term)], limit=1)
)

if not payment_mode:
return {
"success": False,
"error": _('Configuration Error: Payment mode "%s" not found.')
% mode_search_term,
}

# 3. Create the Group
try:
new_group = (
request.env["recurring.contract.group"]
.sudo()
.create(
{
"partner_id": partner.id,
"payment_mode_id": payment_mode.id,
"recurring_unit": recurring_unit,
"recurring_value": int(advance_billing_months),
"active": True,
}
)
)
new_bvr_ref = new_group.compute_partner_bvr_ref(partner)
if new_bvr_ref:
new_group.bvr_reference = new_bvr_ref

if new_group:
values = self._prepare_sponsorship_values(request.env.user.partner_id)
html = request.env["ir.qweb"]._render(
"my_compassion.my2_sponsorships_section", values
)
return {
"success": True,
"html": html,
"group_id": new_group.id,
"payment_methods": values["payment_methods"],
}

return {"success": False}

except odoo.exceptions.ValidationError:
return {
"success": False,
"error": _("An unexpected error occurred. Please try again."),
}
Comment on lines +677 to +681
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

Catching a broad odoo.exceptions.ValidationError and returning a generic error message hides the specific reason for the failure from the user and makes debugging more difficult. For instance, if a unique constraint is violated, Odoo's default JSON-RPC error handling would provide a much more informative message.

Consider removing this try...except block and letting Odoo's framework handle the ValidationError. This will provide more meaningful feedback to the user.

Suggested change
except odoo.exceptions.ValidationError:
return {
"success": False,
"error": _("An unexpected error occurred. Please try again."),
}
# Let Odoo's default error handler manage ValidationErrors for better feedback.
new_group = (
request.env["recurring.contract.group"]
.sudo()
.create(
{
"partner_id": partner.id,
"payment_mode_id": payment_mode.id,
"recurring_unit": recurring_unit,
"recurring_value": int(advance_billing_months),
"active": True,
}
)
)
new_bvr_ref = new_group.compute_partner_bvr_ref(partner)
if new_bvr_ref:
new_group.bvr_reference = new_bvr_ref
return {"success": True, "group_id": new_group.id}


def _prepare_sponsorship_values(self, 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_methods = [group.get_payment_method_info() for group in all_groups]

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

def _get_paginated_paid_invoices(
self, partner, invoice_page=1, invoice_per_page=12
):
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