feat(fusion_claims): action_book_from_wizard + jsonrpc booking routes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 01:00:53 -04:00
parent 245e551c68
commit 92e8a18fcb
5 changed files with 169 additions and 0 deletions

View File

@@ -447,6 +447,117 @@ class FusionTechnicianTaskClaims(models.Model):
}
return self.env['sale.order'].create(so_vals)
def _service_travel_origin(self):
"""Return (lat, lng) of the technician's day-start location, to be used
as the ORIGIN for the per-km travel calculation. NEVER returns the job's
own address (that would give origin == destination == 0 km).
Fallback chain (all read-only — no geocoding API calls here):
1. The technician's personal start address cached coords
(res.users.partner_id.x_fc_start_address_lat/_lng — populated when
the start address is saved, see fusion_tasks/models/res_partner.py).
2. The company HQ start address cached coords, keyed off the
``fusion_claims.technician_start_address`` ICP and cached by
fusion_tasks under ``fusion_tasks.hq_coords:<address>``.
3. (0.0, 0.0) — the correct graceful fallback. _calculate_travel_time
guards on a falsy origin and simply returns False (→ no per-km line).
Geocoding is deliberately NOT performed here: a freshly typed new-client
job address usually has no geocoded destination anyway, so distance is
expected to be 0 in v1. We only avoid passing a WRONG origin.
"""
self.ensure_one()
tech = self.technician_id
if tech:
partner = tech.partner_id
if partner and partner.x_fc_start_address_lat and partner.x_fc_start_address_lng:
return partner.x_fc_start_address_lat, partner.x_fc_start_address_lng
ICP = self.env['ir.config_parameter'].sudo()
hq_addr = (ICP.get_param('fusion_claims.technician_start_address', '') or '').strip()
if hq_addr:
cached = ICP.get_param('fusion_tasks.hq_coords:%s' % hq_addr, '')
if cached and ',' in cached:
try:
lat_s, lng_s = cached.split(',', 1)
return float(lat_s), float(lng_s)
except (ValueError, TypeError):
pass
return 0.0, 0.0
@api.model
def action_book_from_wizard(self, payload):
"""Single entry point for the OWL booking wizard:
resolve/create contact -> create task -> compute distance -> build SO -> link.
Returns {'task_id', 'order_id'}."""
Partner = self.env['res.partner']
cust = payload.get('customer') or {}
# 1. contact: new -> find-or-create (match email then phone); existing -> chosen partner
if payload.get('cust_mode') == 'new':
partner = False
email = (cust.get('email') or '').strip()
phone = (cust.get('phone') or '').strip()
if email:
partner = Partner.search([('email', '=ilike', email)], limit=1)
if not partner and phone:
partner = Partner.search([('phone', '=', phone)], limit=1)
if not partner:
partner = Partner.create({
'name': cust.get('name') or 'Walk-in',
'phone': phone or False, 'email': email or False,
'street': cust.get('street') or False, 'city': cust.get('city') or False,
})
else:
partner = Partner.browse(int(payload['partner_id'])) if payload.get('partner_id') else Partner
category = payload.get('category', 'standard')
timing = payload.get('timing', 'normal')
in_shop = bool(payload.get('in_shop'))
# technician_id is REQUIRED on a task
technician_id = payload.get('technician_id')
if not technician_id:
raise UserError(_("Please choose a technician for this service booking."))
technician_id = int(technician_id)
# 2. task
dur = float(payload.get('duration_hr') or 1.0)
t_start = float(payload.get('time_start') or 9.0)
task_vals = {
'task_type': 'repair',
'technician_id': technician_id,
'scheduled_date': payload.get('date'),
'time_start': t_start,
'time_end': t_start + dur,
'duration_hours': dur,
'is_in_store': in_shop,
'x_fc_service_call_type': '%s_%s' % (category, timing),
'description': payload.get('description') or payload.get('issue') or '',
}
if partner:
task_vals['partner_id'] = partner.id
task = self.create(task_vals)
# 3. per-km distance: only when the rate adds it AND we have a real origin + a
# geocoded job destination. Origin is the technician's start, never the job.
distance_km = 0.0
callout = self.env['fusion.service.rate'].get_callout(category, timing, in_shop=in_shop)
if callout and callout.adds_per_km and not in_shop and task.address_lat and task.address_lng:
origin_lat, origin_lng = task._service_travel_origin()
if origin_lat and origin_lng:
try:
task._calculate_travel_time(origin_lat, origin_lng) # sets travel_distance_km
distance_km = task.travel_distance_km or 0.0
except Exception:
distance_km = 0.0
# 4. draft repair SO + link back to the task
order = self._build_service_so(partner, category, timing, in_shop, distance_km) if partner else False
if order:
task.sale_order_id = order.id
return {'task_id': task.id, 'order_id': order.id if order else False}
# ------------------------------------------------------------------
# VIEW ACTIONS
# ------------------------------------------------------------------