diff --git a/fusion_repairs/__manifest__.py b/fusion_repairs/__manifest__.py
index b0de4ed2..5c9be84d 100644
--- a/fusion_repairs/__manifest__.py
+++ b/fusion_repairs/__manifest__.py
@@ -4,7 +4,7 @@
{
'name': 'Fusion Repairs',
- 'version': '19.0.1.0.7',
+ 'version': '19.0.1.1.0',
'category': 'Inventory/Repairs',
'summary': 'Guided medical equipment repair intake, dispatch, maintenance, and self-service portal',
'description': """
@@ -83,6 +83,7 @@ Copyright (C) 2024-2026 Nexa Systems Inc. All rights reserved.
'views/repair_dashboard_views.xml',
'views/repair_order_views.xml',
'views/sale_order_views.xml',
+ 'views/technician_task_views.xml',
'views/res_partner_views.xml',
'views/res_users_views.xml',
'views/res_config_settings_views.xml',
diff --git a/fusion_repairs/models/intake_service.py b/fusion_repairs/models/intake_service.py
index ef44a55a..b1ab387c 100644
--- a/fusion_repairs/models/intake_service.py
+++ b/fusion_repairs/models/intake_service.py
@@ -39,6 +39,7 @@ class FusionRepairIntakeService(models.AbstractModel):
:param payload: dict with keys:
- partner_id: int (required) or partner_vals: dict to create new partner
- intake_user_id: int (optional, defaults to env.user)
+ - quote_only: bool (optional, C6 - skips dispatch task creation)
- equipment_items: list of dicts, each with:
- product_id: int (optional)
- lot_id: int (optional)
@@ -68,6 +69,7 @@ class FusionRepairIntakeService(models.AbstractModel):
)
equipment = payload.get('equipment_items') or [{}]
+ quote_only = bool(payload.get('quote_only'))
repairs = self.env['repair.order']
for item in equipment:
repair = self._create_single_repair(
@@ -76,6 +78,7 @@ class FusionRepairIntakeService(models.AbstractModel):
session_ref=session_ref,
source=source,
item=item,
+ quote_only=quote_only,
)
repairs |= repair
@@ -103,7 +106,8 @@ class FusionRepairIntakeService(models.AbstractModel):
# CORE CREATION
# ------------------------------------------------------------------
@api.model
- def _create_single_repair(self, partner_id, intake_user, session_ref, source, item):
+ def _create_single_repair(self, partner_id, intake_user, session_ref,
+ source, item, quote_only=False):
Repair = self.env['repair.order']
product_id = item.get('product_id')
@@ -139,7 +143,7 @@ class FusionRepairIntakeService(models.AbstractModel):
self._create_answers(repair, item.get('answers') or [])
# Service catalogue auto-match.
- self._match_service_catalog(repair, item)
+ self._match_service_catalog(repair, item, quote_only=quote_only)
# Check our own repair-warranty (30/90 day re-do free).
self._check_repair_warranty(repair)
@@ -162,11 +166,17 @@ class FusionRepairIntakeService(models.AbstractModel):
# Optional dispatch draft task (urgent / safety).
# Skip if the catalogue match already auto-created one.
+ # Skip entirely if intake is quote-only (C6).
if (
- repair.x_fc_urgency in ('urgent', 'safety')
+ not quote_only
+ and repair.x_fc_urgency in ('urgent', 'safety')
and not repair.x_fc_technician_task_ids
):
self._create_dispatch_task(repair)
+ elif quote_only:
+ repair.message_post(body=Markup(_(
+ 'Created in Quote Only mode - no technician dispatched.'
+ )))
# Emails (client + office).
self._send_intake_emails(repair)
@@ -202,7 +212,7 @@ class FusionRepairIntakeService(models.AbstractModel):
# SERVICE CATALOGUE MATCH
# ------------------------------------------------------------------
@api.model
- def _match_service_catalog(self, repair, item):
+ def _match_service_catalog(self, repair, item, quote_only=False):
category = repair.x_fc_repair_category_id
if not category:
return
@@ -222,7 +232,12 @@ class FusionRepairIntakeService(models.AbstractModel):
'x_fc_estimated_cost': catalog.estimated_cost,
})
# Auto-create dispatch task if catalogue says so (in addition to urgency rule).
- if catalog.auto_schedule and repair.x_fc_technician_task_count == 0:
+ # Quote-only intakes skip this too.
+ if (
+ catalog.auto_schedule
+ and repair.x_fc_technician_task_count == 0
+ and not quote_only
+ ):
self._create_dispatch_task(repair)
# ------------------------------------------------------------------
@@ -410,29 +425,61 @@ class FusionRepairIntakeService(models.AbstractModel):
'description': repair.internal_notes or repair.name,
}
# technician_id is required AND constrained to x_fc_is_field_staff.
- # Use the intake user if they qualify, otherwise the lowest-id active
- # field-staff user as a placeholder for the dispatcher to reassign.
- if repair.user_id and repair.user_id.x_fc_is_field_staff:
- vals['technician_id'] = repair.user_id.id
- else:
- fallback = self.env['res.users'].sudo().search([
- ('x_fc_is_field_staff', '=', True),
- ('active', '=', True),
- ], order='id', limit=1)
- if not fallback:
- _logger.warning(
- 'No field-staff user available - skipping auto-dispatch '
- 'task for repair %s (mark a user as Field Staff under '
- 'Settings > Users).',
- repair.name,
- )
- return
- vals['technician_id'] = fallback.id
+ # D2: prefer a tech whose x_fc_repair_skills covers this repair's
+ # category. Falls back to ANY active field-staff user if no skilled
+ # tech exists, then to the lowest-id field-staff user as a placeholder.
+ tech_id = self._pick_dispatch_technician(repair)
+ if not tech_id:
+ _logger.warning(
+ 'No field-staff user available - skipping auto-dispatch '
+ 'task for repair %s (mark a user as Field Staff under '
+ 'Settings > Users).',
+ repair.name,
+ )
+ return
+ vals['technician_id'] = tech_id
Task.create(vals)
except Exception as e:
_logger.warning('Failed to auto-create dispatch task for repair %s: %s',
repair.name, e)
+ @api.model
+ def _pick_dispatch_technician(self, repair):
+ """D2: pick the best technician for the initial dispatch task.
+
+ Preference order:
+ 1. The intake user IF they are field staff AND have the skill
+ 2. Any active field-staff user with x_fc_repair_skills covering
+ the repair's product category
+ 3. Any active field-staff user (no skills filter)
+
+ Returns the chosen user id, or False if none found.
+ """
+ Users = self.env['res.users'].sudo()
+ category = repair.x_fc_repair_category_id
+
+ # Try intake user first if they qualify.
+ if repair.user_id and repair.user_id.x_fc_is_field_staff:
+ if not category or category in repair.user_id.x_fc_repair_skills:
+ return repair.user_id.id
+
+ # Skills-filtered candidates.
+ if category:
+ skilled = Users.search([
+ ('x_fc_is_field_staff', '=', True),
+ ('active', '=', True),
+ ('x_fc_repair_skills', 'in', category.id),
+ ], order='id', limit=1)
+ if skilled:
+ return skilled.id
+
+ # Any active field staff.
+ fallback = Users.search([
+ ('x_fc_is_field_staff', '=', True),
+ ('active', '=', True),
+ ], order='id', limit=1)
+ return fallback.id if fallback else False
+
# ------------------------------------------------------------------
# EMAILS
# ------------------------------------------------------------------
diff --git a/fusion_repairs/models/technician_task.py b/fusion_repairs/models/technician_task.py
index 40a59996..d4241a9a 100644
--- a/fusion_repairs/models/technician_task.py
+++ b/fusion_repairs/models/technician_task.py
@@ -4,7 +4,7 @@
from markupsafe import Markup
-from odoo import fields, models
+from odoo import _, fields, models
class FusionTechnicianTaskRepairs(models.Model):
@@ -69,3 +69,34 @@ class FusionTechnicianTaskRepairs(models.Model):
'view_mode': 'form',
'res_id': self.x_fc_repair_order_id.id,
}
+
+ # ------------------------------------------------------------------
+ # T1: Open in Maps - returns an act_url action that opens the device's
+ # default maps app (Apple Maps on iOS, Google Maps on Android, browser
+ # otherwise). Address is built from the task's address fields with the
+ # partner address as a fallback.
+ # ------------------------------------------------------------------
+ def action_open_in_maps(self):
+ self.ensure_one()
+ from urllib.parse import quote_plus
+ parts = []
+ for f in ('address_street', 'address_city', 'address_zip'):
+ v = getattr(self, f, None)
+ if v:
+ parts.append(str(v))
+ if not parts and self.partner_id:
+ for f in ('street', 'street2', 'city', 'state_id', 'zip'):
+ v = getattr(self.partner_id, f, None)
+ if v:
+ parts.append(v.name if hasattr(v, 'name') else str(v))
+ if not parts:
+ from odoo.exceptions import UserError
+ raise UserError(_('No address on this task or its client.'))
+ query = quote_plus(', '.join(parts))
+ # https://www.google.com/maps?q=ADDR works on every platform and
+ # automatically deep-links into the native app where supported.
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': f'https://www.google.com/maps?q={query}',
+ 'target': 'new',
+ }
diff --git a/fusion_repairs/views/technician_task_views.xml b/fusion_repairs/views/technician_task_views.xml
new file mode 100644
index 00000000..ec1c8fe4
--- /dev/null
+++ b/fusion_repairs/views/technician_task_views.xml
@@ -0,0 +1,32 @@
+
+