-
Notifications
You must be signed in to change notification settings - Fork 10
T2534 payment method storing and management #231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 14.0-MyCompassion2.0
Are you sure you want to change the base?
T2534 payment method storing and management #231
Conversation
…r a contract and to update differents groups of payment methods
… of payment methods
|
/gemini review |
There was a problem hiding this comment.
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 a significant and valuable feature, refactoring the donations page to group sponsorships by payment method and adding comprehensive management capabilities. The changes are well-structured, spanning across the backend controllers and models to the frontend JavaScript widgets and templates. My review focuses on improving robustness, performance, and maintainability. I've identified a few issues, including a duplicated line of code, a potential logic flaw in the JavaScript event handling, and opportunities for optimization by improving data fetching and moving business logic from templates to the model layer. Addressing these points will make the implementation even more solid.
my_compassion/models/contracts.py
Outdated
|
|
||
| # Move the contract to the new group | ||
| self.write({"group_id": target_group.id}) | ||
| self.write({"group_id": target_group.id}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| except Exception: | ||
| return { | ||
| "success": False, | ||
| "error": _("An unexpected error occurred. Please try again."), | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of a broad except Exception: clause can hide unexpected errors and make debugging more difficult. It's better to catch more specific exceptions that you might anticipate during the group creation process, such as odoo.exceptions.ValidationError. Additionally, you should log the exception to help with troubleshooting in production environments.
| all_icons = self.env["payment.icon"].sudo().search([("image", "!=", False)]) | ||
| for icon in all_icons: | ||
| if icon.name.lower() in self.payment_mode_id.name.lower(): | ||
| info["icon"] = icon.id | ||
| break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation to find a payment icon is inefficient and could be unreliable.
- Performance:
self.env["payment.icon"].sudo().search(...)is called every timeget_payment_method_infois executed. This can lead to performance issues if there are many icons or if the method is called frequently, as it queries the database repeatedly for the same data. - Correctness: The check
if icon.name.lower() in self.payment_mode_id.name.lower():performs a fuzzy match, which might lead to incorrect icons being displayed if payment mode names are substrings of others (e.g., 'Visa' and 'Visa Electron').
Consider fetching all icons once and caching them, or establishing a more direct relationship between account.payment.mode and payment.icon to avoid searching and string matching.
| <t t-name="my_compassion.FormFieldComponent"> | ||
| <div t-attf-class="form-field-container mb-3 #{top_class || ''}"> | ||
| <t t-if="label"> | ||
| <label t-att-for="name" class="text-core-blue body-small bold mb-1"> | ||
| <t t-esc="label" /> | ||
| </label> | ||
| </t> | ||
| <!-- Slot content is simulated by passing value/placeholder --> | ||
| <input | ||
| type="text" | ||
| t-att-name="name" | ||
| t-att-id="name" | ||
| t-attf-class="form-control #{input_class || ''}" | ||
| t-att-value="value" | ||
| t-att-placeholder="placeholder || ''" | ||
| t-att-disabled="disabled" | ||
| /> | ||
| <t t-if="invalid_hint"> | ||
| <div class="invalid-hint text-mid-orange tiny-text mb-1 d-none"> | ||
| <t t-esc="invalid_hint" /> | ||
| </div> | ||
| </t> | ||
| </div> | ||
| </t> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This FormFieldComponent template appears to be a client-side duplication of the server-side theme_compassion_2025.FormFieldComponent. Duplicating component templates increases maintenance overhead, as any changes would need to be applied in both places. While sometimes necessary in Odoo, consider if there's a way to render the backend template via an RPC call or unify the component definitions to have a single source of truth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/gemini Could you provide me an implementation for that ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello SamBachmann, thanks for asking for clarification on this. You're right to ask for an implementation, as achieving a single source of truth for the FormFieldComponent in this context is a bit more involved due to how Odoo's client-side and server-side QWeb templates interact.
The Challenge:
The my_compassion.FormFieldComponent template (lines 159-182 in my_compassion/static/src/xml/my2_payment_method_templates.xml) is a client-side QWeb template. It's loaded by your JavaScript widget (my2_donations.js) and used by other client-side templates like my_compassion.PaymentMethodUpdateForm.
On the other hand, theme_compassion_2025.FormFieldComponent is a server-side QWeb template. Client-side QWeb templates cannot directly call server-side QWeb templates using t-call.
Therefore, simply removing the client-side my_compassion.FormFieldComponent would break the client-side rendering flow for your modals.
Proposed Implementation for Unification (RPC Approach):
To truly unify and avoid this duplication, the most robust approach would be to render the FormFieldComponent server-side and fetch its HTML content via an RPC call when needed. This would involve the following steps:
-
Remove the client-side
my_compassion.FormFieldComponentdefinition.- Delete lines 159-182 from
my_compassion/static/src/xml/my2_payment_method_templates.xml.
- Delete lines 159-182 from
-
Create a new RPC endpoint in your Python controller (
my_compassion/controllers/my2_donations.py).- This endpoint would take parameters like
label,name,value,placeholder,disabled,input_class, andinvalid_hint. - It would then render the server-side
theme_compassion_2025.FormFieldComponentusing these parameters and return the generated HTML string.
# Example in my_compassion/controllers/my2_donations.py @http.route('/my2/render_form_field', type='json', auth='user', website=True) def render_form_field_component(self, label=None, name=None, value=None, placeholder=None, disabled=False, input_class='', invalid_hint=None, **kw): return request.env['ir.ui.view']._render_template( 'theme_compassion_2025.FormFieldComponent', { 'label': label, 'name': name, 'id': name, # Assuming id is same as name for this component 'value': value, 'placeholder': placeholder, 'disabled': disabled, 'input_class': input_class, 'invalid_hint': invalid_hint, } )
- This endpoint would take parameters like
-
Modify your JavaScript widget (
my_compassion/static/src/js/my2_donations.js) and themy_compassion.PaymentMethodUpdateFormtemplate.- In
my_compassion.PaymentMethodUpdateForm, remove thet-call="my_compassion.FormFieldComponent"and replace it with a placeholder element (e.g., adivwith a specific ID) where the dynamically loaded HTML will be injected. - In your
_onOpenPaymentMethodUpdatefunction (or a new helper function), make an RPC call to the new/my2/render_form_fieldendpoint, passing the necessary data for the form field. - Upon receiving the HTML response, inject it into the placeholder element in your modal.
// Example in my_compassion/static/src/js/my2_donations.js _onOpenPaymentMethodUpdate: function (ev) { // ... existing code ... var methodInfo = detail ? detail.method_info : null; var $modalUpdate = $("#payment_method_selector_modal_update"); var $container = $modalUpdate.find("#modal_container").empty(); // Render the accordion structure first $container.html(QWeb.render("my_compassion.PaymentMethodUpdateAccordion", methodInfo)); // Now, fetch and inject the form field HTML via RPC this._rpc({ route: '/my2/render_form_field', params: { label: _t("Reference Number"), name: "ref_number", value: methodInfo.ref_number || '', // Add other parameters as needed }, }).then(function (formFieldHtml) { $modalUpdate.find('#form_field_placeholder').html(formFieldHtml); // Assuming a placeholder div with id 'form_field_placeholder' }); this._renderPaymentMethods($modalUpdate, groupId, "#payment_methods_switch_container"); $modalUpdate.modal("show"); },
<!-- Example in my_compassion/static/src/xml/my2_payment_method_templates.xml for PaymentMethodUpdateForm --> <t t-name="my_compassion.PaymentMethodUpdateForm"> <div class="payment-update-form text-left"> <!-- Placeholder for the server-rendered FormFieldComponent --> <div id="form_field_placeholder"></div> </div> </t>
- In
This approach ensures that the FormFieldComponent logic is maintained in a single server-side template (theme_compassion_2025.FormFieldComponent), and the client-side only receives the rendered HTML, thus eliminating the duplication. This is a medium priority change as it improves maintainability and reduces code duplication.
|
/gemini review |
There was a problem hiding this comment.
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 a significant and valuable feature for payment method management, refactoring the donations page to group sponsorships by payment methods. The implementation is well-structured, leveraging new backend models, controller endpoints, and frontend components, including modals and an Odoo public widget for interactivity. My review focuses on improving performance by addressing a potential N+1 query issue, enhancing the robustness of the new controller endpoints with better input validation and error handling, and correcting a minor typo in a user-facing message. Overall, this is a solid contribution that greatly enhances user capabilities.
| def get_payment_method_info(self): | ||
| """ | ||
| Returns a dict containing display info for the group's payment method. | ||
| Used in MyCompassion2.0 portal. | ||
| """ | ||
| self.ensure_one() | ||
|
|
||
| # Default / Fallback values | ||
| info = { | ||
| "icon": False, | ||
| "ref_number": False, | ||
| "label": _("Unknown Method"), | ||
| "expire_date": False, | ||
| "is_card": False, | ||
| "mode_id": self.payment_mode_id.id if self.payment_mode_id else False, | ||
| "group_id": self.id, | ||
| } | ||
|
|
||
| if not self.payment_mode_id: | ||
| return info | ||
|
|
||
| all_icons = self.env["payment.icon"].sudo().search([("image", "!=", False)]) | ||
| for icon in all_icons: | ||
| if icon.name.lower() in self.payment_mode_id.name.lower(): | ||
| info["icon"] = icon.id | ||
| break | ||
|
|
||
| # Basic Mode Info | ||
| info["label"] = self.payment_mode_id.display_name | ||
| info["type"] = "mode" | ||
| info["ref_number"] = self.bvr_reference if self.bvr_reference else False | ||
|
|
||
| return info |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method has a performance issue (N+1 problem). The line all_icons = self.env["payment.icon"].sudo().search(...) is executed for every single group when this method is called in a loop from the controller or a template. This will lead to a large number of database queries and slow down page load times, especially for users with many payment methods.
To fix this, you should refactor the method to work on a recordset (self) instead of a single record. This involves removing self.ensure_one() and performing the payment.icon search only once outside the loop. The method would then return a list of dictionaries.
After refactoring, you will also need to update the call sites in my_compassion/controllers/my2_donations.py and any templates to call this method on the entire recordset at once, rather than in a loop.
def get_payment_method_info(self):
"""
Returns a list of dicts containing display info for the groups' payment methods.
Used in MyCompassion2.0 portal.
"""
if not self:
return []
all_icons = self.env["payment.icon"].sudo().search([("image", "!=", False)])
icon_data = [(icon.name.lower(), icon.id) for icon in all_icons]
results = []
for group in self:
info = {
"icon": False,
"ref_number": False,
"label": _("Unknown Method"),
"expire_date": False,
"is_card": False,
"mode_id": group.payment_mode_id.id if group.payment_mode_id else False,
"group_id": group.id,
}
if group.payment_mode_id:
info["label"] = group.payment_mode_id.display_name
info["type"] = "mode"
info["ref_number"] = group.bvr_reference or False
pm_name_lower = group.payment_mode_id.name.lower()
for icon_name, icon_id in icon_data:
if icon_name in pm_name_lower:
info["icon"] = icon_id
break
results.append(info)
return results| 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)) | ||
| return {"success": success} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parameters contract_id and group_id are cast to int without proper error handling. If a non-integer value is passed in the JSON request, this will raise a ValueError and result in a 500 Internal Server Error. It's safer to wrap the casting in a try-except block and raise a BadRequest for invalid input. This will provide a clearer error to the client.
This same issue is present in other new endpoints in this file where int() is used on request parameters.
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
try:
contract_id = int(contract_id)
group_id = int(group_id)
except (ValueError, TypeError):
raise BadRequest()
# Verify that the contract belongs to the user
contract = (
request.env["recurring.contract"]
.sudo()
.search([("id", "=", contract_id), ("partner_id", "=", partner.id)])
)
if not contract:
raise NotFound()
success = contract.change_contract_group(group_id)
return {"success": success}| except odoo.exceptions.ValidationError: | ||
| return { | ||
| "success": False, | ||
| "error": _("An unexpected error occurred. Please try again."), | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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} |
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
|
/gemini review |
There was a problem hiding this comment.
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 a significant and well-structured refactoring of the donations page, adding payment method management capabilities. The backend logic is sound, with new controller endpoints and model methods to support the frontend functionality. The frontend is nicely refactored into an Odoo JavaScript widget, handling modals and UI updates gracefully.
My review includes a few suggestions for improvement:
- Translating user-facing error messages.
- Optimizing a database query for better performance.
- Removing leftover debugging code.
- Improving file naming for better maintainability.
Overall, this is a solid implementation of a complex feature.
| "payment_methods": values["payment_methods"], | ||
| } | ||
|
|
||
| return {"success": False, "error": "Operation failed"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| "payment_methods": values["payment_methods"], | ||
| } | ||
|
|
||
| return {"success": False, "error": "Operation failed"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| all_icons = self.env["payment.icon"].sudo().search([("image", "!=", False)]) | ||
| for icon in all_icons: | ||
| if icon.name.lower() in self.payment_mode_id.name.lower(): | ||
| info["icon"] = icon.id | ||
| break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fetching all payment icons from the database on every call to get_payment_method_info is inefficient and could lead to performance issues, especially if the number of icons grows. It's better to cache the list of icons. You could use Odoo's @tools.ormcache() decorator on a model method that fetches the icons to avoid repeated database queries.
| var detail = ev.detail || {}; | ||
| var $container = this.$("#my_sponsorships_container"); | ||
| this.paymentMethods = $container.data("payment-methods") || []; | ||
| console.log(this.paymentMethods); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary
Refactors the
/my2/donationspage to group sponsorships by their payment bundle (Contract Group) and introduces full management capabilities for payment methods (Add, Update, Change) directly from myCompassion2.0.Key Changes
Grouped View: Sponsorships are now visually grouped by their backend payment method.
Payment Management: Added a modal interface to:
Add: Create new manual payment methods (BVR, LSV, Permanent Order).
Update: Edit details (e.g., BVR Reference) for an existing group.
Change: Move a specific sponsorship to another existing payment group.
Feedback: Implemented Toast notifications for user actions (Success/Error).
Controller: Added routes to handle manual group creation and method switching.
Backend: Add methods to models (
res_partner,contracts,contract_group) to retrieve, change and update groups.Fontend: Handle modals, trigger RPC request, allow optimistic UI.
Results:
/my2/donations page displaying
Change payment method for a contract modal
Update payment method for a group of contract modal