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:
2026-03-11 16:19:52 +00:00
parent 1f79cdcaaf
commit 431052920e
274 changed files with 52782 additions and 7302 deletions

View 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

View 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),
)

View 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')

View 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,
}

View 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,
}

View 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.'
)

View 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',
)

View 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)

View 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