changes
This commit is contained in:
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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&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 && typeof google !== 'undefined' && google.maps && 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;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user