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>
159 lines
7.5 KiB
XML
159 lines
7.5 KiB
XML
<?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>
|