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

@@ -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"),