diff --git a/fusion_repairs/__manifest__.py b/fusion_repairs/__manifest__.py index 5c9be84d..681a0cfb 100644 --- a/fusion_repairs/__manifest__.py +++ b/fusion_repairs/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Fusion Repairs', - 'version': '19.0.1.1.0', + 'version': '19.0.1.1.1', 'category': 'Inventory/Repairs', 'summary': 'Guided medical equipment repair intake, dispatch, maintenance, and self-service portal', 'description': """ diff --git a/fusion_repairs/models/intake_service.py b/fusion_repairs/models/intake_service.py index b1ab387c..00feedc4 100644 --- a/fusion_repairs/models/intake_service.py +++ b/fusion_repairs/models/intake_service.py @@ -122,6 +122,7 @@ class FusionRepairIntakeService(models.AbstractModel): 'x_fc_third_party_equipment': bool(item.get('third_party')), 'x_fc_urgency': item.get('urgency') or 'normal', 'x_fc_issue_category': item.get('issue_category') or False, + 'x_fc_is_quote_only': bool(quote_only), 'internal_notes': self._wrap_internal_notes(item), } if product_id: @@ -468,7 +469,7 @@ class FusionRepairIntakeService(models.AbstractModel): skilled = Users.search([ ('x_fc_is_field_staff', '=', True), ('active', '=', True), - ('x_fc_repair_skills', 'in', category.id), + ('x_fc_repair_skills', 'in', [category.id]), ], order='id', limit=1) if skilled: return skilled.id diff --git a/fusion_repairs/models/repair_order.py b/fusion_repairs/models/repair_order.py index d609b67b..4ceecae3 100644 --- a/fusion_repairs/models/repair_order.py +++ b/fusion_repairs/models/repair_order.py @@ -88,6 +88,15 @@ class RepairOrder(models.Model): help='Auto-matched catalogue entry that pre-fills estimated cost and duration.', ) + # C6: quote-only flag (set when intake submitted in quote-only mode). + x_fc_is_quote_only = fields.Boolean( + string='Quote Only', + tracking=True, + index=True, + help='True when the intake was submitted in "Quote Only" mode - the ' + 'office has not yet authorised dispatching a technician.', + ) + # Maintenance contract back-link (Phase 3) x_fc_maintenance_contract_id = fields.Many2one( 'fusion.repair.maintenance.contract', diff --git a/fusion_repairs/models/technician_task.py b/fusion_repairs/models/technician_task.py index d4241a9a..59f97947 100644 --- a/fusion_repairs/models/technician_task.py +++ b/fusion_repairs/models/technician_task.py @@ -2,9 +2,12 @@ # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) +from urllib.parse import quote_plus + from markupsafe import Markup from odoo import _, fields, models +from odoo.exceptions import UserError class FusionTechnicianTaskRepairs(models.Model): @@ -78,25 +81,23 @@ class FusionTechnicianTaskRepairs(models.Model): # ------------------------------------------------------------------ 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 + # Prefer fusion_tasks.address_display because in real data address_street + # often contains the full Google-Places-formatted address; concatenating + # the other address_* fields would duplicate city/zip. + addr = (getattr(self, 'address_display', '') or '').strip() + if not addr and self.partner_id: + p = self.partner_id + parts = [ + p.street, p.street2, p.city, + p.state_id.name if p.state_id else False, + p.zip, + p.country_id.name if p.country_id else False, + ] + addr = ', '.join(str(x) for x in parts if x) + if not addr: 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}', + 'url': f'https://www.google.com/maps?q={quote_plus(addr)}', 'target': 'new', } diff --git a/fusion_repairs/views/repair_order_views.xml b/fusion_repairs/views/repair_order_views.xml index 79a2eb75..36b4bfb1 100644 --- a/fusion_repairs/views/repair_order_views.xml +++ b/fusion_repairs/views/repair_order_views.xml @@ -59,6 +59,7 @@ decoration-warning="x_fc_urgency == 'urgent'" decoration-danger="x_fc_urgency == 'safety'"/> + @@ -233,6 +234,8 @@ domain="[('x_fc_urgency', '=', 'urgent')]"/> + diff --git a/fusion_repairs/views/technician_task_views.xml b/fusion_repairs/views/technician_task_views.xml index ec1c8fe4..f1a1e234 100644 --- a/fusion_repairs/views/technician_task_views.xml +++ b/fusion_repairs/views/technician_task_views.xml @@ -14,7 +14,8 @@ type="object" string="Open in Maps" class="btn-secondary" - icon="fa-map-marker"/> + icon="fa-map-marker" + invisible="not address_display and not partner_id"/> 5 open calls. + dup_domain = [ ('partner_id', '=', w.partner_id.id), ('state', 'not in', ('done', 'cancel')), ('create_date', '>=', cutoff), - ], order='create_date desc', limit=5) - w.duplicate_repair_ids = dupes - w.duplicate_count = len(dupes) + ('company_id', 'in', company_ids), + ] + w.duplicate_repair_ids = Repair.search( + dup_domain, order='create_date desc', limit=5, + ) + w.duplicate_count = Repair.search_count(dup_domain) - open_invoices = Move.search([ - ('partner_id', 'child_of', w.partner_id.id), + # commercial_partner_id is the canonical "billed-to root" - covers + # child contacts AND walks up from a child if the caller IS a child. + commercial = w.partner_id.commercial_partner_id or w.partner_id + inv_domain = [ + ('commercial_partner_id', '=', commercial.id), ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('payment_state', 'in', ('not_paid', 'partial')), - ]) - balance = sum(open_invoices.mapped('amount_residual')) - w.outstanding_balance = balance - w.outstanding_invoice_count = len(open_invoices) - w.show_outstanding_warning = balance >= threshold + ('company_id', 'in', company_ids), + ] + # _read_group pushes the SUM to Postgres - O(1) load vs O(N) records. + rows = Move._read_group( + inv_domain, aggregates=['amount_residual:sum', '__count'], + ) + balance, invoice_count = rows[0] if rows else (0.0, 0) + w.currency_id = default_currency + w.outstanding_balance = balance or 0.0 + w.outstanding_invoice_count = invoice_count or 0 + w.show_outstanding_warning = (balance or 0.0) >= threshold # ------------------------------------------------------------------ # SUBMIT diff --git a/fusion_repairs/wizard/repair_intake_wizard_views.xml b/fusion_repairs/wizard/repair_intake_wizard_views.xml index 6da93f7b..0591bdd1 100644 --- a/fusion_repairs/wizard/repair_intake_wizard_views.xml +++ b/fusion_repairs/wizard/repair_intake_wizard_views.xml @@ -23,7 +23,8 @@ Open repair already exists for this client - ( in last 14 days). + ( + in last days). Consider adding a note to the existing repair instead. +