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:
@@ -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
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user