From 80d9a960e7414f818c3df4010f0a19bc03b20605 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Thu, 4 Jun 2026 00:38:27 -0400 Subject: [PATCH] feat(fusion_claims): add fusion.service.rate model + resolvers Co-Authored-By: Claude Opus 4.8 (1M context) --- fusion_claims/models/__init__.py | 3 +- fusion_claims/models/service_rate.py | 81 ++++++++++++++++++++++++ fusion_claims/tests/__init__.py | 1 + fusion_claims/tests/test_service_rate.py | 55 ++++++++++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 fusion_claims/models/service_rate.py create mode 100644 fusion_claims/tests/test_service_rate.py diff --git a/fusion_claims/models/__init__.py b/fusion_claims/models/__init__.py index 5026c7d0..9c86ce2c 100644 --- a/fusion_claims/models/__init__.py +++ b/fusion_claims/models/__init__.py @@ -26,4 +26,5 @@ from . import ai_agent_ext from . import dashboard from . import res_partner from . import technician_task -from . import page11_sign_request \ No newline at end of file +from . import page11_sign_request +from . import service_rate \ No newline at end of file diff --git a/fusion_claims/models/service_rate.py b/fusion_claims/models/service_rate.py new file mode 100644 index 00000000..f3c137b3 --- /dev/null +++ b/fusion_claims/models/service_rate.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models + + +class FusionServiceRate(models.Model): + _name = 'fusion.service.rate' + _description = 'Field Service Rate' + _order = 'sequence, rate_kind, category, timing' + + name = fields.Char(string='Name', required=True) + code = fields.Char( + string='Code', required=True, index=True, + help='Stable code used by the booking engine, e.g. callout_standard_normal, per_km.', + ) + rate_kind = fields.Selection([ + ('callout', 'Service Call-out'), + ('labour', 'Labour'), + ('travel', 'Travel / per-km'), + ('delivery', 'Delivery / Pickup'), + ('other', 'Other'), + ], string='Kind', required=True, default='callout') + category = fields.Selection([ + ('standard', 'Standard'), + ('lift', 'Lift & Elevating'), + ('na', 'N/A'), + ], string='Category', default='na') + timing = fields.Selection([ + ('normal', 'Normal'), + ('rush', 'Rush'), + ('afterhours', 'After-Hours'), + ('na', 'N/A'), + ], string='Timing', default='na') + in_shop = fields.Boolean(string='In-Shop') + product_id = fields.Many2one( + 'product.product', string='Invoice Product', required=True, ondelete='restrict', + help='Product used on the sale-order line (description, tax, income account).', + ) + price = fields.Monetary( + string='Rate', required=True, currency_field='currency_id', + help='Editable price used on the SO line and the on-screen estimate.', + ) + currency_id = fields.Many2one( + 'res.currency', string='Currency', + default=lambda self: self.env.company.currency_id, + ) + unit = fields.Selection([ + ('fixed', 'Flat'), + ('per_hour', 'Per hour'), + ('per_km', 'Per km'), + ], string='Unit', required=True, default='fixed') + adds_per_km = fields.Boolean( + string='Adds per-km travel', + help='Call-outs billed as $X + per-km \xd7 2-way (rush / after-hours).', + ) + included_labour_min = fields.Integer( + string='Included labour (min)', default=0, + help='Free labour minutes bundled into a service call (e.g. 30).', + ) + active = fields.Boolean(string='Active', default=True) + sequence = fields.Integer(string='Sequence', default=10) + + _unique_code = models.Constraint( + 'UNIQUE(code)', + 'A service-rate code must be unique.', + ) + + @api.model + def get_callout(self, category, timing, in_shop=False): + """Active call-out rate for category+timing. Empty recordset when in-shop.""" + if in_shop: + return self.browse() + return self.search([ + ('rate_kind', '=', 'callout'), + ('category', '=', category), + ('timing', '=', timing), + ], limit=1) + + @api.model + def get_rate(self, code): + """Active rate row by code (e.g. 'per_km', 'labour_onsite').""" + return self.search([('code', '=', code)], limit=1) diff --git a/fusion_claims/tests/__init__.py b/fusion_claims/tests/__init__.py index 561d21da..3d63968a 100644 --- a/fusion_claims/tests/__init__.py +++ b/fusion_claims/tests/__init__.py @@ -3,3 +3,4 @@ from . import test_signed_pages_gate from . import test_application_received_wizard from . import test_dashboard +from . import test_service_rate diff --git a/fusion_claims/tests/test_service_rate.py b/fusion_claims/tests/test_service_rate.py new file mode 100644 index 00000000..f9d217d0 --- /dev/null +++ b/fusion_claims/tests/test_service_rate.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class TestServiceRate(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Rate = cls.env['fusion.service.rate'] + cls.product = cls.env['product.product'].create({ + 'name': 'Test Service Product', 'type': 'service', + }) + + def _make(self, **kw): + vals = dict(name='Rate', code='c1', rate_kind='callout', category='standard', + timing='normal', product_id=self.product.id, price=95.0, unit='fixed') + vals.update(kw) + return self.Rate.create(vals) + + def test_get_callout_matches_category_and_timing(self): + r = self._make(code='callout_standard_normal', category='standard', timing='normal', price=95.0) + self._make(code='callout_lift_normal', category='lift', timing='normal', price=160.0) + self.assertEqual(self.Rate.get_callout('standard', 'normal'), r) + + def test_get_callout_in_shop_returns_empty(self): + self._make(code='callout_standard_normal_b') + self.assertFalse(self.Rate.get_callout('standard', 'normal', in_shop=True)) + + def test_get_rate_by_code(self): + r = self._make(code='per_km', rate_kind='travel', category='na', timing='na', unit='per_km', price=0.70) + self.assertEqual(self.Rate.get_rate('per_km'), r) + + def test_code_must_be_unique(self): + self._make(code='dup') + with self.assertRaises(Exception): + self._make(code='dup') + self.env.flush_all() + + def test_seeded_callouts_exist(self): + # standard normal $95, lift after-hours $205 are the canonical seeds + std = self.env.ref('fusion_claims.rate_callout_standard_normal') + self.assertEqual(std.price, 95.0) + self.assertEqual(std.rate_kind, 'callout') + self.assertTrue(std.product_id) + lift_ah = self.env.ref('fusion_claims.rate_callout_lift_afterhours') + self.assertEqual(lift_ah.price, 205.0) + self.assertTrue(lift_ah.adds_per_km) + + def test_seeded_per_km(self): + km = self.env['fusion.service.rate'].get_rate('per_km') + self.assertTrue(km) + self.assertEqual(km.unit, 'per_km') + self.assertEqual(km.price, 0.70)