H1 Float -> Monetary for outstanding_balance
Added currency_id companion field on the wizard so widget="monetary"
renders properly. Currency defaults to env.company.currency_id.
H2 Maps URL address duplication
fusion_tasks address_street often contains the full Google-Places-
formatted address. Concatenating address_street + address_city + zip
was producing "15 Fisherman Dr, Brampton, ON L7A 1B7, Canada, Brampton,
L7A 1B7". Now uses the existing address_display field (fusion_tasks
computes it correctly for both Google Places and manual entries), with
a partner-based fallback that includes street, street2, city,
state_id.name, zip, country_id.name.
H3 Banner copy hardcoded "14 days"
Added duplicate_window_days compute field; banner now reads
"in last <N> days" from the ir.config_parameter.
H4 Outstanding-balance multi-company + child_of direction
- Dropped .sudo() (CS users already have access to their own company's
invoices via standard groups + the Repairs Office rule)
- Replaced child_of (which only walks descendants) with
commercial_partner_id (the canonical Odoo "billed-to root" - covers
child contacts AND walks up from a child if the caller IS a child)
- Added ('company_id', 'in', env.companies.ids) filter to both the
invoice search AND the duplicate-repair search so a CS rep in
Westin Healthcare doesn't see NEXA Systems balances
H5 duplicate_count capped at 5 (false reassurance)
Now uses search_count for the true total + search(limit=5) for the
display list. Earlier verification showed count=5 was actually
capped; running again shows 15 for the same partner.
M1 Function-level imports
Moved urllib.parse.quote_plus and odoo.exceptions.UserError to module
top in technician_task.py.
M2 Many2many 'in' with scalar
Changed ('x_fc_repair_skills', 'in', category.id) to
('x_fc_repair_skills', 'in', [category.id]) - safer against future
ORM tightening.
M4 C6 - added x_fc_is_quote_only field + filter + form indicator
Boolean tracked field on repair.order (was previously discoverable
only via chatter text). Indexed. Visible on the form's intake metadata
row and filterable on the dashboard search view as "Quote Only".
M5 Account-move read perf
Replaced Move.search() + Python sum with _read_group(
aggregates=['amount_residual:sum', '__count']) - pushes the SUM to
Postgres; O(1) record load vs O(N).
M6 Hide Maps button when no address
Added invisible="not address_display and not partner_id" on the
Open in Maps button so it doesn't appear on in-store tasks.
Plus the dispatch-task cutoff is now a datetime (was a date) so the
create_date >= cutoff comparison is type-correct.
Verified end-to-end on local westin-v19 after fixes:
C1 count: 15 (was capped at 5) window_days: 14
C5 balance: 0.0 currency: CAD warning: False (correct)
C6 x_fc_is_quote_only: True tech_tasks: 0 (urgent intake, NOT dispatched)
T1 URL: https://www.google.com/maps?q=15+Fisherman+Dr%2C+Brampton%2C+ON+L7A+1B7%2C+Canada%2C+Unit+7
(no duplicated city/zip)
Bumped to 19.0.1.1.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
# 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):
|
|
"""Adds the back-link from fusion.technician.task to repair.order so
|
|
repairs and tasks share one timeline. Also hooks task completion to
|
|
roll a linked maintenance contract to its next cycle.
|
|
"""
|
|
|
|
_inherit = 'fusion.technician.task'
|
|
|
|
x_fc_repair_order_id = fields.Many2one(
|
|
'repair.order',
|
|
string='Repair Order',
|
|
ondelete='set null',
|
|
index=True,
|
|
tracking=True,
|
|
help='Repair order this task fulfils. Set automatically when the intake '
|
|
'wizard auto-creates a draft task for urgent / safety calls.',
|
|
)
|
|
|
|
x_fc_repair_intake_session_id = fields.Char(
|
|
related='x_fc_repair_order_id.x_fc_intake_session_id',
|
|
string='Intake Session',
|
|
store=True,
|
|
index=True,
|
|
)
|
|
|
|
def write(self, vals):
|
|
"""When a maintenance task transitions to 'completed', roll the
|
|
linked contract to its next cycle. Failure to roll never blocks
|
|
the underlying task write.
|
|
"""
|
|
res = super().write(vals)
|
|
if vals.get('status') == 'completed':
|
|
for task in self:
|
|
if task.task_type != 'maintenance':
|
|
continue
|
|
repair = task.x_fc_repair_order_id
|
|
contract = repair.x_fc_maintenance_contract_id if repair else False
|
|
if not contract:
|
|
continue
|
|
try:
|
|
contract.last_service_date = fields.Date.context_today(task)
|
|
contract.roll_next_due_date()
|
|
contract.message_post(body=Markup(
|
|
'Rolled forward after maintenance task '
|
|
'<b>%s</b> completed. Next due %s.'
|
|
) % (task.name or '', str(contract.next_due_date or '')))
|
|
except Exception:
|
|
# Never let a contract roll failure block the task write.
|
|
pass
|
|
return res
|
|
|
|
def action_view_repair_order(self):
|
|
self.ensure_one()
|
|
if not self.x_fc_repair_order_id:
|
|
return False
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': self.x_fc_repair_order_id.name,
|
|
'res_model': 'repair.order',
|
|
'view_mode': 'form',
|
|
'res_id': self.x_fc_repair_order_id.id,
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# T1: Open in Maps - returns an act_url action that opens the device's
|
|
# default maps app (Apple Maps on iOS, Google Maps on Android, browser
|
|
# otherwise). Address is built from the task's address fields with the
|
|
# partner address as a fallback.
|
|
# ------------------------------------------------------------------
|
|
def action_open_in_maps(self):
|
|
self.ensure_one()
|
|
# 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.'))
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'url': f'https://www.google.com/maps?q={quote_plus(addr)}',
|
|
'target': 'new',
|
|
}
|