This commit is contained in:
gsinghpal
2026-03-11 12:15:53 -04:00
parent f81e0cd918
commit db4b9aa278
1210 changed files with 173089 additions and 4044 deletions

View File

@@ -448,18 +448,34 @@ class FusionRentalController(http.Controller):
{'error': _("This link is invalid or has expired.")},
)
google_api_key = request.env['ir.config_parameter'].sudo().get_param(
'fusion_claims.google_maps_api_key', ''
)
if not google_api_key:
google_api_key = request.env['ir.config_parameter'].sudo().get_param(
'fusion_rental.google_maps_api_key', ''
)
poynt_business_id = ''
poynt_application_id = ''
provider = request.env['payment.provider'].sudo().search([
('code', '=', 'poynt'),
('state', '!=', 'disabled'),
], limit=1)
if provider:
poynt_business_id = provider.poynt_business_id or ''
raw_app_id = provider.poynt_application_id or ''
from odoo.addons.fusion_poynt.utils import clean_application_id
poynt_application_id = clean_application_id(raw_app_id) or raw_app_id
return request.render(
'fusion_rental.card_reauthorization_page',
{
'order': order,
'partner': order.partner_id,
'poynt_business_id': provider.poynt_business_id if provider else '',
'poynt_application_id': provider.poynt_application_id if provider else '',
'google_api_key': google_api_key,
'poynt_business_id': poynt_business_id,
'poynt_application_id': poynt_application_id,
},
)

View File

@@ -71,8 +71,16 @@
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">New Return</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.rental_return_date and format_datetime(object.rental_return_date, tz=object.company_id.partner_id.tz or 'America/Toronto', dt_format='MMMM dd, yyyy \'at\' hh:mm a') or ''"/></td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;font-weight:600;border-top:2px solid #e2e8f0;">Renewal #</td><td style="padding:10px 14px;color:#38a169;font-size:14px;font-weight:700;border-top:2px solid #e2e8f0;"><t t-out="object.rental_renewal_count"/></td></tr>
</table>
<t t-set="inv" t-value="ctx.get('renewal_invoice')"/>
<t t-if="inv and ctx.get('payment_ok')">
<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">
<tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;color:#718096;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid #e2e8f0;">Payment Receipt</td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;width:35%;">Invoice</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="inv.name or 'Draft'"/></td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;font-weight:600;border-top:2px solid #e2e8f0;">Amount Charged</td><td style="padding:10px 14px;color:#38a169;font-size:14px;font-weight:700;border-top:2px solid #e2e8f0;"><t t-out="inv.amount_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr>
</table>
</t>
<div style="border-left:3px solid #38a169;padding:12px 16px;margin:0 0 24px 0;background:#f7fafc;">
<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;" t-if="ctx.get('payment_ok')">Payment has been collected from your card on file. No further action is required.</p>
<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;" t-if="ctx.get('payment_ok')">Payment has been collected from your card on file. The invoice and payment receipt are attached. No further action is required.</p>
<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;" t-if="not ctx.get('payment_ok')">Our team will be in touch regarding payment for this renewal period.</p>
</div>
<t t-if="not is_html_empty(object.user_id.signature)" data-o-mail-quote-container="1">
@@ -1265,10 +1273,43 @@
</div>
</div>
<!-- Google Places API -->
<script t-att-src="'https://maps.googleapis.com/maps/api/js?key=%s&amp;libraries=places' % (google_api_key or '')">/* Google Places */</script>
<!-- Poynt Collect JS SDK -->
<script src="https://cdn.poynt.net/collect.js"/>
<script>
(function() {
/* ---- Google Places address autocomplete ---- */
var addrInput = document.getElementById('billingAddress');
if (addrInput &amp;&amp; typeof google !== 'undefined' &amp;&amp; google.maps &amp;&amp; google.maps.places) {
var autocomplete = new google.maps.places.Autocomplete(addrInput, {
types: ['address'],
componentRestrictions: { country: ['ca', 'us'] },
});
autocomplete.setFields(['address_components', 'formatted_address']);
autocomplete.addListener('place_changed', function() {
var place = autocomplete.getPlace();
if (!place.address_components) return;
var street = '', city = '', prov = '', postal = '';
place.address_components.forEach(function(c) {
var t = c.types;
if (t.includes('street_number')) street = c.long_name + ' ';
if (t.includes('route')) street += c.long_name;
if (t.includes('locality')) city = c.long_name;
if (t.includes('administrative_area_level_1')) prov = c.short_name;
if (t.includes('postal_code')) postal = c.long_name;
});
addrInput.value = street.trim();
var cityEl = document.getElementById('billingCity');
var stateEl = document.getElementById('billingState');
var postalEl = document.getElementById('billingPostalCode');
if (cityEl) cityEl.value = city;
if (stateEl) stateEl.value = prov;
if (postalEl) postalEl.value = postal;
});
}
var cEl = document.querySelector('.container[data-poynt-business-id]');
var bizId = cEl ? cEl.getAttribute('data-poynt-business-id') : '';
var appId = cEl ? cEl.getAttribute('data-poynt-application-id') : '';
@@ -1329,7 +1370,15 @@
if (!poyntCollect) { showAlert('Payment form is not ready.', 'danger'); return; }
poyntNonce = null;
poyntCollect.getNonce({ businessId: bizId });
var nameParts = name.split(' ');
var firstName = nameParts[0] || '';
var lastName = nameParts.slice(1).join(' ') || '';
poyntCollect.getNonce({
businessId: bizId,
firstName: firstName,
lastName: lastName,
zipCode: postal,
});
var btn = this;
btn.disabled = true;
@@ -1341,7 +1390,7 @@
if (poyntNonce) {
clearInterval(waitForNonce);
submitReauthorization(poyntNonce, btn);
} else if (attempts > 50) {
} else if (attempts > 75) {
clearInterval(waitForNonce);
showAlert('Card authorization timed out. Please try again.', 'danger');
btn.disabled = false;

View File

@@ -4,6 +4,8 @@ import uuid
from datetime import timedelta
from zoneinfo import ZoneInfo
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
@@ -70,7 +72,19 @@ class SaleOrder(models.Model):
string="Original Duration (Days)",
compute='_compute_rental_original_duration',
store=True,
help="Original rental duration in days, used for renewal period calculation.",
help="Current rental period length in days, used for reminder/marketing calculations.",
)
rental_renewal_period = fields.Selection(
[
('daily', 'Daily'),
('weekly', 'Weekly'),
('monthly', 'Monthly'),
('yearly', 'Yearly'),
],
string="Renewal Period",
default='monthly',
help="How often the rental auto-renews. Monthly uses calendar months "
"so the renewal date stays on the same day each month.",
)
rental_renewal_log_ids = fields.One2many(
'rental.renewal.log',
@@ -452,10 +466,11 @@ class SaleOrder(models.Model):
)
def _get_rental_duration_days(self):
"""Return the rental duration to use for renewal.
"""Return the current rental period length in days.
Uses the stored original duration so renewals keep the same
period length even if dates were manually adjusted.
Used for percentage-based calculations (reminders, marketing)
and short-term detection. NOT used for computing the next
renewal date -- see ``_get_renewal_delta`` for that.
"""
self.ensure_one()
if self.rental_original_duration:
@@ -464,6 +479,23 @@ class SaleOrder(models.Model):
return max((self.rental_return_date - self.rental_start_date).days, 1)
return 30
def _get_renewal_delta(self):
"""Return the relativedelta/timedelta to add for the next renewal.
Monthly and yearly periods use ``relativedelta`` so the renewal
date always lands on the same calendar day (e.g. the 13th).
Daily and weekly periods use a fixed ``timedelta``.
"""
self.ensure_one()
period = self.rental_renewal_period or 'monthly'
if period == 'monthly':
return relativedelta(months=1)
if period == 'yearly':
return relativedelta(years=1)
if period == 'weekly':
return timedelta(weeks=1)
return timedelta(days=max(self._get_rental_duration_days(), 1))
def _get_marketing_target_date(self):
"""When to send the purchase marketing email (percentage of period after start)."""
self.ensure_one()
@@ -592,12 +624,19 @@ class SaleOrder(models.Model):
and logs the renewal event.
"""
self.ensure_one()
duration = self._get_rental_duration_days()
rental_lines = self._get_rental_only_lines()
if not rental_lines:
_logger.warning(
"Skipping auto-renewal for %s: no rental lines to invoice.",
self.name,
)
return
old_start = self.rental_start_date
old_return = self.rental_return_date
new_start = old_return
new_return = new_start + timedelta(days=duration)
new_return = new_start + self._get_renewal_delta()
self.write({
'rental_start_date': new_start,
@@ -624,8 +663,6 @@ class SaleOrder(models.Model):
payment_ok = False
if invoice and self.rental_payment_token_id:
payment_ok = self._collect_renewal_payment(invoice, renewal_log)
if payment_ok:
self._send_invoice_with_receipt(invoice, 'renewal')
if invoice and not self.rental_payment_token_id:
self._notify_staff_manual_payment(invoice)
@@ -697,7 +734,7 @@ class SaleOrder(models.Model):
self._notify_staff_manual_payment(invoice)
return False
except (UserError, ValidationError) as e:
except Exception as e:
_logger.error("Auto-payment failed for rental %s: %s", self.name, e)
renewal_log.write({
'payment_status': 'failed',
@@ -724,7 +761,7 @@ class SaleOrder(models.Model):
)
def _send_renewal_confirmation_email(self, renewal_log, payment_ok):
"""Send renewal confirmation email with invoice + receipt attached."""
"""Send a single renewal email with invoice PDF + Poynt receipt attached."""
self.ensure_one()
template = self.env.ref(
'fusion_rental.mail_template_rental_renewed',
@@ -743,17 +780,20 @@ class SaleOrder(models.Model):
attachment_ids = self._generate_invoice_attachments(
invoice, 'Renewal',
)
receipt_ids = self._find_poynt_receipt_attachments(invoice)
attachment_ids.extend(receipt_ids)
try:
template.with_context(
payment_ok=payment_ok,
renewal_log=renewal_log,
renewal_invoice=invoice,
).send_mail(
self.id,
force_send=True,
email_values={'attachment_ids': attachment_ids} if attachment_ids else {},
)
_logger.warning("Renewal confirmation email sent for %s", self.name)
_logger.info("Renewal confirmation email sent for %s", self.name)
except Exception as e:
_logger.error("Failed to send renewal confirmation email for %s: %s", self.name, e)
@@ -919,16 +959,16 @@ class SaleOrder(models.Model):
continue
try:
order._process_auto_renewal()
with self.env.cr.savepoint():
order._process_auto_renewal()
except Exception as e:
_logger.error("Auto-renewal failed for %s: %s", order.name, e)
def action_manual_renewal(self):
"""Open the manual renewal wizard."""
self.ensure_one()
duration = self._get_rental_duration_days()
new_start = self.rental_return_date
new_return = new_start + timedelta(days=duration) if new_start else False
new_return = new_start + self._get_renewal_delta() if new_start else False
return {
'name': _("Renew Rental"),

View File

@@ -145,6 +145,8 @@
invisible="rental_auto_renew"
required="is_rental_order and not rental_auto_renew"
placeholder="Reason for disabling auto-renewal..."/>
<field name="rental_renewal_period"
invisible="not rental_auto_renew"/>
<field name="rental_max_renewals"
invisible="not rental_auto_renew"/>
<field name="rental_next_renewal_date"

View File

@@ -149,7 +149,6 @@ class ManualRenewalWizard(models.TransientModel):
'state': 'done',
'payment_status': 'paid',
})
order._send_invoice_with_receipt(invoice, 'renewal')
order._send_renewal_confirmation_email(renewal_log, True)
return {'type': 'ir.actions.act_window_close'}
else: