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:
@@ -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').
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user