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

@@ -4,7 +4,7 @@
{
'name': 'Fusion Repairs',
'version': '19.0.1.3.1',
'version': '19.0.1.4.0',
'category': 'Inventory/Repairs',
'summary': 'Guided medical equipment repair intake, dispatch, maintenance, and self-service portal',
'description': """
@@ -82,6 +82,7 @@ Copyright (C) 2024-2026 Nexa Systems Inc. All rights reserved.
'views/repair_warranty_views.xml',
'views/maintenance_contract_views.xml',
'views/repair_dashboard_views.xml',
'views/repair_inspection_views.xml',
'views/repair_order_views.xml',
'views/sale_order_views.xml',
'views/technician_task_views.xml',
@@ -98,6 +99,7 @@ Copyright (C) 2024-2026 Nexa Systems Inc. All rights reserved.
'wizard/qr_sticker_wizard_views.xml',
# Reports
'report/qr_sticker_report.xml',
'report/inspection_certificate_report.xml',
# Menus (last, after all referenced actions exist)
'views/menus.xml',
],

View File

@@ -56,6 +56,19 @@
<field name="active" eval="True"/>
</record>
<!-- M1: Inspection certificate expiry reminders (30 + 7 days). -->
<record id="cron_inspection_expiry_reminders" model="ir.cron">
<field name="name">Fusion Repairs: Inspection certificate expiry reminders</field>
<field name="model_id" ref="model_fusion_repair_inspection_certificate"/>
<field name="state">code</field>
<field name="code">model.cron_send_expiry_reminders()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall" eval="(DateTime.now().replace(hour=9, minute=0, second=0, microsecond=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="active" eval="True"/>
</record>
<!-- M3: Offer loaner activity for long-running repairs. Runs daily. -->
<record id="cron_offer_loaner" model="ir.cron">
<field name="name">Fusion Repairs: Offer loaner for long-running repairs</field>

View File

@@ -24,6 +24,18 @@
<field name="company_id" eval="False"/>
</record>
<!-- Inspection certificate reference: CERT-YYYY-NNNN, yearly reset. -->
<record id="seq_repair_inspection_certificate" model="ir.sequence">
<field name="name">Inspection Certificate</field>
<field name="code">fusion.repair.inspection.certificate</field>
<field name="prefix">CERT-%(year)s-</field>
<field name="padding">4</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
<field name="use_date_range" eval="True"/>
<field name="company_id" eval="False"/>
</record>
<!-- Date-based repair.order reference: RO-YYYYMM-NN, counter resets monthly.
use_date_range=True so Odoo creates an ir.sequence.date_range record
per month with its own number_next, giving each month a fresh -01.

View File

@@ -143,6 +143,52 @@
<field name="auto_delete" eval="True"/>
</record>
<!-- ============================================================== -->
<!-- M1: Inspection certificate expiry reminder -->
<!-- ============================================================== -->
<record id="email_template_inspection_expiry_reminder" model="mail.template">
<field name="name">Repair: Inspection Certificate Expiry Reminder</field>
<field name="model_id" ref="model_fusion_repair_inspection_certificate"/>
<field name="subject">Your {{ object.product_id.display_name }} inspection certificate expires {{ object.expiry_date }}</field>
<field name="email_from">{{ (object.company_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="body_html" type="html">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="height:4px;background-color:#d97706;"></div>
<div style="padding:32px 28px;">
<p style="color:#d97706;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;">
<t t-out="object.company_id.name"/>
</p>
<h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">Annual safety inspection coming due</h2>
<p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
Hello <t t-out="object.partner_id.name or 'there'"/>, the safety inspection
certificate on your <strong><t t-out="object.product_id.display_name"/></strong>
(certificate <strong><t t-out="object.name"/></strong>) expires
<strong><t t-out="object.expiry_date" t-options="{'widget': 'date'}"/></strong>.
Annual re-inspection keeps your equipment compliant with local safety regulations.
</p>
<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);width:40%;">Certificate</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.name"/></td></tr>
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Equipment</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.product_id.display_name"/></td></tr>
<t t-if="object.lot_id">
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Serial</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.lot_id.name"/></td></tr>
</t>
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;">Expires</td><td style="padding:10px 14px;font-size:14px;color:#d97706;font-weight:600;"><t t-out="object.expiry_date" t-options="{'widget': 'date'}"/></td></tr>
</table>
<div style="border-left:3px solid #d97706;padding:12px 16px;margin:0 0 24px 0;">
<p style="margin:0;font-size:14px;line-height:1.5;">
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.
</p>
</div>
</div>
</div>
</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
<!-- ============================================================== -->
<!-- On-Call Safety Page -->
<!-- ============================================================== -->

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_report_inspection_certificate" model="ir.actions.report">
<field name="name">Inspection Certificate</field>
<field name="model">fusion.repair.inspection.certificate</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">fusion_repairs.report_inspection_certificate</field>
<field name="report_file">fusion_repairs.report_inspection_certificate</field>
<field name="print_report_name">'Inspection Certificate - %s' % (object.name)</field>
<field name="binding_model_id" ref="model_fusion_repair_inspection_certificate"/>
<field name="binding_type">report</field>
</record>
<template id="report_inspection_certificate">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="cert">
<t t-call="web.external_layout">
<div class="page">
<style>
.cert-wrap {
font-family: sans-serif;
padding: 20mm 18mm;
text-align: center;
}
.cert-banner {
font-size: 11pt;
letter-spacing: 0.4em;
text-transform: uppercase;
color: #c0a544;
margin-bottom: 8mm;
}
.cert-title {
font-size: 30pt;
font-weight: 700;
margin: 4mm 0;
}
.cert-sub {
font-size: 13pt;
color: #555;
margin: 0 0 12mm 0;
}
.cert-issued-to {
font-size: 11pt;
color: #666;
text-transform: uppercase;
letter-spacing: 0.3em;
margin-bottom: 4mm;
}
.cert-client {
font-size: 20pt;
font-weight: 600;
margin-bottom: 12mm;
}
.cert-info {
display: flex;
justify-content: center;
gap: 18mm;
margin: 10mm 0;
}
.cert-info-item {
font-size: 10pt;
text-align: left;
}
.cert-info-item .label {
text-transform: uppercase;
color: #888;
letter-spacing: 0.2em;
font-size: 8pt;
margin-bottom: 1mm;
}
.cert-info-item .value {
font-size: 12pt;
font-weight: 600;
}
.cert-footer {
margin-top: 18mm;
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.cert-sig {
font-size: 10pt;
color: #666;
border-top: 1px solid #999;
padding-top: 2mm;
width: 70mm;
text-align: center;
}
.cert-number {
font-size: 9pt;
color: #888;
font-family: ui-monospace, monospace;
}
</style>
<div class="cert-wrap">
<div class="cert-banner">Certificate of Inspection</div>
<div class="cert-title">Safety Inspected</div>
<div class="cert-sub">
This certifies that the equipment described below has passed
its annual safety inspection in accordance with applicable
local regulations.
</div>
<div class="cert-issued-to">Issued To</div>
<div class="cert-client"><t t-out="cert.partner_id.name"/></div>
<div class="cert-info">
<div class="cert-info-item">
<div class="label">Equipment</div>
<div class="value"><t t-out="cert.product_id.display_name"/></div>
</div>
<t t-if="cert.lot_id">
<div class="cert-info-item">
<div class="label">Serial</div>
<div class="value"><t t-out="cert.lot_id.name"/></div>
</div>
</t>
<div class="cert-info-item">
<div class="label">Jurisdiction</div>
<div class="value">
<t t-out="dict(cert._fields['jurisdiction'].selection).get(cert.jurisdiction)"/>
</div>
</div>
</div>
<div class="cert-info">
<div class="cert-info-item">
<div class="label">Issued</div>
<div class="value">
<t t-out="cert.issued_date" t-options="{'widget': 'date'}"/>
</div>
</div>
<div class="cert-info-item">
<div class="label">Valid Until</div>
<div class="value">
<t t-out="cert.expiry_date" t-options="{'widget': 'date'}"/>
</div>
</div>
</div>
<div class="cert-footer">
<div class="cert-number">
Certificate #<t t-out="cert.name"/>
</div>
<div class="cert-sig">
<t t-out="cert.inspector_user_id.name"/><br/>
Inspector
</div>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@@ -26,3 +26,7 @@ access_technician_task_repairs_manager,Technician Task Repairs Manager Full,fusi
access_repair_self_check_rule_user,Self-Check Rule User Read,model_fusion_repair_self_check_rule,group_fusion_repairs_user,1,0,0,0
access_repair_self_check_rule_manager,Self-Check Rule Manager Full,model_fusion_repair_self_check_rule,group_fusion_repairs_manager,1,1,1,1
access_qr_sticker_wizard_user,QR Sticker Wizard User Full,model_fusion_repair_qr_sticker_wizard,group_fusion_repairs_user,1,1,1,1
access_repair_inspection_user,Inspection Cert User Read,model_fusion_repair_inspection_certificate,group_fusion_repairs_user,1,0,0,0
access_repair_inspection_dispatcher,Inspection Cert Dispatcher,model_fusion_repair_inspection_certificate,group_fusion_repairs_dispatcher,1,1,1,0
access_repair_inspection_manager,Inspection Cert Manager Full,model_fusion_repair_inspection_certificate,group_fusion_repairs_manager,1,1,1,1
access_repair_inspection_technician,Inspection Cert Field Tech Create,model_fusion_repair_inspection_certificate,fusion_tasks.group_field_technician,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
26 access_repair_self_check_rule_user Self-Check Rule User Read model_fusion_repair_self_check_rule group_fusion_repairs_user 1 0 0 0
27 access_repair_self_check_rule_manager Self-Check Rule Manager Full model_fusion_repair_self_check_rule group_fusion_repairs_manager 1 1 1 1
28 access_qr_sticker_wizard_user QR Sticker Wizard User Full model_fusion_repair_qr_sticker_wizard group_fusion_repairs_user 1 1 1 1
29 access_repair_inspection_user Inspection Cert User Read model_fusion_repair_inspection_certificate group_fusion_repairs_user 1 0 0 0
30 access_repair_inspection_dispatcher Inspection Cert Dispatcher model_fusion_repair_inspection_certificate group_fusion_repairs_dispatcher 1 1 1 0
31 access_repair_inspection_manager Inspection Cert Manager Full model_fusion_repair_inspection_certificate group_fusion_repairs_manager 1 1 1 1
32 access_repair_inspection_technician Inspection Cert Field Tech Create model_fusion_repair_inspection_certificate fusion_tasks.group_field_technician 1 1 1 0

View File

@@ -39,6 +39,12 @@
action="action_maintenance_contract"
sequence="30"/>
<menuitem id="menu_fusion_repairs_inspections"
name="Inspection Certificates"
parent="menu_fusion_repairs_root"
action="action_repair_inspection"
sequence="35"/>
<!-- Configuration -->
<menuitem id="menu_fusion_repairs_configuration"
name="Configuration"

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_repair_inspection_list" model="ir.ui.view">
<field name="name">fusion.repair.inspection.certificate.list</field>
<field name="model">fusion.repair.inspection.certificate</field>
<field name="arch" type="xml">
<list string="Inspection Certificates"
decoration-success="status == 'valid'"
decoration-warning="status == 'expiring'"
decoration-danger="status == 'expired' or status == 'revoked'">
<field name="name"/>
<field name="partner_id"/>
<field name="product_id"/>
<field name="lot_id" optional="show"/>
<field name="jurisdiction"/>
<field name="issued_date"/>
<field name="expiry_date"/>
<field name="status" widget="badge"/>
<field name="inspector_user_id" optional="hide"/>
</list>
</field>
</record>
<record id="view_repair_inspection_form" model="ir.ui.view">
<field name="name">fusion.repair.inspection.certificate.form</field>
<field name="model">fusion.repair.inspection.certificate</field>
<field name="arch" type="xml">
<form string="Inspection Certificate">
<header>
<button name="action_print" type="object" string="Print Certificate"
class="btn-primary" icon="fa-print"/>
<button name="action_revoke" type="object" string="Revoke"
class="btn-secondary" icon="fa-ban"
invisible="revoked == True"
confirm="Revoke this certificate? This cannot be undone."/>
<field name="status" widget="badge"
decoration-success="status == 'valid'"
decoration-warning="status == 'expiring'"
decoration-danger="status == 'expired' or status == 'revoked'"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<group>
<group>
<field name="partner_id" options="{'no_create': True}"/>
<field name="product_id" options="{'no_create': True}"/>
<field name="lot_id"/>
<field name="repair_order_id" readonly="1"/>
</group>
<group>
<field name="inspector_user_id"/>
<field name="jurisdiction"/>
<field name="issued_date"/>
<field name="valid_for_months"/>
<field name="expiry_date" readonly="1"/>
<field name="last_reminder_band" readonly="1"/>
<field name="revoked" readonly="1"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<field name="notes" placeholder="Inspection notes, measurements, photos taken..."/>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="action_repair_inspection" model="ir.actions.act_window">
<field name="name">Inspection Certificates</field>
<field name="res_model">fusion.repair.inspection.certificate</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

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"