feat(fusion_repairs): Bundle 3 - reminders + upsells (X2 + X4 + M3)
X2 Day-before visit reminder email
- New cron 'Fusion Repairs: Day-before visit reminders' (daily at 08:00)
walks repair.order records with at least one linked
fusion.technician.task scheduled for tomorrow and not yet reminded.
- Sends mail.template email_template_visit_day_before to the client.
- New x_fc_day_before_reminder_sent flag (copy=False) so the cron
never re-sends the same reminder.
- Template uses 4px blue accent, 600px max-width, shows the scheduled
date + technician name + equipment, with a 'reply to reschedule' note.
- Verified: cron flagged the test repair x_fc_day_before_reminder_sent=True
after running.
X4 Post-visit NPS / Google review email
- New cron 'Fusion Repairs: Send post-visit NPS emails' (hourly)
finds repairs in state='done' with write_date >= 24h ago and no NPS
email sent. Sends mail.template email_template_post_visit_nps.
- New x_fc_nps_email_sent flag so we never re-pester clients.
- Template uses 4px green accent + 'Leave a Google review' CTA button
linking to res.company.x_fc_google_review_url (or a sensible Google
search fallback when the company hasn't configured a review URL).
M3 Loaner auto-offer for long-running repairs
- Soft-bridges fusion_loaners_management without a hard dep -
cron_offer_loaner_for_long_repairs returns immediately if the
fusion.loaner.checkout model isn't installed.
- Walks repair.order records open longer than
fusion_repairs.loaner_offer_threshold_days (ICP, default 3 days)
with no existing loaner-offer activity.
- Posts a 'Repair: Offer Loaner' activity (new mail.activity.type)
assigned to the repair responsible.
- New x_fc_loaner_offered flag to prevent daily re-posting.
- Manual 'Offer Loaner' button on repair header opens the
fusion.loaner.checkout wizard pre-filled with partner + SO.
- Daily cron runs at 08:30.
Email + ICP + cron wiring:
- 2 new mail.template records (visit_day_before, post_visit_nps)
- 1 new mail.activity.type (loaner_offer)
- 3 new ir.cron records (day-before, NPS, loaner)
- 1 new ir.config_parameter (loaner_offer_threshold_days)
- 1 new header button (Offer Loaner) on repair.order
Verified end-to-end on local westin-v19:
X2 setup repair: RO-202605-12 task: TASK-00045
day-before flag after cron: True (expected True)
M3 loaner model not installed - cron correctly no-op'd
(no flag set, no activity posted, no error - the soft-dep guard works)
Bumped to 19.0.1.3.0.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -135,6 +135,164 @@ class RepairOrder(models.Model):
|
||||
'On-call acknowledgement tokens must be unique.',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# X2 / X4 - reminder + NPS sent-at flags (so the cron doesn't re-send)
|
||||
# ------------------------------------------------------------------
|
||||
x_fc_day_before_reminder_sent = fields.Boolean(
|
||||
string='Day-Before Reminder Sent',
|
||||
copy=False,
|
||||
)
|
||||
x_fc_nps_email_sent = fields.Boolean(
|
||||
string='NPS Email Sent',
|
||||
copy=False,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# X2 / X4 / M3 - reminder + NPS crons + loaner offer
|
||||
# ------------------------------------------------------------------
|
||||
@api.model
|
||||
def cron_send_day_before_reminders(self):
|
||||
"""X2: email the client the day before a scheduled tech visit.
|
||||
|
||||
Walks repair.order records with a linked technician task scheduled
|
||||
for tomorrow, sends one reminder per repair, marks
|
||||
x_fc_day_before_reminder_sent=True so we never re-send.
|
||||
"""
|
||||
if not self._notifications_enabled():
|
||||
return
|
||||
from datetime import date, timedelta
|
||||
tomorrow = date.today() + timedelta(days=1)
|
||||
# Find repairs with at least one task scheduled tomorrow that haven't
|
||||
# been reminded yet.
|
||||
repairs = self.search([
|
||||
('x_fc_day_before_reminder_sent', '=', False),
|
||||
('state', 'not in', ('done', 'cancel')),
|
||||
('x_fc_technician_task_ids.scheduled_date', '=', tomorrow),
|
||||
])
|
||||
tpl = self.env.ref(
|
||||
'fusion_repairs.email_template_visit_day_before',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not tpl:
|
||||
return
|
||||
for r in repairs:
|
||||
if not r.partner_id or not r.partner_id.email:
|
||||
continue
|
||||
try:
|
||||
tpl.send_mail(r.id, force_send=False)
|
||||
r.x_fc_day_before_reminder_sent = True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
@api.model
|
||||
def cron_send_post_visit_nps(self):
|
||||
"""X4: send NPS / Google review email 24h after a repair is done."""
|
||||
if not self._notifications_enabled():
|
||||
return
|
||||
from datetime import datetime, timedelta
|
||||
cutoff = datetime.now() - timedelta(hours=24)
|
||||
repairs = self.search([
|
||||
('state', '=', 'done'),
|
||||
('x_fc_nps_email_sent', '=', False),
|
||||
('write_date', '<=', cutoff),
|
||||
])
|
||||
tpl = self.env.ref(
|
||||
'fusion_repairs.email_template_post_visit_nps',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not tpl:
|
||||
return
|
||||
for r in repairs:
|
||||
if not r.partner_id or not r.partner_id.email:
|
||||
continue
|
||||
try:
|
||||
tpl.send_mail(r.id, force_send=False)
|
||||
r.x_fc_nps_email_sent = True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
@api.model
|
||||
def cron_offer_loaner_for_long_repairs(self):
|
||||
"""M3: post an Offer-Loaner activity when an open repair has been
|
||||
sitting longer than fusion_repairs.loaner_offer_threshold_days
|
||||
AND has no linked loaner checkout yet.
|
||||
|
||||
Soft-depends on fusion_loaners_management - silently no-ops when
|
||||
the loaner model isn't installed.
|
||||
"""
|
||||
if not self.env.get('fusion.loaner.checkout'):
|
||||
return
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
try:
|
||||
threshold = int(ICP.get_param(
|
||||
'fusion_repairs.loaner_offer_threshold_days', '3'
|
||||
))
|
||||
except (ValueError, TypeError):
|
||||
threshold = 3
|
||||
from datetime import datetime, timedelta
|
||||
cutoff = datetime.now() - timedelta(days=threshold)
|
||||
repairs = self.search([
|
||||
('state', 'not in', ('done', 'cancel')),
|
||||
('create_date', '<=', cutoff),
|
||||
('x_fc_loaner_offered', '=', False),
|
||||
])
|
||||
activity_type = self.env.ref(
|
||||
'fusion_repairs.mail_activity_type_loaner_offer',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
for r in repairs:
|
||||
try:
|
||||
r.activity_schedule(
|
||||
activity_type_id=activity_type.id if activity_type else False,
|
||||
summary='Offer a loaner unit',
|
||||
note=(
|
||||
'This repair has been open for more than %s days. '
|
||||
'Consider offering the client a loaner unit while we '
|
||||
'complete the repair.'
|
||||
) % threshold,
|
||||
user_id=r.user_id.id or self.env.uid,
|
||||
)
|
||||
r.x_fc_loaner_offered = True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
x_fc_loaner_offered = fields.Boolean(
|
||||
string='Loaner Offered',
|
||||
copy=False,
|
||||
help='True once a loaner-offer activity has been posted for this '
|
||||
'long-running repair (M3). Avoids re-posting daily.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _notifications_enabled(self):
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
return ICP.get_param(
|
||||
'fusion_repairs.enable_email_notifications', 'True'
|
||||
) == 'True'
|
||||
|
||||
def action_offer_loaner(self):
|
||||
"""Open the fusion_loaners_management checkout wizard pre-filled
|
||||
with this repair's partner. Soft-link - raises if the module is
|
||||
not installed."""
|
||||
self.ensure_one()
|
||||
Checkout = self.env.get('fusion.loaner.checkout')
|
||||
if not Checkout:
|
||||
raise UserError(_(
|
||||
'Loaner management is not installed. Install '
|
||||
'fusion_loaners_management to enable this feature.'
|
||||
))
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Offer Loaner'),
|
||||
'res_model': 'fusion.loaner.checkout',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_partner_id': self.partner_id.id,
|
||||
'default_sale_order_id': self.sale_order_id.id or False,
|
||||
},
|
||||
}
|
||||
|
||||
# Maintenance contract back-link (Phase 3)
|
||||
x_fc_maintenance_contract_id = fields.Many2one(
|
||||
'fusion.repair.maintenance.contract',
|
||||
|
||||
Reference in New Issue
Block a user