Critical - C1: _sql_constraints -> models.Constraint (Odoo 19 deprecation rule violation) - C2: variance threshold no longer uses abs() - under-cost is good news, must not block invoicing. Now only OVER-cost triggers requires_requote. - C3: roll_next_due_date() was dead code - now wired from fusion.technician.task.write() when a maintenance task transitions to 'completed', so the whole maintenance lifecycle actually advances. - C4: warranty.is_active was store=True but time-dependent (became stale). Dropped store=True; find_active_for() now filters by expiry_date directly. High - H1: added x_fc_maintenance_contract_id back-link on repair.order and populated it from create_repair_from_booking(). - H2: find_active_for() returns empty when neither lot nor product is supplied - prevents cross-product false warranty matches. - H3: visit-report wizard now creates stock.move records of repair_line_type 'add' for each part line, so Odoo's native action_create_sale_order() chain has lines to invoice and stock gets consumed properly. - H4: office intake email template now carries a fallback email_to header computed from res.company.x_fc_office_notification_ids (or company email), so it does not silently send with no recipient. - H5: maintenance reminder cron nextcall now always rolls to tomorrow at 07:00 local time, so installing/upgrading after 07:00 does not immediately fire all the day's reminders. - H6: public portal no longer hardcodes UID 1 as the intake user fallback (which in Odoo 19 is OdooBot). Prefers base.user_admin, else the lowest-id non-share user, else SUPERUSER_ID. - H7: public portal validates client_email via tools.email_normalize before partner creation; malformed addresses redirect with error=email. - H8: find_best_match() returns empty when no symptom keywords match (no silent first-catalog guess) and uses word-boundary regex to avoid matching 'battery' inside 'no battery problem'. Medium - M1: _inherit moved next to _name on maintenance_contract (cosmetic but brittle if Odoo refactors model class detection) - M2: relativedelta(months=N) instead of timedelta(days=N*30) for warranty and maintenance intervals (correct month boundaries) - M3: unique constraint on fusion.repair.maintenance.contract.booking_token - M6: dispatch task fallback now searches for an actual x_fc_is_field_staff user; gracefully skips and logs if no field staff exists (instead of silently failing the constraint check) - M7: maintenance contract list view date decoration uses context_today() (date) instead of strftime(string) - the str comparison would TypeError - M9: Visit Report button hidden on draft repairs and when no technician task is linked yet Low - L2: portal-created partners get default lang + company_id so mail templates render in the right language - L3: dropped unused exception variable in sales rep portal controller - L4: visit-report wizard 'found another issue' now redirects to the spawned stub repair so the tech can fill it in immediately - L5: dropped unrecognized data-string from <app> in settings view Public portal also: rate-limit check moved BEFORE the counter increment so blocked attempts do not keep inflating the bucket. All fixes verified end-to-end on local westin-v19: - variance one-sided: 0.5h labour vs $500 est -> requires_requote=False; 2h x $250 + $200 parts vs $100 est -> requires_requote=True - maintenance roll-forward: created MC/00006 due 2026-05-31, completed linked maintenance task -> contract rolled to 2026-11-21 with last_reminder_band reset - warranty find_active_for(partner only) -> empty recordset - service catalog find_best_match with unrelated text -> empty recordset - pg_constraint shows fusion_repair_maintenance_contract_booking_token_unique - /repair landing still 200 after restart Co-authored-by: Cursor <cursoragent@cursor.com>
151 lines
12 KiB
XML
151 lines
12 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<!--
|
|
Fusion Repairs Mail Templates.
|
|
Styling: 4px accent bar, 600px max-width, dark/light safe.
|
|
Mirrors fusion_claims/data/mail_template_data.xml ADP templates for consistency.
|
|
-->
|
|
<odoo>
|
|
<data noupdate="1">
|
|
|
|
<!-- ============================================================== -->
|
|
<!-- Repair Intake Received - Client Confirmation -->
|
|
<!-- ============================================================== -->
|
|
<record id="email_template_intake_received_client" model="mail.template">
|
|
<field name="name">Repair: Intake Received (Client)</field>
|
|
<field name="model_id" ref="repair.model_repair_order"/>
|
|
<field name="subject">{{ object.company_id.name }} - Service Call {{ object.name or 'received' }}</field>
|
|
<field name="email_from">{{ (object.user_id.email_formatted or 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:#2B6CB0;"></div>
|
|
<div style="padding:32px 28px;">
|
|
<p style="color:#2B6CB0;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;">We received your service request</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'"/>, thank you for letting us know about your equipment.
|
|
Your service call reference is <strong><t t-out="object.name"/></strong>.
|
|
</p>
|
|
<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">
|
|
<tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;opacity:0.55;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid rgba(128,128,128,0.25);">Service Call Details</td></tr>
|
|
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);width:35%;">Reference</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>
|
|
<t t-if="object.product_id">
|
|
<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 t-if="object.schedule_date">
|
|
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Scheduled</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.schedule_date" t-options="{'widget': 'datetime'}"/></td></tr>
|
|
</t>
|
|
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;">Status</td><td style="padding:10px 14px;font-size:14px;"><t t-out="dict(object._fields['state'].selection).get(object.state)"/></td></tr>
|
|
</table>
|
|
<div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;">
|
|
<p style="margin:0;font-size:14px;line-height:1.5;">
|
|
A team member will be in touch shortly to confirm the next steps.
|
|
If you need to reach us before then, please contact our office directly.
|
|
</p>
|
|
</div>
|
|
<t t-if="not is_html_empty(object.user_id.signature)" data-o-mail-quote-container="1">
|
|
<div data-o-mail-quote="1">--<br data-o-mail-quote="1"/><t t-out="object.user_id.signature or ''" data-o-mail-quote="1"/></div>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
</field>
|
|
<field name="lang">{{ object.partner_id.lang }}</field>
|
|
<field name="auto_delete" eval="True"/>
|
|
</record>
|
|
|
|
<!-- ============================================================== -->
|
|
<!-- Maintenance Due Reminder -->
|
|
<!-- ============================================================== -->
|
|
<record id="email_template_maintenance_due_reminder" model="mail.template">
|
|
<field name="name">Repair: Maintenance Due Reminder</field>
|
|
<field name="model_id" ref="model_fusion_repair_maintenance_contract"/>
|
|
<field name="subject">{{ object.company_id.name }} - Time to schedule your equipment maintenance</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:#38a169;"></div>
|
|
<div style="padding:32px 28px;">
|
|
<p style="color:#38a169;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;">Your equipment is due for maintenance</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'"/>, your
|
|
<strong><t t-out="object.product_id.display_name or 'equipment'"/></strong>
|
|
is due for its next scheduled maintenance visit on
|
|
<strong><t t-out="object.next_due_date" t-options="{'widget': 'date'}"/></strong>.
|
|
</p>
|
|
<div style="text-align:center;margin:0 0 24px 0;">
|
|
<a t-attf-href="/repairs/maintenance/book/{{ object.booking_token }}"
|
|
style="display:inline-block;padding:14px 28px;background-color:#38a169;color:#ffffff;text-decoration:none;border-radius:6px;font-size:16px;font-weight:600;">
|
|
Book my maintenance visit
|
|
</a>
|
|
</div>
|
|
<div style="border-left:3px solid #38a169;padding:12px 16px;margin:0 0 24px 0;">
|
|
<p style="margin:0;font-size:14px;line-height:1.5;">
|
|
Regular maintenance keeps your equipment safe and reliable. Use the
|
|
button above to confirm and we will reach out to schedule a time that works for you.
|
|
</p>
|
|
</div>
|
|
<p style="opacity:0.55;font-size:12px;margin:0;">
|
|
Contract reference <strong><t t-out="object.name"/></strong>.
|
|
If you no longer have this equipment, you can ignore this email.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</field>
|
|
<field name="lang">{{ object.partner_id.lang }}</field>
|
|
<field name="auto_delete" eval="True"/>
|
|
</record>
|
|
|
|
<!-- ============================================================== -->
|
|
<!-- Repair Intake Received - Office Notification -->
|
|
<!-- ============================================================== -->
|
|
<record id="email_template_intake_received_office" model="mail.template">
|
|
<field name="name">Repair: Intake Received (Office)</field>
|
|
<field name="model_id" ref="repair.model_repair_order"/>
|
|
<field name="subject">[New Service Call] {{ object.partner_id.name or 'Walk-in' }} - {{ object.name or 'n/a' }}</field>
|
|
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
|
|
<field name="email_to">{{ ','.join(p.email for p in (object.company_id.x_fc_office_notification_ids if 'x_fc_office_notification_ids' in object.company_id._fields else []) if p.email) or (object.company_id.email or '') }}</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:#d69e2e;"></div>
|
|
<div style="padding:32px 28px;">
|
|
<p style="color:#d69e2e;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;">
|
|
Internal: New Service Call
|
|
</p>
|
|
<h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">A new repair has been submitted</h2>
|
|
<p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
|
|
Submitted by <strong><t t-out="object.x_fc_intake_user_id.name or object.user_id.name or 'system'"/></strong>
|
|
via the <strong><t t-out="dict(object._fields['x_fc_intake_source'].selection).get(object.x_fc_intake_source) or 'intake'"/></strong>.
|
|
</p>
|
|
<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">
|
|
<tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;opacity:0.55;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid rgba(128,128,128,0.25);">Details</td></tr>
|
|
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);width:35%;">Reference</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);">Client</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.partner_id.name or 'Walk-in / unknown'"/></td></tr>
|
|
<t t-if="object.partner_id.phone">
|
|
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Phone</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.partner_id.phone"/></td></tr>
|
|
</t>
|
|
<t t-if="object.product_id">
|
|
<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>
|
|
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Urgency</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="dict(object._fields['x_fc_urgency'].selection).get(object.x_fc_urgency) or 'normal'"/></td></tr>
|
|
<t t-if="object.x_fc_third_party_equipment">
|
|
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Third-party</td><td style="padding:10px 14px;color:#d69e2e;font-size:14px;font-weight:600;border-bottom:1px solid rgba(128,128,128,0.15);">Yes - equipment not sold by us</td></tr>
|
|
</t>
|
|
<t t-if="object.under_warranty">
|
|
<tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Warranty</td><td style="padding:10px 14px;color:#38a169;font-size:14px;font-weight:600;border-bottom:1px solid rgba(128,128,128,0.15);">Under warranty</td></tr>
|
|
</t>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</field>
|
|
<field name="auto_delete" eval="True"/>
|
|
</record>
|
|
|
|
</data>
|
|
</odoo>
|