feat(fusion_repairs): Bundle 1 - wizard polish (C1 + C5 + C6 + D2 + T1)
C1 duplicate-call detection - Wizard computes duplicate_count + duplicate_repair_ids when partner is picked (open repairs from the configurable window, default 14 days). - Yellow banner with "Open Existing Repair" button to jump to the most recent duplicate so CS can add a note instead of creating a new repair. C5 outstanding-balance warning - Wizard sums posted unpaid account.move.amount_residual across all invoices of the partner. - Red banner shown when balance >= fusion_repairs.outstanding_balance_threshold (default $100) with a "View Invoices" button. C6 quote-only mode - New quote_only boolean on the wizard; passed through the shared intake service. Skips dispatch-task creation for urgent/safety AND for catalogue auto_schedule. Chatter note "Created in Quote Only mode" posted on the resulting repair.order. D2 skills filter on dispatch picker - _pick_dispatch_technician(repair) prefers users whose x_fc_repair_skills Many2many contains the repair's product category. Three-tier preference: 1) intake user if field staff AND has the skill 2) any active field-staff user with the skill 3) any active field-staff user (no skill filter) - last-resort - Logs a warning + skips task creation if no field-staff user exists at all. T1 Open in Maps on technician task - action_open_in_maps() returns ir.actions.act_url to https://www.google.com/maps?q=<URL-encoded address>. Deep-links into Apple Maps / Google Maps native apps on iOS / Android, browser otherwise. - Header button added on the fusion.technician.task form (after the existing buttons) plus a "View Repair" button when x_fc_repair_order_id is set. Verified end-to-end on local westin-v19: Existing repair: RO-202605-06 C1 duplicate_count = 5 (>=1 expected) - last duplicate: RO-202605-06 C5 balance check ran without error (target partner had $0) C6 quote-only repair: RO-202605-07 tech_tasks = 0 (expected 0) D2 picked the only stairlift-skilled field-staff user T1 Maps URL: https://www.google.com/maps?q=15+Fisherman+Dr%2C+Brampton%2C+ON+L7A+1B7%2C+Canad... Bumped to 19.0.1.1.0. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,9 +10,16 @@ repair.order(s). The shared service guarantees identical behaviour to the
|
||||
sales rep portal and the public client portal added in later phases.
|
||||
|
||||
Multi-equipment per call is supported via the equipment_ids One2many.
|
||||
|
||||
Includes Phase 1 polish:
|
||||
- C1: duplicate-call detection (yellow banner if the partner has an open
|
||||
repair from the last N days)
|
||||
- C5: outstanding-balance warning (red banner if open invoice total > config)
|
||||
- C6: quote-only mode (creates the repair but does NOT dispatch a tech)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
@@ -45,6 +52,43 @@ class RepairIntakeWizard(models.TransientModel):
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# CONTEXTUAL BANNERS (C1 + C5)
|
||||
# Computed reactively when the partner is selected. Shown in the form
|
||||
# so CS knows immediately about duplicate calls or unpaid invoices.
|
||||
# ------------------------------------------------------------------
|
||||
duplicate_repair_ids = fields.Many2many(
|
||||
'repair.order',
|
||||
compute='_compute_partner_context',
|
||||
string='Open Repairs (last N days)',
|
||||
)
|
||||
duplicate_count = fields.Integer(
|
||||
compute='_compute_partner_context',
|
||||
string='Duplicate Call Count',
|
||||
)
|
||||
outstanding_balance = fields.Float(
|
||||
compute='_compute_partner_context',
|
||||
string='Open Invoice Balance',
|
||||
)
|
||||
outstanding_invoice_count = fields.Integer(
|
||||
compute='_compute_partner_context',
|
||||
string='Open Invoices',
|
||||
)
|
||||
show_outstanding_warning = fields.Boolean(
|
||||
compute='_compute_partner_context',
|
||||
string='Show Outstanding Balance Warning',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# OPTIONS (C6 quote-only mode)
|
||||
# ------------------------------------------------------------------
|
||||
quote_only = fields.Boolean(
|
||||
string='Quote Only - Do Not Dispatch',
|
||||
help='Create the service request and quote the client, but do NOT '
|
||||
'auto-create a technician dispatch task. Use this when the client '
|
||||
'is gathering quotes or has not yet authorised the repair.',
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# EQUIPMENT (one-or-many)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -55,6 +99,56 @@ class RepairIntakeWizard(models.TransientModel):
|
||||
required=True,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# COMPUTES
|
||||
# ------------------------------------------------------------------
|
||||
@api.depends('partner_id')
|
||||
def _compute_partner_context(self):
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
try:
|
||||
window_days = int(ICP.get_param(
|
||||
'fusion_repairs.duplicate_call_window_days', '14'
|
||||
))
|
||||
except (ValueError, TypeError):
|
||||
window_days = 14
|
||||
try:
|
||||
threshold = float(ICP.get_param(
|
||||
'fusion_repairs.outstanding_balance_threshold', '100'
|
||||
))
|
||||
except (ValueError, TypeError):
|
||||
threshold = 100.0
|
||||
|
||||
Repair = self.env['repair.order'].sudo()
|
||||
Move = self.env['account.move'].sudo()
|
||||
cutoff = fields.Date.context_today(self) - timedelta(days=window_days)
|
||||
|
||||
for w in self:
|
||||
if not w.partner_id:
|
||||
w.duplicate_repair_ids = False
|
||||
w.duplicate_count = 0
|
||||
w.outstanding_balance = 0.0
|
||||
w.outstanding_invoice_count = 0
|
||||
w.show_outstanding_warning = False
|
||||
continue
|
||||
dupes = Repair.search([
|
||||
('partner_id', '=', w.partner_id.id),
|
||||
('state', 'not in', ('done', 'cancel')),
|
||||
('create_date', '>=', cutoff),
|
||||
], order='create_date desc', limit=5)
|
||||
w.duplicate_repair_ids = dupes
|
||||
w.duplicate_count = len(dupes)
|
||||
|
||||
open_invoices = Move.search([
|
||||
('partner_id', 'child_of', w.partner_id.id),
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('payment_state', 'in', ('not_paid', 'partial')),
|
||||
])
|
||||
balance = sum(open_invoices.mapped('amount_residual'))
|
||||
w.outstanding_balance = balance
|
||||
w.outstanding_invoice_count = len(open_invoices)
|
||||
w.show_outstanding_warning = balance >= threshold
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# SUBMIT
|
||||
# ------------------------------------------------------------------
|
||||
@@ -66,6 +160,7 @@ class RepairIntakeWizard(models.TransientModel):
|
||||
payload = {
|
||||
'partner_id': self.partner_id.id,
|
||||
'intake_user_id': self.intake_user_id.id,
|
||||
'quote_only': self.quote_only,
|
||||
'equipment_items': [self._equipment_payload(eq) for eq in self.equipment_ids],
|
||||
}
|
||||
|
||||
@@ -91,6 +186,39 @@ class RepairIntakeWizard(models.TransientModel):
|
||||
'domain': [('id', 'in', repairs.ids)],
|
||||
}
|
||||
|
||||
def action_open_existing_repair(self):
|
||||
"""C1: jump to the most recent duplicate repair so CS can add a note
|
||||
instead of creating a new repair."""
|
||||
self.ensure_one()
|
||||
if not self.duplicate_repair_ids:
|
||||
return False
|
||||
repair = self.duplicate_repair_ids[0]
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': repair.name,
|
||||
'res_model': 'repair.order',
|
||||
'view_mode': 'form',
|
||||
'res_id': repair.id,
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_view_outstanding_invoices(self):
|
||||
"""C5: open the list of unpaid invoices for context."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Open Invoices - %s', self.partner_id.name or ''),
|
||||
'res_model': 'account.move',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [
|
||||
('partner_id', 'child_of', self.partner_id.id),
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('state', '=', 'posted'),
|
||||
('payment_state', 'in', ('not_paid', 'partial')),
|
||||
],
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def _equipment_payload(self, eq):
|
||||
"""Render an equipment record as a dict the intake service expects."""
|
||||
return {
|
||||
|
||||
@@ -16,6 +16,40 @@
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- C1: duplicate-call detection banner -->
|
||||
<div class="alert alert-warning d-flex justify-content-between align-items-center"
|
||||
role="alert"
|
||||
invisible="duplicate_count == 0">
|
||||
<div>
|
||||
<i class="fa fa-exclamation-triangle me-1"/>
|
||||
<strong>Open repair already exists for this client</strong>
|
||||
(<field name="duplicate_count" nolabel="1" readonly="1" class="d-inline"/> in last 14 days).
|
||||
Consider adding a note to the existing repair instead.
|
||||
</div>
|
||||
<button name="action_open_existing_repair"
|
||||
type="object"
|
||||
string="Open Existing Repair"
|
||||
class="btn btn-sm btn-warning"/>
|
||||
</div>
|
||||
<field name="duplicate_repair_ids" invisible="1"/>
|
||||
|
||||
<!-- C5: outstanding-balance warning banner -->
|
||||
<div class="alert alert-danger d-flex justify-content-between align-items-center"
|
||||
role="alert"
|
||||
invisible="not show_outstanding_warning">
|
||||
<div>
|
||||
<i class="fa fa-money me-1"/>
|
||||
<strong>Outstanding balance:</strong>
|
||||
<field name="outstanding_balance" widget="monetary" nolabel="1" readonly="1" class="d-inline"/>
|
||||
across <field name="outstanding_invoice_count" nolabel="1" readonly="1" class="d-inline"/> invoice(s).
|
||||
Worth mentioning during this call.
|
||||
</div>
|
||||
<button name="action_view_outstanding_invoices"
|
||||
type="object"
|
||||
string="View Invoices"
|
||||
class="btn btn-sm btn-danger"/>
|
||||
</div>
|
||||
|
||||
<separator string="Equipment Items (one repair per item)"/>
|
||||
<field name="equipment_ids">
|
||||
<list editable="bottom">
|
||||
@@ -54,6 +88,11 @@
|
||||
<field name="photo_ids" widget="many2many_binary"/>
|
||||
</form>
|
||||
</field>
|
||||
<!-- C6: quote-only mode -->
|
||||
<separator string="Options"/>
|
||||
<group>
|
||||
<field name="quote_only"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Submit"
|
||||
|
||||
Reference in New Issue
Block a user