feat: separate fusion field service and LTC into standalone modules, update core modules
- fusion_claims: separated field service logic, updated controllers/views - fusion_tasks: updated task views and map integration - fusion_authorizer_portal: added page 11 signing, schedule booking, migrations - fusion_shipping: new standalone shipping module (Canada Post, FedEx, DHL, Purolator) - fusion_ltc_management: new standalone LTC management module
This commit is contained in:
12
fusion_ltc_management/models/__init__.py
Normal file
12
fusion_ltc_management/models/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import ltc_facility
|
||||
from . import ltc_repair
|
||||
from . import ltc_cleanup
|
||||
from . import ltc_form_submission
|
||||
from . import res_partner
|
||||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
from . import technician_task
|
||||
167
fusion_ltc_management/models/ltc_cleanup.py
Normal file
167
fusion_ltc_management/models/ltc_cleanup.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class FusionLTCCleanup(models.Model):
|
||||
_name = 'fusion.ltc.cleanup'
|
||||
_description = 'LTC Cleanup Schedule'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'scheduled_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: _('New'),
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.ltc.facility',
|
||||
string='LTC Facility',
|
||||
required=True,
|
||||
tracking=True,
|
||||
index=True,
|
||||
)
|
||||
scheduled_date = fields.Date(
|
||||
string='Scheduled Date',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
completed_date = fields.Date(
|
||||
string='Completed Date',
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection([
|
||||
('scheduled', 'Scheduled'),
|
||||
('in_progress', 'In Progress'),
|
||||
('completed', 'Completed'),
|
||||
('cancelled', 'Cancelled'),
|
||||
('rescheduled', 'Rescheduled'),
|
||||
], string='Status', default='scheduled', required=True, tracking=True)
|
||||
|
||||
technician_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Technician',
|
||||
tracking=True,
|
||||
)
|
||||
task_id = fields.Many2one(
|
||||
'fusion.technician.task',
|
||||
string='Field Service Task',
|
||||
)
|
||||
notes = fields.Text(string='Notes')
|
||||
items_cleaned = fields.Integer(string='Items Cleaned')
|
||||
photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'ltc_cleanup_photo_rel',
|
||||
'cleanup_id',
|
||||
'attachment_id',
|
||||
string='Photos',
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('fusion.ltc.cleanup') or _('New')
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_start(self):
|
||||
self.write({'state': 'in_progress'})
|
||||
|
||||
def action_complete(self):
|
||||
self.write({
|
||||
'state': 'completed',
|
||||
'completed_date': fields.Date.context_today(self),
|
||||
})
|
||||
for record in self:
|
||||
record._schedule_next_cleanup()
|
||||
record.message_post(
|
||||
body=_("Cleanup completed. Items cleaned: %s", record.items_cleaned or 0),
|
||||
message_type='comment',
|
||||
)
|
||||
|
||||
def action_cancel(self):
|
||||
self.write({'state': 'cancelled'})
|
||||
|
||||
def action_reschedule(self):
|
||||
self.write({'state': 'rescheduled'})
|
||||
|
||||
def action_reset(self):
|
||||
self.write({'state': 'scheduled'})
|
||||
|
||||
def _schedule_next_cleanup(self):
|
||||
facility = self.facility_id
|
||||
interval = facility._get_cleanup_interval_days()
|
||||
next_date = (self.completed_date or fields.Date.context_today(self)) + timedelta(days=interval)
|
||||
facility.next_cleanup_date = next_date
|
||||
|
||||
next_cleanup = self.env['fusion.ltc.cleanup'].create({
|
||||
'facility_id': facility.id,
|
||||
'scheduled_date': next_date,
|
||||
'technician_id': self.technician_id.id if self.technician_id else False,
|
||||
})
|
||||
|
||||
self.activity_schedule(
|
||||
'mail.mail_activity_data_todo',
|
||||
date_deadline=next_date - timedelta(days=7),
|
||||
summary=_('Upcoming cleanup at %s', facility.name),
|
||||
note=_('Next cleanup is scheduled for %s at %s.', next_date, facility.name),
|
||||
)
|
||||
return next_cleanup
|
||||
|
||||
def action_create_task(self):
|
||||
self.ensure_one()
|
||||
if self.task_id:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.technician.task',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.task_id.id,
|
||||
}
|
||||
task = self.env['fusion.technician.task'].create({
|
||||
'task_type': 'ltc_visit',
|
||||
'facility_id': self.facility_id.id,
|
||||
'scheduled_date': self.scheduled_date,
|
||||
'technician_id': self.technician_id.id if self.technician_id else False,
|
||||
'description': _('Cleanup visit at %s', self.facility_id.name),
|
||||
})
|
||||
self.task_id = task.id
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.technician.task',
|
||||
'view_mode': 'form',
|
||||
'res_id': task.id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _cron_schedule_cleanups(self):
|
||||
today = fields.Date.context_today(self)
|
||||
week_ahead = today + timedelta(days=7)
|
||||
facilities = self.env['fusion.ltc.facility'].search([
|
||||
('active', '=', True),
|
||||
('cleanup_frequency', '!=', False),
|
||||
('next_cleanup_date', '<=', week_ahead),
|
||||
('next_cleanup_date', '>=', today),
|
||||
])
|
||||
for facility in facilities:
|
||||
existing = self.search([
|
||||
('facility_id', '=', facility.id),
|
||||
('scheduled_date', '=', facility.next_cleanup_date),
|
||||
('state', 'not in', ['cancelled', 'rescheduled']),
|
||||
], limit=1)
|
||||
if not existing:
|
||||
cleanup = self.create({
|
||||
'facility_id': facility.id,
|
||||
'scheduled_date': facility.next_cleanup_date,
|
||||
})
|
||||
cleanup.activity_schedule(
|
||||
'mail.mail_activity_data_todo',
|
||||
date_deadline=facility.next_cleanup_date - timedelta(days=3),
|
||||
summary=_('Cleanup scheduled at %s', facility.name),
|
||||
note=_('Cleanup is scheduled for %s.', facility.next_cleanup_date),
|
||||
)
|
||||
314
fusion_ltc_management/models/ltc_facility.py
Normal file
314
fusion_ltc_management/models/ltc_facility.py
Normal file
@@ -0,0 +1,314 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class FusionLTCFacility(models.Model):
|
||||
_name = 'fusion.ltc.facility'
|
||||
_description = 'LTC Facility'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Facility Name',
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
code = fields.Char(
|
||||
string='Code',
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: _('New'),
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
image_1920 = fields.Image(string='Image', max_width=1920, max_height=1920)
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Contact Record',
|
||||
help='The facility as a contact in the system',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# Address
|
||||
street = fields.Char(string='Street')
|
||||
street2 = fields.Char(string='Street2')
|
||||
city = fields.Char(string='City')
|
||||
state_id = fields.Many2one(
|
||||
'res.country.state',
|
||||
string='Province',
|
||||
domain="[('country_id', '=', country_id)]",
|
||||
)
|
||||
zip = fields.Char(string='Postal Code')
|
||||
country_id = fields.Many2one('res.country', string='Country')
|
||||
phone = fields.Char(string='Phone')
|
||||
email = fields.Char(string='Email')
|
||||
website = fields.Char(string='Website')
|
||||
|
||||
# Key contacts
|
||||
director_of_care_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Director of Care',
|
||||
tracking=True,
|
||||
)
|
||||
service_supervisor_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Service Supervisor',
|
||||
tracking=True,
|
||||
)
|
||||
physiotherapist_ids = fields.Many2many(
|
||||
'res.partner',
|
||||
'ltc_facility_physiotherapist_rel',
|
||||
'facility_id',
|
||||
'partner_id',
|
||||
string='Physiotherapists',
|
||||
help='Primary contacts for equipment recommendations and communication',
|
||||
)
|
||||
|
||||
# Structure
|
||||
number_of_floors = fields.Integer(string='Number of Floors')
|
||||
floor_ids = fields.One2many(
|
||||
'fusion.ltc.floor',
|
||||
'facility_id',
|
||||
string='Floors',
|
||||
)
|
||||
|
||||
# Contract
|
||||
contract_start_date = fields.Date(string='Contract Start Date', tracking=True)
|
||||
contract_end_date = fields.Date(string='Contract End Date', tracking=True)
|
||||
contract_file = fields.Binary(
|
||||
string='Contract Document',
|
||||
attachment=True,
|
||||
)
|
||||
contract_file_filename = fields.Char(string='Contract Filename')
|
||||
contract_notes = fields.Text(string='Contract Notes')
|
||||
|
||||
# Cleanup scheduling
|
||||
cleanup_frequency = fields.Selection([
|
||||
('quarterly', 'Quarterly (Every 3 Months)'),
|
||||
('semi_annual', 'Semi-Annual (Every 6 Months)'),
|
||||
('annual', 'Annual (Yearly)'),
|
||||
('custom', 'Custom Interval'),
|
||||
], string='Cleanup Frequency', default='quarterly')
|
||||
cleanup_interval_days = fields.Integer(
|
||||
string='Custom Interval (Days)',
|
||||
help='Number of days between cleanups when using custom interval',
|
||||
)
|
||||
next_cleanup_date = fields.Date(
|
||||
string='Next Cleanup Date',
|
||||
compute='_compute_next_cleanup_date',
|
||||
store=True,
|
||||
readonly=False,
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# Related records
|
||||
repair_ids = fields.One2many('fusion.ltc.repair', 'facility_id', string='Repairs')
|
||||
cleanup_ids = fields.One2many('fusion.ltc.cleanup', 'facility_id', string='Cleanups')
|
||||
|
||||
# Computed counts
|
||||
repair_count = fields.Integer(compute='_compute_repair_count', string='Total Repairs')
|
||||
active_repair_count = fields.Integer(compute='_compute_repair_count', string='Active Repairs')
|
||||
cleanup_count = fields.Integer(compute='_compute_cleanup_count', string='Cleanups')
|
||||
|
||||
notes = fields.Html(string='Notes')
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('code', _('New')) == _('New'):
|
||||
vals['code'] = self.env['ir.sequence'].next_by_code('fusion.ltc.facility') or _('New')
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.depends('contract_start_date', 'cleanup_frequency', 'cleanup_interval_days')
|
||||
def _compute_next_cleanup_date(self):
|
||||
today = fields.Date.context_today(self)
|
||||
for facility in self:
|
||||
start = facility.contract_start_date
|
||||
freq = facility.cleanup_frequency
|
||||
if not start or not freq:
|
||||
if not facility.next_cleanup_date:
|
||||
facility.next_cleanup_date = False
|
||||
continue
|
||||
|
||||
interval = facility._get_cleanup_interval_days()
|
||||
delta = relativedelta(days=interval)
|
||||
|
||||
candidate = start + delta
|
||||
while candidate < today:
|
||||
candidate += delta
|
||||
|
||||
facility.next_cleanup_date = candidate
|
||||
|
||||
def action_preview_contract(self):
|
||||
self.ensure_one()
|
||||
if not self.contract_file:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('No Document'),
|
||||
'message': _('No contract document has been uploaded yet.'),
|
||||
'type': 'warning',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
|
||||
attachment = self.env['ir.attachment'].search([
|
||||
('res_model', '=', self._name),
|
||||
('res_id', '=', self.id),
|
||||
('res_field', '=', 'contract_file'),
|
||||
], limit=1)
|
||||
|
||||
if not attachment:
|
||||
attachment = self.env['ir.attachment'].search([
|
||||
('res_model', '=', self._name),
|
||||
('res_id', '=', self.id),
|
||||
('name', '=', self.contract_file_filename or 'contract_file'),
|
||||
], limit=1, order='id desc')
|
||||
|
||||
if attachment:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'fusion_claims.preview_document',
|
||||
'params': {
|
||||
'attachment_id': attachment.id,
|
||||
'title': _('Contract - %s', self.name),
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Error'),
|
||||
'message': _('Could not load contract document.'),
|
||||
'type': 'danger',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
|
||||
def _compute_repair_count(self):
|
||||
for facility in self:
|
||||
repairs = facility.repair_ids
|
||||
facility.repair_count = len(repairs)
|
||||
facility.active_repair_count = len(repairs.filtered(
|
||||
lambda r: r.stage_id and not r.stage_id.fold
|
||||
))
|
||||
|
||||
def _compute_cleanup_count(self):
|
||||
for facility in self:
|
||||
facility.cleanup_count = len(facility.cleanup_ids)
|
||||
|
||||
def action_view_repairs(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Repairs - %s', self.name),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.ltc.repair',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('facility_id', '=', self.id)],
|
||||
'context': {'default_facility_id': self.id},
|
||||
}
|
||||
|
||||
def action_view_cleanups(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Cleanups - %s', self.name),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.ltc.cleanup',
|
||||
'view_mode': 'list,kanban,form',
|
||||
'domain': [('facility_id', '=', self.id)],
|
||||
'context': {'default_facility_id': self.id},
|
||||
}
|
||||
|
||||
def _get_cleanup_interval_days(self):
|
||||
mapping = {
|
||||
'quarterly': 90,
|
||||
'semi_annual': 180,
|
||||
'annual': 365,
|
||||
}
|
||||
if self.cleanup_frequency == 'custom':
|
||||
return self.cleanup_interval_days or 90
|
||||
return mapping.get(self.cleanup_frequency, 90)
|
||||
|
||||
|
||||
class FusionLTCFloor(models.Model):
|
||||
_name = 'fusion.ltc.floor'
|
||||
_description = 'LTC Facility Floor'
|
||||
_order = 'sequence, name'
|
||||
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.ltc.facility',
|
||||
string='Facility',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
name = fields.Char(string='Floor Name', required=True)
|
||||
sequence = fields.Integer(string='Sequence', default=10)
|
||||
station_ids = fields.One2many(
|
||||
'fusion.ltc.station',
|
||||
'floor_id',
|
||||
string='Nursing Stations',
|
||||
)
|
||||
head_nurse_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Head Nurse',
|
||||
)
|
||||
physiotherapist_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Physiotherapist',
|
||||
help='Floor-level physiotherapist if different from facility level',
|
||||
)
|
||||
|
||||
|
||||
class FusionLTCStation(models.Model):
|
||||
_name = 'fusion.ltc.station'
|
||||
_description = 'LTC Nursing Station'
|
||||
_order = 'sequence, name'
|
||||
|
||||
floor_id = fields.Many2one(
|
||||
'fusion.ltc.floor',
|
||||
string='Floor',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
name = fields.Char(string='Station Name', required=True)
|
||||
sequence = fields.Integer(string='Sequence', default=10)
|
||||
head_nurse_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Head Nurse',
|
||||
)
|
||||
phone = fields.Char(string='Phone')
|
||||
|
||||
|
||||
class FusionLTCFamilyContact(models.Model):
|
||||
_name = 'fusion.ltc.family.contact'
|
||||
_description = 'LTC Resident Family Contact'
|
||||
_order = 'is_poa desc, name'
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Resident',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
name = fields.Char(string='Contact Name', required=True)
|
||||
relationship = fields.Selection([
|
||||
('spouse', 'Spouse'),
|
||||
('child', 'Child'),
|
||||
('sibling', 'Sibling'),
|
||||
('parent', 'Parent'),
|
||||
('guardian', 'Guardian'),
|
||||
('poa', 'Power of Attorney'),
|
||||
('other', 'Other'),
|
||||
], string='Relationship')
|
||||
phone = fields.Char(string='Phone')
|
||||
phone2 = fields.Char(string='Phone 2')
|
||||
email = fields.Char(string='Email')
|
||||
is_poa = fields.Boolean(string='Is POA', help='Is this person the Power of Attorney?')
|
||||
notes = fields.Char(string='Notes')
|
||||
68
fusion_ltc_management/models/ltc_form_submission.py
Normal file
68
fusion_ltc_management/models/ltc_form_submission.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class FusionLTCFormSubmission(models.Model):
|
||||
_name = 'fusion.ltc.form.submission'
|
||||
_description = 'LTC Form Submission'
|
||||
_order = 'submitted_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: _('New'),
|
||||
)
|
||||
form_type = fields.Selection([
|
||||
('repair', 'Repair Request'),
|
||||
], string='Form Type', default='repair', required=True, index=True)
|
||||
repair_id = fields.Many2one(
|
||||
'fusion.ltc.repair',
|
||||
string='Repair Request',
|
||||
ondelete='set null',
|
||||
index=True,
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.ltc.facility',
|
||||
string='Facility',
|
||||
index=True,
|
||||
)
|
||||
client_name = fields.Char(string='Client Name')
|
||||
room_number = fields.Char(string='Room Number')
|
||||
product_serial = fields.Char(string='Product Serial #')
|
||||
is_emergency = fields.Boolean(string='Emergency')
|
||||
submitted_date = fields.Datetime(
|
||||
string='Submitted Date',
|
||||
default=fields.Datetime.now,
|
||||
readonly=True,
|
||||
)
|
||||
ip_address = fields.Char(string='IP Address', readonly=True)
|
||||
status = fields.Selection([
|
||||
('submitted', 'Submitted'),
|
||||
('processed', 'Processed'),
|
||||
('rejected', 'Rejected'),
|
||||
], string='Status', default='submitted', tracking=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code(
|
||||
'fusion.ltc.form.submission'
|
||||
) or _('New')
|
||||
return super().create(vals_list)
|
||||
|
||||
def action_view_repair(self):
|
||||
self.ensure_one()
|
||||
if not self.repair_id:
|
||||
return
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.ltc.repair',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.repair_id.id,
|
||||
}
|
||||
376
fusion_ltc_management/models/ltc_repair.py
Normal file
376
fusion_ltc_management/models/ltc_repair.py
Normal file
@@ -0,0 +1,376 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class FusionLTCRepairStage(models.Model):
|
||||
_name = 'fusion.ltc.repair.stage'
|
||||
_description = 'LTC Repair Stage'
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char(string='Stage Name', required=True, translate=True)
|
||||
sequence = fields.Integer(string='Sequence', default=10)
|
||||
fold = fields.Boolean(
|
||||
string='Folded in Kanban',
|
||||
help='Folded stages are hidden by default in the kanban view',
|
||||
)
|
||||
color = fields.Char(
|
||||
string='Stage Color',
|
||||
help='CSS color class for stage badge (e.g. info, success, warning, danger)',
|
||||
default='secondary',
|
||||
)
|
||||
description = fields.Text(string='Description')
|
||||
|
||||
|
||||
class FusionLTCRepair(models.Model):
|
||||
_name = 'fusion.ltc.repair'
|
||||
_description = 'LTC Repair Request'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'issue_reported_date desc, id desc'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
copy=False,
|
||||
readonly=True,
|
||||
default=lambda self: _('New'),
|
||||
)
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.ltc.facility',
|
||||
string='LTC Facility',
|
||||
required=True,
|
||||
tracking=True,
|
||||
index=True,
|
||||
)
|
||||
client_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Client/Resident',
|
||||
tracking=True,
|
||||
help='Link to the resident contact record',
|
||||
)
|
||||
client_name = fields.Char(
|
||||
string='Client Name',
|
||||
help='Quick entry name when no contact record exists',
|
||||
)
|
||||
display_client_name = fields.Char(
|
||||
compute='_compute_display_client_name',
|
||||
string='Client',
|
||||
store=True,
|
||||
)
|
||||
room_number = fields.Char(string='Room Number')
|
||||
|
||||
stage_id = fields.Many2one(
|
||||
'fusion.ltc.repair.stage',
|
||||
string='Stage',
|
||||
tracking=True,
|
||||
group_expand='_read_group_stage_ids',
|
||||
default=lambda self: self._default_stage_id(),
|
||||
index=True,
|
||||
)
|
||||
kanban_state = fields.Selection([
|
||||
('normal', 'In Progress'),
|
||||
('done', 'Ready'),
|
||||
('blocked', 'Blocked'),
|
||||
], string='Kanban State', default='normal', tracking=True)
|
||||
color = fields.Integer(string='Color Index')
|
||||
|
||||
is_emergency = fields.Boolean(
|
||||
string='Emergency Repair',
|
||||
tracking=True,
|
||||
help='Emergency visits may be chargeable at an extra rate',
|
||||
)
|
||||
priority = fields.Selection([
|
||||
('0', 'Normal'),
|
||||
('1', 'High'),
|
||||
], string='Priority', default='0')
|
||||
|
||||
product_serial = fields.Char(string='Product Serial #')
|
||||
product_id = fields.Many2one(
|
||||
'product.product',
|
||||
string='Product',
|
||||
help='Link to product record if applicable',
|
||||
)
|
||||
issue_description = fields.Text(
|
||||
string='Issue Description',
|
||||
required=True,
|
||||
)
|
||||
issue_reported_date = fields.Date(
|
||||
string='Issue Reported Date',
|
||||
required=True,
|
||||
default=fields.Date.context_today,
|
||||
tracking=True,
|
||||
)
|
||||
issue_fixed_date = fields.Date(
|
||||
string='Issue Fixed Date',
|
||||
tracking=True,
|
||||
)
|
||||
resolution_description = fields.Text(string='Resolution Description')
|
||||
|
||||
assigned_technician_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Assigned Technician',
|
||||
tracking=True,
|
||||
)
|
||||
task_id = fields.Many2one(
|
||||
'fusion.technician.task',
|
||||
string='Field Service Task',
|
||||
tracking=True,
|
||||
)
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
tracking=True,
|
||||
help='Sale order created for this repair if applicable',
|
||||
)
|
||||
sale_order_name = fields.Char(
|
||||
related='sale_order_id.name',
|
||||
string='SO Reference',
|
||||
)
|
||||
|
||||
poa_name = fields.Char(string='Family/POA Name')
|
||||
poa_phone = fields.Char(string='Family/POA Phone')
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
related='company_id.currency_id',
|
||||
)
|
||||
repair_value = fields.Monetary(
|
||||
string='Repair Value',
|
||||
currency_field='currency_id',
|
||||
)
|
||||
|
||||
photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'ltc_repair_photo_rel',
|
||||
'repair_id',
|
||||
'attachment_id',
|
||||
string='Photos (Legacy)',
|
||||
)
|
||||
before_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'ltc_repair_before_photo_rel',
|
||||
'repair_id',
|
||||
'attachment_id',
|
||||
string='Before Photos',
|
||||
)
|
||||
after_photo_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'ltc_repair_after_photo_rel',
|
||||
'repair_id',
|
||||
'attachment_id',
|
||||
string='After Photos',
|
||||
)
|
||||
notes = fields.Text(string='Internal Notes')
|
||||
|
||||
source = fields.Selection([
|
||||
('portal_form', 'Portal Form'),
|
||||
('manual', 'Manual Entry'),
|
||||
('phone', 'Phone Call'),
|
||||
('migrated', 'Migrated'),
|
||||
], string='Source', default='manual', tracking=True)
|
||||
|
||||
stage_color = fields.Char(
|
||||
related='stage_id.color',
|
||||
string='Stage Color',
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('fusion.ltc.repair') or _('New')
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
record._post_creation_message()
|
||||
return records
|
||||
|
||||
def _post_creation_message(self):
|
||||
body = _(
|
||||
"Repair request submitted for <b>%(client)s</b> in Room <b>%(room)s</b>"
|
||||
" at <b>%(facility)s</b>.<br/>"
|
||||
"Issue: %(issue)s",
|
||||
client=self.display_client_name or 'N/A',
|
||||
room=self.room_number or 'N/A',
|
||||
facility=self.facility_id.name,
|
||||
issue=self.issue_description or '',
|
||||
)
|
||||
self.message_post(body=body, message_type='comment')
|
||||
|
||||
def _default_stage_id(self):
|
||||
return self.env['fusion.ltc.repair.stage'].search([], order='sequence', limit=1).id
|
||||
|
||||
@api.model
|
||||
def _read_group_stage_ids(self, stages, domain):
|
||||
return self.env['fusion.ltc.repair.stage'].search([], order='sequence')
|
||||
|
||||
@api.depends('client_id', 'client_name')
|
||||
def _compute_display_client_name(self):
|
||||
for repair in self:
|
||||
if repair.client_id:
|
||||
repair.display_client_name = repair.client_id.name
|
||||
else:
|
||||
repair.display_client_name = repair.client_name or ''
|
||||
|
||||
@api.onchange('client_id')
|
||||
def _onchange_client_id(self):
|
||||
if self.client_id:
|
||||
self.client_name = self.client_id.name
|
||||
if hasattr(self.client_id, 'x_fc_ltc_room_number') and self.client_id.x_fc_ltc_room_number:
|
||||
self.room_number = self.client_id.x_fc_ltc_room_number
|
||||
if hasattr(self.client_id, 'x_fc_ltc_facility_id') and self.client_id.x_fc_ltc_facility_id:
|
||||
self.facility_id = self.client_id.x_fc_ltc_facility_id
|
||||
|
||||
def action_view_sale_order(self):
|
||||
self.ensure_one()
|
||||
if not self.sale_order_id:
|
||||
return
|
||||
return {
|
||||
'name': self.sale_order_id.name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'sale.order',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.sale_order_id.id,
|
||||
}
|
||||
|
||||
def action_view_task(self):
|
||||
self.ensure_one()
|
||||
if not self.task_id:
|
||||
return
|
||||
return {
|
||||
'name': self.task_id.name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.technician.task',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.task_id.id,
|
||||
}
|
||||
|
||||
def action_create_sale_order(self):
|
||||
self.ensure_one()
|
||||
if self.sale_order_id:
|
||||
raise UserError(_('A sale order already exists for this repair.'))
|
||||
|
||||
if not self.client_id and self.client_name:
|
||||
return {
|
||||
'name': _('Link Contact'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fusion.ltc.repair.create.so.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_repair_id': self.id,
|
||||
'default_client_name': self.client_name,
|
||||
},
|
||||
}
|
||||
|
||||
if not self.client_id:
|
||||
raise UserError(_('Please set a client before creating a sale order.'))
|
||||
|
||||
return self._create_linked_sale_order()
|
||||
|
||||
def _create_linked_sale_order(self):
|
||||
self.ensure_one()
|
||||
SaleOrder = self.env['sale.order']
|
||||
OrderLine = self.env['sale.order.line']
|
||||
|
||||
so_vals = {
|
||||
'partner_id': self.client_id.id,
|
||||
'x_fc_ltc_repair_id': self.id,
|
||||
}
|
||||
sale_order = SaleOrder.create(so_vals)
|
||||
|
||||
seq = 10
|
||||
OrderLine.create({
|
||||
'order_id': sale_order.id,
|
||||
'display_type': 'line_section',
|
||||
'name': 'PRODUCTS & REPAIRS',
|
||||
'sequence': seq,
|
||||
})
|
||||
seq += 10
|
||||
|
||||
repair_tmpl = self.env.ref(
|
||||
'fusion_ltc_management.product_ltc_repair_service', raise_if_not_found=False
|
||||
)
|
||||
repair_product = (
|
||||
repair_tmpl.product_variant_id if repair_tmpl else False
|
||||
)
|
||||
line_vals = {
|
||||
'order_id': sale_order.id,
|
||||
'sequence': seq,
|
||||
'name': 'Repairs at LTC Home - %s' % (self.facility_id.name or ''),
|
||||
}
|
||||
if repair_product:
|
||||
line_vals['product_id'] = repair_product.id
|
||||
else:
|
||||
line_vals['display_type'] = 'line_note'
|
||||
OrderLine.create(line_vals)
|
||||
seq += 10
|
||||
|
||||
OrderLine.create({
|
||||
'order_id': sale_order.id,
|
||||
'display_type': 'line_section',
|
||||
'name': 'REPORTED ISSUES',
|
||||
'sequence': seq,
|
||||
})
|
||||
seq += 10
|
||||
|
||||
if self.issue_description:
|
||||
OrderLine.create({
|
||||
'order_id': sale_order.id,
|
||||
'display_type': 'line_note',
|
||||
'name': self.issue_description,
|
||||
'sequence': seq,
|
||||
})
|
||||
seq += 10
|
||||
|
||||
if self.issue_reported_date:
|
||||
OrderLine.create({
|
||||
'order_id': sale_order.id,
|
||||
'display_type': 'line_note',
|
||||
'name': 'Reported Date: %s' % self.issue_reported_date,
|
||||
'sequence': seq,
|
||||
})
|
||||
seq += 10
|
||||
|
||||
OrderLine.create({
|
||||
'order_id': sale_order.id,
|
||||
'display_type': 'line_section',
|
||||
'name': 'PROPOSED RESOLUTION',
|
||||
'sequence': seq,
|
||||
})
|
||||
seq += 10
|
||||
|
||||
if self.resolution_description:
|
||||
OrderLine.create({
|
||||
'order_id': sale_order.id,
|
||||
'display_type': 'line_note',
|
||||
'name': self.resolution_description,
|
||||
'sequence': seq,
|
||||
})
|
||||
seq += 10
|
||||
|
||||
if self.product_serial:
|
||||
OrderLine.create({
|
||||
'order_id': sale_order.id,
|
||||
'display_type': 'line_note',
|
||||
'name': 'Serial Number: %s' % self.product_serial,
|
||||
'sequence': seq,
|
||||
})
|
||||
|
||||
self.sale_order_id = sale_order.id
|
||||
|
||||
return {
|
||||
'name': sale_order.name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'sale.order',
|
||||
'view_mode': 'form',
|
||||
'res_id': sale_order.id,
|
||||
}
|
||||
29
fusion_ltc_management/models/res_config_settings.py
Normal file
29
fusion_ltc_management/models/res_config_settings.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# =========================================================================
|
||||
# LTC PORTAL FORMS
|
||||
# =========================================================================
|
||||
|
||||
fc_ltc_form_password = fields.Char(
|
||||
string='LTC Form Access Password',
|
||||
config_parameter='fusion_ltc_management.ltc_form_password',
|
||||
help='Minimum 4 characters. Share with facility staff to access the repair form.',
|
||||
)
|
||||
|
||||
def set_values(self):
|
||||
super().set_values()
|
||||
# Validate LTC form password length
|
||||
form_pw = self.fc_ltc_form_password or ''
|
||||
if form_pw and len(form_pw.strip()) < 4:
|
||||
raise ValidationError(
|
||||
'LTC Form Access Password must be at least 4 characters.'
|
||||
)
|
||||
28
fusion_ltc_management/models/res_partner.py
Normal file
28
fusion_ltc_management/models/res_partner.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
# ==========================================================================
|
||||
# LTC FIELDS
|
||||
# ==========================================================================
|
||||
x_fc_ltc_facility_id = fields.Many2one(
|
||||
'fusion.ltc.facility',
|
||||
string='LTC Home',
|
||||
tracking=True,
|
||||
help='Long-Term Care Home this resident belongs to',
|
||||
)
|
||||
x_fc_ltc_room_number = fields.Char(
|
||||
string='Room Number',
|
||||
tracking=True,
|
||||
)
|
||||
x_fc_ltc_family_contact_ids = fields.One2many(
|
||||
'fusion.ltc.family.contact',
|
||||
'partner_id',
|
||||
string='Family Contacts',
|
||||
)
|
||||
30
fusion_ltc_management/models/sale_order.py
Normal file
30
fusion_ltc_management/models/sale_order.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
# ==========================================================================
|
||||
# LTC REPAIR LINK
|
||||
# ==========================================================================
|
||||
x_fc_ltc_repair_id = fields.Many2one(
|
||||
'fusion.ltc.repair',
|
||||
string='LTC Repair',
|
||||
tracking=True,
|
||||
ondelete='set null',
|
||||
index=True,
|
||||
)
|
||||
x_fc_is_ltc_repair_sale = fields.Boolean(
|
||||
compute='_compute_is_ltc_repair_sale',
|
||||
store=True,
|
||||
string='Is LTC Repair Sale',
|
||||
)
|
||||
|
||||
@api.depends('x_fc_ltc_repair_id')
|
||||
def _compute_is_ltc_repair_sale(self):
|
||||
for order in self:
|
||||
order.x_fc_is_ltc_repair_sale = bool(order.x_fc_ltc_repair_id)
|
||||
42
fusion_ltc_management/models/technician_task.py
Normal file
42
fusion_ltc_management/models/technician_task.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
"""
|
||||
Fusion Technician Task - LTC Extension
|
||||
Adds LTC facility field and onchange behavior
|
||||
to the base fusion.technician.task model.
|
||||
"""
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class FusionTechnicianTaskLTC(models.Model):
|
||||
_inherit = 'fusion.technician.task'
|
||||
|
||||
facility_id = fields.Many2one(
|
||||
'fusion.ltc.facility',
|
||||
string='LTC Facility',
|
||||
tracking=True,
|
||||
help='LTC Home for this visit',
|
||||
)
|
||||
|
||||
@api.onchange('facility_id')
|
||||
def _onchange_facility_id(self):
|
||||
"""Auto-fill address from the LTC facility."""
|
||||
if self.facility_id and self.task_type == 'ltc_visit':
|
||||
fac = self.facility_id
|
||||
self.address_street = fac.street or ''
|
||||
self.address_street2 = fac.street2 or ''
|
||||
self.address_city = fac.city or ''
|
||||
self.address_state_id = fac.state_id.id if fac.state_id else False
|
||||
self.address_zip = fac.zip or ''
|
||||
self.description = self.description or _(
|
||||
'LTC Visit at %s', fac.name
|
||||
)
|
||||
|
||||
@api.onchange('task_type')
|
||||
def _onchange_task_type_ltc(self):
|
||||
if self.task_type == 'ltc_visit':
|
||||
self.sale_order_id = False
|
||||
self.purchase_order_id = False
|
||||
Reference in New Issue
Block a user