Files
Odoo-Modules/fusion_repairs/views/repair_order_views.xml
gsinghpal 3a15164605 fix(fusion_repairs): Bundle 1 code-review fixes (H1-H5 + M1-M6)
H1 Float -> Monetary for outstanding_balance
  Added currency_id companion field on the wizard so widget="monetary"
  renders properly. Currency defaults to env.company.currency_id.

H2 Maps URL address duplication
  fusion_tasks address_street often contains the full Google-Places-
  formatted address. Concatenating address_street + address_city + zip
  was producing "15 Fisherman Dr, Brampton, ON L7A 1B7, Canada, Brampton,
  L7A 1B7". Now uses the existing address_display field (fusion_tasks
  computes it correctly for both Google Places and manual entries), with
  a partner-based fallback that includes street, street2, city,
  state_id.name, zip, country_id.name.

H3 Banner copy hardcoded "14 days"
  Added duplicate_window_days compute field; banner now reads
  "in last <N> days" from the ir.config_parameter.

H4 Outstanding-balance multi-company + child_of direction
  - Dropped .sudo() (CS users already have access to their own company's
    invoices via standard groups + the Repairs Office rule)
  - Replaced child_of (which only walks descendants) with
    commercial_partner_id (the canonical Odoo "billed-to root" - covers
    child contacts AND walks up from a child if the caller IS a child)
  - Added ('company_id', 'in', env.companies.ids) filter to both the
    invoice search AND the duplicate-repair search so a CS rep in
    Westin Healthcare doesn't see NEXA Systems balances

H5 duplicate_count capped at 5 (false reassurance)
  Now uses search_count for the true total + search(limit=5) for the
  display list. Earlier verification showed count=5 was actually
  capped; running again shows 15 for the same partner.

M1 Function-level imports
  Moved urllib.parse.quote_plus and odoo.exceptions.UserError to module
  top in technician_task.py.

M2 Many2many 'in' with scalar
  Changed ('x_fc_repair_skills', 'in', category.id) to
  ('x_fc_repair_skills', 'in', [category.id]) - safer against future
  ORM tightening.

M4 C6 - added x_fc_is_quote_only field + filter + form indicator
  Boolean tracked field on repair.order (was previously discoverable
  only via chatter text). Indexed. Visible on the form's intake metadata
  row and filterable on the dashboard search view as "Quote Only".

M5 Account-move read perf
  Replaced Move.search() + Python sum with _read_group(
    aggregates=['amount_residual:sum', '__count']) - pushes the SUM to
  Postgres; O(1) record load vs O(N).

M6 Hide Maps button when no address
  Added invisible="not address_display and not partner_id" on the
  Open in Maps button so it doesn't appear on in-store tasks.

Plus the dispatch-task cutoff is now a datetime (was a date) so the
create_date >= cutoff comparison is type-correct.

Verified end-to-end on local westin-v19 after fixes:
  C1 count: 15 (was capped at 5)  window_days: 14
  C5 balance: 0.0  currency: CAD  warning: False (correct)
  C6 x_fc_is_quote_only: True  tech_tasks: 0 (urgent intake, NOT dispatched)
  T1 URL: https://www.google.com/maps?q=15+Fisherman+Dr%2C+Brampton%2C+ON+L7A+1B7%2C+Canada%2C+Unit+7
       (no duplicated city/zip)

Bumped to 19.0.1.1.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 23:34:34 -04:00

283 lines
15 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================== -->
<!-- Form view extensions -->
<!-- ============================================================== -->
<record id="view_repair_order_form_inherit_fusion_repairs" model="ir.ui.view">
<field name="name">repair.order.form.inherit.fusion_repairs</field>
<field name="model">repair.order</field>
<field name="inherit_id" ref="repair.view_repair_order_form"/>
<field name="arch" type="xml">
<!-- Header action buttons (visit report + collect payment) -->
<xpath expr="//header" position="inside">
<button name="action_open_visit_report"
type="object"
string="Visit Report"
class="btn-primary"
invisible="state in ('draft', 'cancel') or x_fc_technician_task_count == 0"
groups="fusion_repairs.group_fusion_repairs_user"/>
<button name="action_collect_payment"
type="object"
string="Collect Payment"
class="btn-secondary"
invisible="state != 'done'"
groups="fusion_repairs.group_fusion_repairs_user"/>
</xpath>
<!-- Smart buttons: Technician Tasks + Intake Answers + Original SO. -->
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button name="action_view_technician_tasks"
type="object"
class="oe_stat_button"
icon="fa-wrench"
invisible="x_fc_technician_task_count == 0">
<field name="x_fc_technician_task_count" widget="statinfo" string="Tech Tasks"/>
</button>
<button name="action_view_intake_answers"
type="object"
class="oe_stat_button"
icon="fa-list-alt"
invisible="x_fc_intake_answer_count == 0">
<field name="x_fc_intake_answer_count" widget="statinfo" string="Answers"/>
</button>
<button name="action_view_original_sale_order"
type="object"
class="oe_stat_button"
icon="fa-dollar"
invisible="not x_fc_original_sale_order_id">
<field name="x_fc_original_sale_order_id" widget="statinfo" string="Original SO"/>
</button>
</xpath>
<!-- Add intake metadata under partner_id -->
<xpath expr="//field[@name='partner_id']" position="after">
<field name="x_fc_repair_category_id" options="{'no_create': True}"/>
<field name="x_fc_urgency" widget="badge"
decoration-success="x_fc_urgency == 'normal'"
decoration-warning="x_fc_urgency == 'urgent'"
decoration-danger="x_fc_urgency == 'safety'"/>
<field name="x_fc_third_party_equipment"/>
<field name="x_fc_is_quote_only"/>
<field name="x_fc_intake_source" readonly="1"/>
<field name="x_fc_intake_user_id" readonly="1" invisible="not x_fc_intake_user_id"/>
<field name="x_fc_intake_session_id" readonly="1" invisible="not x_fc_intake_session_id"/>
</xpath>
<!-- Add a Fusion Repairs notebook tab with intake + photos. -->
<xpath expr="//notebook" position="inside">
<page string="Intake" name="fusion_intake">
<group>
<group>
<field name="x_fc_intake_template_id" readonly="1"/>
<field name="x_fc_issue_category"/>
</group>
<group>
<field name="x_fc_warranty_override_reason"
placeholder="Reason if warranty status was overridden"/>
<field name="x_fc_estimated_duration" widget="float_time"/>
</group>
</group>
<separator string="Answers"/>
<field name="x_fc_intake_answer_ids" readonly="1">
<list>
<field name="sequence" column_invisible="True"/>
<field name="question_name"/>
<field name="value_display"/>
<field name="question_type" optional="hide"/>
</list>
</field>
<separator string="Photos &amp; Videos"/>
<field name="x_fc_photo_ids" widget="many2many_binary"/>
</page>
<page string="Pricing" name="fusion_pricing" invisible="not x_fc_estimated_cost and not x_fc_actual_cost">
<group>
<group>
<field name="x_fc_estimated_cost" widget="monetary"/>
<field name="x_fc_actual_cost" widget="monetary"/>
</group>
<group>
<field name="x_fc_cost_variance_pct" widget="float" digits="[16,2]"/>
<field name="x_fc_requires_requote"/>
<field name="company_currency_id" invisible="1"/>
</group>
</group>
</page>
<page string="AI Brief" name="fusion_ai" invisible="not x_fc_ai_summary">
<field name="x_fc_ai_summary" readonly="1"/>
</page>
</xpath>
</field>
</record>
<!-- ============================================================== -->
<!-- Kanban: add urgency badge + intake source -->
<!-- ============================================================== -->
<record id="view_repair_order_kanban_inherit_fusion_repairs" model="ir.ui.view">
<field name="name">repair.order.kanban.inherit.fusion_repairs</field>
<field name="model">repair.order</field>
<field name="inherit_id" ref="repair.view_repair_kanban"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="x_fc_urgency"/>
<field name="x_fc_third_party_equipment"/>
</xpath>
</field>
</record>
<!-- ============================================================== -->
<!-- List: add urgency + source columns -->
<!-- ============================================================== -->
<record id="view_repair_order_list_inherit_fusion_repairs" model="ir.ui.view">
<field name="name">repair.order.list.inherit.fusion_repairs</field>
<field name="model">repair.order</field>
<field name="inherit_id" ref="repair.view_repair_order_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="x_fc_urgency" widget="badge"
decoration-success="x_fc_urgency == 'normal'"
decoration-warning="x_fc_urgency == 'urgent'"
decoration-danger="x_fc_urgency == 'safety'"
optional="show"/>
<field name="x_fc_intake_source" optional="hide"/>
<field name="x_fc_third_party_equipment" optional="hide"/>
</xpath>
</field>
</record>
<!-- ============================================================== -->
<!-- New Service Call action - opens the wizard as a modal -->
<!-- ============================================================== -->
<record id="action_open_repair_intake_wizard" model="ir.actions.act_window">
<field name="name">New Service Call</field>
<field name="res_model">fusion.repair.intake.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- ============================================================== -->
<!-- Fusion Repairs Service Calls dashboard -->
<!-- Branded kanban / list of repair.order filtered to repairs that -->
<!-- came through one of the Fusion intake surfaces, with the -->
<!-- New Service Call wizard wired into the header. -->
<!-- ============================================================== -->
<record id="view_fusion_repair_dashboard_kanban" model="ir.ui.view">
<field name="name">repair.order.dashboard.fusion_repairs</field>
<field name="model">repair.order</field>
<field name="arch" type="xml">
<kanban default_group_by="state"
class="o_kanban_small_column o_kanban_repair_dashboard"
sample="1">
<field name="name"/>
<field name="partner_id"/>
<field name="state"/>
<field name="x_fc_urgency"/>
<field name="x_fc_third_party_equipment"/>
<field name="x_fc_repair_category_id"/>
<field name="x_fc_intake_source"/>
<field name="x_fc_estimated_cost"/>
<field name="company_currency_id"/>
<field name="schedule_date"/>
<templates>
<t t-name="card">
<div class="d-flex justify-content-between mb-2">
<strong class="o_kanban_record_title">
<field name="name"/>
</strong>
<span t-attf-class="badge {{ {'safety':'text-bg-danger','urgent':'text-bg-warning','normal':'text-bg-secondary'}[record.x_fc_urgency.raw_value] }}">
<field name="x_fc_urgency"/>
</span>
</div>
<div class="text-muted small mb-1">
<i class="fa fa-user me-1"/>
<field name="partner_id"/>
</div>
<div class="text-muted small mb-1" t-if="record.x_fc_repair_category_id.raw_value">
<i class="fa fa-wrench me-1"/>
<field name="x_fc_repair_category_id"/>
</div>
<div class="text-muted small mb-1" t-if="record.schedule_date.raw_value">
<i class="fa fa-calendar me-1"/>
<field name="schedule_date" widget="date"/>
</div>
<div class="d-flex justify-content-between mt-2">
<span class="small text-muted">
<field name="x_fc_intake_source"/>
</span>
<span t-if="record.x_fc_third_party_equipment.raw_value"
class="badge text-bg-warning small">3rd-party</span>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fusion_repair_dashboard_search" model="ir.ui.view">
<field name="name">repair.order.search.fusion_repairs</field>
<field name="model">repair.order</field>
<field name="arch" type="xml">
<search string="Service Calls">
<field name="name"/>
<field name="partner_id"/>
<field name="x_fc_repair_category_id"/>
<filter string="Today" name="today"
domain="[('create_date', '&gt;=', datetime.datetime.combine(context_today(), datetime.time(0,0,0)))]"/>
<filter string="This Week" name="week"
domain="[('create_date', '&gt;=', datetime.datetime.combine(context_today() - datetime.timedelta(days=7), datetime.time(0,0,0)))]"/>
<separator/>
<filter string="Safety" name="safety"
domain="[('x_fc_urgency', '=', 'safety')]"/>
<filter string="Urgent" name="urgent"
domain="[('x_fc_urgency', '=', 'urgent')]"/>
<filter string="Third-Party" name="thirdparty"
domain="[('x_fc_third_party_equipment', '=', True)]"/>
<filter string="Quote Only" name="quote_only"
domain="[('x_fc_is_quote_only', '=', True)]"/>
<separator/>
<filter string="From Backend Wizard" name="src_backend"
domain="[('x_fc_intake_source', '=', 'backend_wizard')]"/>
<filter string="From Sales Rep Portal" name="src_salesrep"
domain="[('x_fc_intake_source', '=', 'sales_rep_portal')]"/>
<filter string="From Client Portal" name="src_client"
domain="[('x_fc_intake_source', '=', 'client_portal')]"/>
<separator/>
<filter string="Open (not closed)" name="open"
domain="[('state', 'not in', ('done', 'cancel'))]"/>
<group>
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
<filter string="Urgency" name="group_urgency" context="{'group_by': 'x_fc_urgency'}"/>
<filter string="Category" name="group_category" context="{'group_by': 'x_fc_repair_category_id'}"/>
<filter string="Intake Source" name="group_source" context="{'group_by': 'x_fc_intake_source'}"/>
</group>
</search>
</field>
</record>
<record id="action_fusion_repair_dashboard" model="ir.actions.act_window">
<field name="name">Service Calls</field>
<field name="res_model">repair.order</field>
<field name="view_mode">kanban,list,form</field>
<field name="search_view_id" ref="view_fusion_repair_dashboard_search"/>
<field name="context">{'search_default_open': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">No service calls yet</p>
<p>
Click <strong>New</strong> in the top-left to open the guided
intake wizard. The form will walk you through caller info,
equipment selection, the issue, urgency and photos.
</p>
</field>
</record>
<record id="action_fusion_repair_dashboard_kanban" model="ir.actions.act_window.view">
<field name="sequence" eval="1"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="view_fusion_repair_dashboard_kanban"/>
<field name="act_window_id" ref="action_fusion_repair_dashboard"/>
</record>
</odoo>