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,5 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from . import portal_repair

View File

@@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
from odoo import http, _, fields
from odoo.http import request
import base64
import logging
_logger = logging.getLogger(__name__)
class LTCRepairPortal(http.Controller):
def _is_password_required(self):
password = request.env['ir.config_parameter'].sudo().get_param(
'fusion_ltc_management.ltc_form_password', ''
)
return bool(password and password.strip())
def _process_photos(self, file_list, repair):
attachment_ids = []
for photo in file_list:
if photo and photo.filename:
data = photo.read()
if data:
attachment = request.env['ir.attachment'].sudo().create({
'name': photo.filename,
'datas': base64.b64encode(data),
'res_model': 'fusion.ltc.repair',
'res_id': repair.id,
})
attachment_ids.append(attachment.id)
return attachment_ids
def _is_authenticated(self):
if not request.env.user._is_public():
return True
if not self._is_password_required():
return True
return request.session.get('ltc_form_authenticated', False)
@http.route('/repair-form', type='http', auth='public', website=True,
sitemap=False)
def repair_form(self, **kw):
if not self._is_authenticated():
return request.render(
'fusion_ltc_management.portal_ltc_repair_password',
{'error': kw.get('auth_error', False)}
)
facilities = request.env['fusion.ltc.facility'].sudo().search(
[('active', '=', True)], order='name'
)
is_technician = not request.env.user._is_public() and request.env.user.has_group(
'base.group_user'
)
return request.render(
'fusion_ltc_management.portal_ltc_repair_form',
{
'facilities': facilities,
'today': fields.Date.today(),
'is_technician': is_technician,
}
)
@http.route('/repair-form/auth', type='http', auth='public',
website=True, methods=['POST'], csrf=True)
def repair_form_auth(self, **kw):
stored_password = request.env['ir.config_parameter'].sudo().get_param(
'fusion_ltc_management.ltc_form_password', ''
).strip()
entered_password = (kw.get('password', '') or '').strip()
if stored_password and entered_password == stored_password:
request.session['ltc_form_authenticated'] = True
return request.redirect('/repair-form')
return request.render(
'fusion_ltc_management.portal_ltc_repair_password',
{'error': True}
)
@http.route('/repair-form/submit', type='http', auth='public',
website=True, methods=['POST'], csrf=True)
def repair_form_submit(self, **kw):
if not self._is_authenticated():
return request.redirect('/repair-form')
try:
facility_id = int(kw.get('facility_id', 0))
if not facility_id:
return request.redirect('/repair-form?error=facility')
vals = {
'facility_id': facility_id,
'client_name': kw.get('client_name', '').strip(),
'room_number': kw.get('room_number', '').strip(),
'product_serial': kw.get('product_serial', '').strip(),
'issue_description': kw.get('issue_description', '').strip(),
'issue_reported_date': kw.get('issue_reported_date') or fields.Date.today(),
'is_emergency': kw.get('is_emergency') == 'on',
'poa_name': kw.get('poa_name', '').strip() or False,
'poa_phone': kw.get('poa_phone', '').strip() or False,
'source': 'portal_form',
}
if not vals['client_name']:
return request.redirect('/repair-form?error=name')
if not vals['issue_description']:
return request.redirect('/repair-form?error=description')
before_files = request.httprequest.files.getlist('before_photos')
has_before = any(f and f.filename for f in before_files)
if not has_before:
return request.redirect('/repair-form?error=photos')
repair = request.env['fusion.ltc.repair'].sudo().create(vals)
before_ids = self._process_photos(before_files, repair)
if before_ids:
repair.sudo().write({
'before_photo_ids': [(6, 0, before_ids)],
})
after_files = request.httprequest.files.getlist('after_photos')
after_ids = self._process_photos(after_files, repair)
if after_ids:
repair.sudo().write({
'after_photo_ids': [(6, 0, after_ids)],
})
resolved = kw.get('resolved') == 'yes'
if resolved:
resolution = kw.get('resolution_description', '').strip()
if resolution:
repair.sudo().write({
'resolution_description': resolution,
'issue_fixed_date': fields.Date.today(),
})
repair.sudo().activity_schedule(
'mail.mail_activity_data_todo',
summary=_('New repair request from portal: %s', repair.display_client_name),
note=_(
'Repair request submitted via portal form for %s at %s (Room %s).',
repair.display_client_name,
repair.facility_id.name,
repair.room_number or 'N/A',
),
)
ip_address = request.httprequest.headers.get(
'X-Forwarded-For', request.httprequest.remote_addr
)
if ip_address and ',' in ip_address:
ip_address = ip_address.split(',')[0].strip()
try:
request.env['fusion.ltc.form.submission'].sudo().create({
'form_type': 'repair',
'repair_id': repair.id,
'facility_id': facility_id,
'client_name': vals['client_name'],
'room_number': vals['room_number'],
'product_serial': vals['product_serial'],
'is_emergency': vals['is_emergency'],
'ip_address': ip_address or '',
'status': 'processed',
})
except Exception:
_logger.warning('Failed to log form submission', exc_info=True)
return request.render(
'fusion_ltc_management.portal_ltc_repair_thank_you',
{'repair': repair}
)
except Exception:
_logger.exception('Error submitting LTC repair form')
return request.redirect('/repair-form?error=server')

View File

@@ -0,0 +1,322 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_ltc_repair_form"
name="LTC Repair Form">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<section class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="text-center mb-4">
<h1>LTC Repairs Request</h1>
<p class="lead text-muted">
Submit a repair request for medical equipment at your facility.
</p>
</div>
<t t-if="request.params.get('error') == 'facility'">
<div class="alert alert-danger">Please select a facility.</div>
</t>
<t t-if="request.params.get('error') == 'name'">
<div class="alert alert-danger">Patient name is required.</div>
</t>
<t t-if="request.params.get('error') == 'description'">
<div class="alert alert-danger">Issue description is required.</div>
</t>
<t t-if="request.params.get('error') == 'photos'">
<div class="alert alert-danger">At least one before photo is required.</div>
</t>
<t t-if="request.params.get('error') == 'server'">
<div class="alert alert-danger">
An error occurred. Please try again or contact us.
</div>
</t>
<form action="/repair-form/submit" method="POST"
enctype="multipart/form-data"
class="card shadow-sm">
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<div class="card-body p-4">
<div class="mb-4">
<div class="form-check">
<input type="checkbox" class="form-check-input"
id="is_emergency" name="is_emergency"/>
<label class="form-check-label fw-bold text-danger"
for="is_emergency">
Is this an Emergency Repair Request?
</label>
</div>
<small class="text-muted">
Emergency visits may be chargeable at an extra rate.
</small>
</div>
<hr/>
<div class="mb-3">
<label for="facility_id" class="form-label fw-bold">
Facility Location *
</label>
<select name="facility_id" id="facility_id"
class="form-select" required="required">
<option value="">-- Select Facility --</option>
<t t-foreach="facilities" t-as="fac">
<option t-att-value="fac.id">
<t t-esc="fac.name"/>
</option>
</t>
</select>
</div>
<div class="mb-3">
<label for="client_name" class="form-label fw-bold">
Patient Name *
</label>
<input type="text" name="client_name" id="client_name"
class="form-control" required="required"
placeholder="Enter patient name"/>
</div>
<div class="mb-3">
<label for="room_number" class="form-label fw-bold">
Room Number *
</label>
<input type="text" name="room_number" id="room_number"
class="form-control" required="required"
placeholder="e.g. 305"/>
</div>
<div class="mb-3">
<label for="issue_description" class="form-label fw-bold">
Describe the Issue *
</label>
<textarea name="issue_description" id="issue_description"
class="form-control" rows="4"
required="required"
placeholder="Please provide as much detail as possible about the issue."/>
</div>
<div class="mb-3">
<label for="issue_reported_date" class="form-label fw-bold">
Issue Reported Date *
</label>
<input type="date" name="issue_reported_date"
id="issue_reported_date"
class="form-control" required="required"
t-att-value="today"/>
</div>
<div class="mb-3">
<label for="product_serial" class="form-label fw-bold">
Product Serial # *
</label>
<input type="text" name="product_serial"
id="product_serial"
class="form-control" required="required"
placeholder="Serial number is required for repairs"/>
</div>
<div class="mb-3">
<label for="before_photos" class="form-label fw-bold">
Before Photos (Reported Condition) *
</label>
<input type="file" name="before_photos" id="before_photos"
class="form-control" multiple="multiple"
accept="image/*" required="required"/>
<small class="text-muted">
At least 1 photo required. Up to 4 photos (max 10MB each).
</small>
</div>
<hr/>
<h5>Family / POA Contact</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label for="poa_name" class="form-label">
Relative/POA Name
</label>
<input type="text" name="poa_name" id="poa_name"
class="form-control"
placeholder="Contact name"/>
</div>
<div class="col-md-6 mb-3">
<label for="poa_phone" class="form-label">
Relative/POA Phone
</label>
<input type="tel" name="poa_phone" id="poa_phone"
class="form-control"
placeholder="Phone number"/>
</div>
</div>
<t t-if="is_technician">
<hr/>
<div class="bg-light p-3 rounded mb-3">
<p class="fw-bold text-muted mb-2">
FOR TECHNICIAN USE ONLY
</p>
<div class="mb-3">
<label class="form-label">
Has the issue been resolved?
</label>
<div class="form-check form-check-inline">
<input type="radio" name="resolved" value="yes"
class="form-check-input" id="resolved_yes"/>
<label class="form-check-label"
for="resolved_yes">Yes</label>
</div>
<div class="form-check form-check-inline">
<input type="radio" name="resolved" value="no"
class="form-check-input" id="resolved_no"
checked="checked"/>
<label class="form-check-label"
for="resolved_no">No</label>
</div>
</div>
<div id="resolution_section"
style="display: none;">
<div class="mb-3">
<label for="resolution_description"
class="form-label">
Describe the Solution
</label>
<textarea name="resolution_description"
id="resolution_description"
class="form-control" rows="3"
placeholder="How was the issue resolved?"/>
</div>
<div class="mb-3">
<label for="after_photos" class="form-label fw-bold">
After Photos (Completed Repair)
</label>
<input type="file" name="after_photos" id="after_photos"
class="form-control" multiple="multiple"
accept="image/*"/>
<small class="text-muted">
Attach after repair is completed. Up to 4 photos (max 10MB each).
</small>
</div>
</div>
</div>
</t>
<div class="text-center mt-4">
<button type="submit" class="btn btn-primary btn-lg px-5">
Submit Repair Request
</button>
</div>
</div>
</form>
</div>
</div>
</section>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var section = document.getElementById('resolution_section');
if (!section) return;
var radios = document.querySelectorAll('input[name="resolved"]');
radios.forEach(function(r) {
r.addEventListener('change', function() {
section.style.display = this.value === 'yes' ? 'block' : 'none';
});
});
});
</script>
</t>
</template>
<template id="portal_ltc_repair_thank_you"
name="Repair Request Submitted">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<section class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6 text-center">
<div class="mb-4">
<i class="fa fa-check-circle text-success"
style="font-size: 4rem;"/>
</div>
<h2>Thank You!</h2>
<p class="lead text-muted">
Your repair request has been submitted successfully.
</p>
<div class="card mt-4">
<div class="card-body">
<p><strong>Reference:</strong>
<t t-esc="repair.name"/></p>
<p><strong>Facility:</strong>
<t t-esc="repair.facility_id.name"/></p>
<p><strong>Patient:</strong>
<t t-esc="repair.display_client_name"/></p>
<p><strong>Room:</strong>
<t t-esc="repair.room_number"/></p>
</div>
</div>
<a href="/repair-form" class="btn btn-outline-primary mt-4">
Submit Another Request
</a>
</div>
</div>
</section>
</div>
</t>
</template>
<template id="portal_ltc_repair_password"
name="LTC Repair Form - Password">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<section class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-sm">
<div class="card-body p-4 text-center">
<div class="mb-3">
<i class="fa fa-lock text-primary"
style="font-size: 3rem;"/>
</div>
<h3>LTC Repairs Request</h3>
<p class="text-muted">
Please enter the access password to continue.
</p>
<t t-if="error">
<div class="alert alert-danger">
Incorrect password. Please try again.
</div>
</t>
<form action="/repair-form/auth" method="POST"
class="mt-3">
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<div class="mb-3">
<input type="password" name="password"
class="form-control form-control-lg text-center"
placeholder="Enter password"
minlength="4" required="required"
autofocus="autofocus"/>
</div>
<button type="submit"
class="btn btn-primary btn-lg w-100">
Access Form
</button>
</form>
</div>
</div>
</div>
</div>
</section>
</div>
</t>
</template>
</odoo>