+ Hello , the safety inspection
+ certificate on your
+ (certificate ) expires
+ .
+ Annual re-inspection keeps your equipment compliant with local safety regulations.
+
+
+
Certificate
+
Equipment
+
+
Serial
+
+
Expires
+
+
+
+ Reply to this email or call our office to book your re-inspection. We will
+ send our certified technician to confirm everything is safe and renew your
+ certificate.
+
+
+
+
+
+ {{ object.partner_id.lang }}
+
+
+
diff --git a/fusion_repairs/models/__init__.py b/fusion_repairs/models/__init__.py
index 9733b7dd..90e32bdc 100644
--- a/fusion_repairs/models/__init__.py
+++ b/fusion_repairs/models/__init__.py
@@ -12,6 +12,7 @@ from . import maintenance_contract
from . import repair_self_check_rule
from . import repair_ai_service
from . import repair_on_call_service
+from . import repair_inspection
from . import product_template
from . import res_partner
from . import res_users
diff --git a/fusion_repairs/models/repair_inspection.py b/fusion_repairs/models/repair_inspection.py
new file mode 100644
index 00000000..12e6e58e
--- /dev/null
+++ b/fusion_repairs/models/repair_inspection.py
@@ -0,0 +1,229 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024-2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+
+"""Compliance inspection certificates (M1).
+
+Per the design spec section "Phase 4 - Compliance, claims, analytics":
+ Stairlifts / porch lifts need an annual safety inspection certificate
+ (jurisdictional requirement in many places). This model tracks issued
+ certificates, their expiry dates, and a daily cron warns the office +
+ client when one is approaching the 30-day expiry mark.
+
+A certificate is issued AFTER a successful inspection technician visit -
+the visit-report wizard's "Issue Compliance Certificate" button creates
+the record and renders a PDF.
+
+Phase 1 jurisdiction support: a single 'Ontario' jurisdiction text field
+on the certificate; future phases add per-jurisdiction PDF templates.
+"""
+
+from datetime import timedelta
+
+from dateutil.relativedelta import relativedelta
+
+from odoo import _, api, fields, models
+
+
+class FusionRepairInspectionCertificate(models.Model):
+ _name = 'fusion.repair.inspection.certificate'
+ _inherit = ['mail.thread']
+ _description = 'Repair Inspection Certificate'
+ _order = 'issued_date desc, id desc'
+
+ name = fields.Char(
+ string='Certificate Number',
+ required=True,
+ default='New',
+ copy=False,
+ readonly=True,
+ tracking=True,
+ )
+ partner_id = fields.Many2one(
+ 'res.partner',
+ string='Client',
+ required=True,
+ tracking=True,
+ index=True,
+ )
+ product_id = fields.Many2one(
+ 'product.product',
+ string='Equipment',
+ required=True,
+ domain="[('x_fc_repair_category_id.safety_critical', '=', True)]",
+ tracking=True,
+ )
+ lot_id = fields.Many2one(
+ 'stock.lot',
+ string='Serial Number',
+ tracking=True,
+ )
+ repair_order_id = fields.Many2one(
+ 'repair.order',
+ string='Inspection Repair',
+ help='The repair / technician task during which this inspection was done.',
+ ondelete='set null',
+ )
+ inspector_user_id = fields.Many2one(
+ 'res.users',
+ string='Inspector',
+ required=True,
+ default=lambda self: self.env.user,
+ tracking=True,
+ domain="[('x_fc_is_field_staff', '=', True)]",
+ )
+
+ jurisdiction = fields.Selection(
+ [
+ ('on', 'Ontario'),
+ ('bc', 'British Columbia'),
+ ('ab', 'Alberta'),
+ ('qc', 'Quebec'),
+ ('other', 'Other'),
+ ],
+ string='Jurisdiction',
+ default='on',
+ tracking=True,
+ )
+
+ issued_date = fields.Date(
+ string='Issued',
+ required=True,
+ default=fields.Date.context_today,
+ tracking=True,
+ )
+ valid_for_months = fields.Integer(
+ string='Valid For (Months)',
+ default=12,
+ required=True,
+ )
+ expiry_date = fields.Date(
+ string='Expires',
+ compute='_compute_expiry_date',
+ store=True,
+ tracking=True,
+ )
+
+ # Status compute (non-stored - time-dependent, per Bundle 1 C4 fix pattern).
+ status = fields.Selection(
+ [
+ ('valid', 'Valid'),
+ ('expiring', 'Expiring Soon'),
+ ('expired', 'Expired'),
+ ('revoked', 'Revoked'),
+ ],
+ string='Status',
+ compute='_compute_status',
+ )
+ revoked = fields.Boolean(
+ string='Revoked',
+ copy=False,
+ tracking=True,
+ )
+
+ notes = fields.Html(string='Inspector Notes')
+ company_id = fields.Many2one(
+ 'res.company',
+ string='Company',
+ default=lambda self: self.env.company,
+ )
+
+ # Reminder tracking (X2-style band markers so the cron doesn't spam).
+ last_reminder_band = fields.Selection(
+ [('30', '30 days'), ('7', '7 days')],
+ string='Last Reminder',
+ copy=False,
+ )
+
+ _certificate_number_unique = models.Constraint(
+ 'unique(name)',
+ 'Inspection certificate numbers must be unique.',
+ )
+
+ # ------------------------------------------------------------------
+ # CREATE
+ # ------------------------------------------------------------------
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if vals.get('name', 'New') == 'New':
+ vals['name'] = self.env['ir.sequence'].next_by_code(
+ 'fusion.repair.inspection.certificate'
+ ) or 'CERT/NEW'
+ return super().create(vals_list)
+
+ # ------------------------------------------------------------------
+ # COMPUTES
+ # ------------------------------------------------------------------
+ @api.depends('issued_date', 'valid_for_months')
+ def _compute_expiry_date(self):
+ for c in self:
+ if c.issued_date and c.valid_for_months:
+ c.expiry_date = c.issued_date + relativedelta(months=c.valid_for_months)
+ else:
+ c.expiry_date = False
+
+ def _compute_status(self):
+ today = fields.Date.context_today(self)
+ for c in self:
+ if c.revoked:
+ c.status = 'revoked'
+ elif not c.expiry_date:
+ c.status = 'valid'
+ elif c.expiry_date < today:
+ c.status = 'expired'
+ elif c.expiry_date <= today + timedelta(days=30):
+ c.status = 'expiring'
+ else:
+ c.status = 'valid'
+
+ # ------------------------------------------------------------------
+ # ACTIONS
+ # ------------------------------------------------------------------
+ def action_revoke(self):
+ for c in self:
+ c.revoked = True
+ c.message_post(body=_('Certificate revoked.'))
+
+ def action_print(self):
+ self.ensure_one()
+ return self.env.ref(
+ 'fusion_repairs.action_report_inspection_certificate'
+ ).report_action(self)
+
+ # ------------------------------------------------------------------
+ # CRON: warn the client 30 + 7 days before expiry
+ # ------------------------------------------------------------------
+ @api.model
+ def cron_send_expiry_reminders(self):
+ """Daily cron. Sends a reminder at the 30-day band, then again at
+ the 7-day band, so the client books their re-inspection visit
+ before the certificate lapses."""
+ Service = self.env.get('fusion.repair.intake.service')
+ if Service and not Service._notifications_enabled():
+ return
+ today = fields.Date.context_today(self)
+ tpl = self.env.ref(
+ 'fusion_repairs.email_template_inspection_expiry_reminder',
+ raise_if_not_found=False,
+ )
+ if not tpl:
+ return
+ for band_label, days in (('30', 30), ('7', 7)):
+ target = today + timedelta(days=days)
+ certs = self.search([
+ ('revoked', '=', False),
+ ('expiry_date', '=', target),
+ ('partner_id.email', '!=', False),
+ '|', ('last_reminder_band', '=', False),
+ ('last_reminder_band', '!=', band_label),
+ ])
+ for c in certs:
+ # Skip if a smaller band already sent (30 -> 7 progression).
+ if c.last_reminder_band and int(c.last_reminder_band) <= days:
+ continue
+ try:
+ tpl.send_mail(c.id, force_send=False)
+ c.last_reminder_band = band_label
+ except Exception:
+ continue
diff --git a/fusion_repairs/report/inspection_certificate_report.xml b/fusion_repairs/report/inspection_certificate_report.xml
new file mode 100644
index 00000000..d7849b04
--- /dev/null
+++ b/fusion_repairs/report/inspection_certificate_report.xml
@@ -0,0 +1,158 @@
+
+
+
+
+ Inspection Certificate
+ fusion.repair.inspection.certificate
+ qweb-pdf
+ fusion_repairs.report_inspection_certificate
+ fusion_repairs.report_inspection_certificate
+ 'Inspection Certificate - %s' % (object.name)
+
+ report
+
+
+
+
+
+
+
+
+
+
Certificate of Inspection
+
Safety Inspected
+
+ This certifies that the equipment described below has passed
+ its annual safety inspection in accordance with applicable
+ local regulations.
+