# -*- coding: utf-8 -*- # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) """Service callout rate card. When we dispatch a tech to a client's home (as opposed to in-store work), this rate card answers "what do we charge?" for any combination of: * tier (regular hours / after-hours / weekend / statutory holiday) * number of technicians dispatched * actual labour hours billable (after the 30 min the callout fee covers) * round-trip travel kilometres beyond the threshold The Bundle 8 emergency surcharge sits ON TOP of this when CS flags a repair as a rush (same-day squeeze, etc.). They are separate concepts: - callout rate = the BASELINE house-call price - emergency surcharge = added "drop everything" premium """ from odoo import _, api, fields, models class FusionRepairCalloutRate(models.Model): _name = 'fusion.repair.callout.rate' _description = 'Service Callout Rate Card' _order = 'effective_from desc, tier, id' name = fields.Char(compute='_compute_name', store=True) tier = fields.Selection( [ ('regular', 'Regular Business Hours'), ('after_hours', 'After Hours (weekday evening)'), ('weekend', 'Weekend'), ('holiday', 'Statutory Holiday'), ], string='Tier', required=True, default='regular', ) # ---- Base callout (covers first 30 minutes of labour) ---- base_callout_fee = fields.Monetary( string='Base Callout Fee (1 tech)', currency_field='currency_id', required=True, default=0.0, help='Charge for dispatching one technician to the client. INCLUDES ' 'the first 30 minutes for inspection / check / report. Repair ' 'labour above the 30 min is charged hourly at hourly_labor_rate.', ) second_tech_fee = fields.Monetary( string='Second Technician Fee', currency_field='currency_id', default=0.0, help='Added to the callout when a 2nd technician is dispatched alongside ' 'the first. Lower than a second base callout because they share ' 'travel.', ) additional_tech_fee = fields.Monetary( string='Each Additional Technician Fee', currency_field='currency_id', default=0.0, help='Applied to the 3rd, 4th... technician on the same callout. ' 'Defaults to second_tech_fee if left zero.', ) # ---- Hourly labour (after the included 30 min) ---- hourly_labor_rate = fields.Monetary( string='Hourly Labour Rate (per tech)', currency_field='currency_id', required=True, default=0.0, help='Per-technician hourly rate applied to billable labour above the ' '30 min the callout covers. Minimum bill is minimum_labor_hours ' 'even if the tech finished faster.', ) minimum_labor_hours = fields.Float( string='Minimum Billable Hours', default=1.0, help='Round-up floor for labour beyond the included 30 min. Set 1.0 ' 'so a 20-minute fix still bills 1.0 hours.', ) # ---- Travel ---- travel_distance_threshold_km = fields.Float( string='Free Travel Distance (km, one-way)', default=25.0, help='Travel under this distance is free. Beyond it, every additional ' 'kilometre is charged at travel_per_km_fee, BOTH WAYS (so the bill ' 'is per-km * (one_way_km - threshold) * 2).', ) travel_per_km_fee = fields.Monetary( string='Per-km Fee Over Threshold', currency_field='currency_id', default=0.0, help='Per technician, per kilometre, both ways.', ) # ---- Bookkeeping ---- effective_from = fields.Date( string='Effective From', default=fields.Date.context_today, required=True, help='Rate effective from this date. Newer rates supersede older ones ' 'for the same (tier, company).', ) currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.company.currency_id, ) company_id = fields.Many2one( 'res.company', default=lambda self: self.env.company, ) active = fields.Boolean(default=True) description = fields.Text( help='Optional notes shown to CS / dispatchers - e.g. "applies after 5 PM weekdays".', ) @api.depends('tier', 'base_callout_fee', 'effective_from') def _compute_name(self): for r in self: tier_label = dict(self._fields['tier'].selection).get(r.tier) or '?' r.name = ( f'{tier_label} - ${r.base_callout_fee:.0f} callout' f' (from {r.effective_from})' ) @api.model def get_for_tier(self, tier, on_date=None): """Return the active rate row for `tier` effective on `on_date` (default today). Returns empty recordset if none configured.""" on_date = on_date or fields.Date.context_today(self) return self.sudo().search([ ('tier', '=', tier), ('active', '=', True), ('effective_from', '<=', on_date), ('company_id', 'in', self.env.companies.ids), ], order='effective_from desc', limit=1)