changes
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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