feat(fusion_repairs): Bundle 4 - M1 compliance inspection certificates

New fusion.repair.inspection.certificate model for the annual safety
inspections required on stairlifts, porch lifts, and power wheelchairs
in many jurisdictions.

Model
- mail.thread chatter-tracked; fields: name (CERT-YYYY-NNNN auto-seq),
  partner_id, product_id (filtered to safety-critical categories), lot_id,
  repair_order_id back-link, inspector_user_id (must be field staff),
  jurisdiction (selection: Ontario / BC / Alberta / Quebec / Other),
  issued_date, valid_for_months (default 12), expiry_date (computed,
  stored, uses relativedelta - correct month boundaries), status
  (non-stored compute: valid / expiring / expired / revoked), revoked,
  notes, last_reminder_band.
- Unique constraint on certificate number (models.Constraint, not
  _sql_constraints, per project rule).
- Sequence 'fusion.repair.inspection.certificate' with use_date_range=True
  so the counter resets each year (CERT-2026-0001 ... CERT-2027-0001).

Visit report integration
- New issue_inspection_cert checkbox on fusion.repair.visit.report.wizard.
- When ticked AND the repair's category is safety_critical, action_confirm()
  creates the certificate via _create_inspection_certificate() and
  redirects to the cert form so the tech can print immediately.
- Non-safety-critical equipment quietly skips with a chatter note
  explaining why.

PDF report
- web.html_container + web.external_layout, model bound so it appears
  as a Print action on the certificate form.
- 'Certificate of Inspection' / 'Safety Inspected' gold-banner layout
  with client name, equipment, serial, jurisdiction, issued + expiry
  dates, inspector signature line, and the certificate number.
- Print Certificate button in form header.

Daily cron
- cron_send_expiry_reminders runs at 09:00, sends two band-tracked
  reminders (30 days + 7 days before expiry) to the client.
- New mail.template email_template_inspection_expiry_reminder with
  4px amber accent, certificate ref, equipment, expiry date, and a
  CTA to call to book the re-inspection visit.
- last_reminder_band on the cert prevents re-sending the same band.

Backend wiring
- New menu entry 'Fusion Repairs > Inspection Certificates'.
- ACL: User read, Dispatcher write, Manager unlink. Field technicians
  can create (they need to issue from the field).
- List view with red/amber/green status decoration.
- Form with statusbar, header buttons (Print, Revoke with confirm),
  chatter.

Verified end-to-end on local westin-v19:
  Stairlift repair RO-202605-15 -> visit-report with issue_inspection_cert=True
  -> CERT-2026-0001 issued (status=valid, expires 2027-05-21)
  Cert CERT-2026-0002 expiring in 30 days -> cron flagged
  last_reminder_band='30' (would email client).

Bumped to 19.0.1.4.0 (minor bump for the new public-facing capability).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
gsinghpal
2026-05-21 00:11:59 -04:00
parent ef0c096e48
commit 65c4d8801c
12 changed files with 618 additions and 5 deletions

View File

@@ -66,6 +66,19 @@ class RepairVisitReportWizard(models.TransientModel):
help='Tick to spawn a follow-up repair after saving this visit.',
)
# M1: tick when the visit was a safety inspection. On save the wizard
# creates a fusion.repair.inspection.certificate.
issue_inspection_cert = fields.Boolean(
string='Issue Compliance Certificate',
help='Tick when the visit was an annual safety inspection. Creates an '
'inspection certificate record and prints the PDF on save.',
)
inspection_cert_id = fields.Many2one(
'fusion.repair.inspection.certificate',
string='Issued Certificate',
readonly=True,
)
# Variance display
estimated_cost = fields.Monetary(
related='repair_id.x_fc_estimated_cost',
@@ -169,17 +182,66 @@ class RepairVisitReportWizard(models.TransientModel):
)) % {'name': stub.name or ''},
)
# M1: issue an inspection certificate when the box is ticked
# AND the equipment is safety-critical (stairlift / porch lift / power chair).
if self.issue_inspection_cert:
self._create_inspection_certificate(repair)
# If a stub was spawned, open it directly so the tech can fill in details.
target_id = stub.id if stub else repair.id
target_name = stub.name if stub else repair.name
# Otherwise, if a certificate was issued, jump to it so the tech can print.
if stub:
return {
'type': 'ir.actions.act_window',
'name': stub.name,
'res_model': 'repair.order',
'view_mode': 'form',
'res_id': stub.id,
}
if self.inspection_cert_id:
return {
'type': 'ir.actions.act_window',
'name': self.inspection_cert_id.name,
'res_model': 'fusion.repair.inspection.certificate',
'view_mode': 'form',
'res_id': self.inspection_cert_id.id,
}
return {
'type': 'ir.actions.act_window',
'name': target_name,
'name': repair.name,
'res_model': 'repair.order',
'view_mode': 'form',
'res_id': target_id,
'res_id': repair.id,
}
def _create_inspection_certificate(self, repair):
"""M1: create the inspection certificate. Requires a safety-critical
equipment category - otherwise just logs to chatter and skips."""
category = repair.x_fc_repair_category_id
if not category or not category.safety_critical:
repair.message_post(body=_(
'Inspection certificate skipped - equipment category is not '
'flagged as safety_critical. Only stairlifts, porch lifts, '
'and power wheelchairs receive annual certificates.'
))
return
if not repair.product_id:
repair.message_post(body=_(
'Inspection certificate skipped - the repair has no product set.'
))
return
Cert = self.env['fusion.repair.inspection.certificate'].sudo()
cert = Cert.create({
'partner_id': repair.partner_id.id,
'product_id': repair.product_id.id,
'lot_id': repair.lot_id.id if repair.lot_id else False,
'repair_order_id': repair.id,
'inspector_user_id': self.technician_id.id or self.env.uid,
})
self.inspection_cert_id = cert
repair.message_post(body=_(
'Issued inspection certificate %s (expires %s).'
) % (cert.name, cert.expiry_date))
def _create_repair_part_moves(self, repair):
"""Create stock.move records for each part used (repair_line_type='add').

View File

@@ -49,6 +49,7 @@
<separator string="Outcome"/>
<field name="notes"/>
<field name="found_another_issue"/>
<field name="issue_inspection_cert"/>
</sheet>
<footer>
<button string="Save Visit Report"