changes
This commit is contained in:
7
fusion_ltc_management/__init__.py
Normal file
7
fusion_ltc_management/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import controllers
|
||||
76
fusion_ltc_management/__manifest__.py
Normal file
76
fusion_ltc_management/__manifest__.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
{
|
||||
'name': 'Fusion LTC Management',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Sales',
|
||||
'summary': 'Long-Term Care Facility Management - Repairs, Cleanups, and Portal Forms',
|
||||
'description': """
|
||||
Fusion LTC Management
|
||||
=====================
|
||||
|
||||
Comprehensive Long-Term Care (LTC) facility management module.
|
||||
|
||||
Key Features:
|
||||
-------------
|
||||
• LTC Facility management with floors, nursing stations, and contacts
|
||||
• Equipment repair request tracking with Kanban workflow
|
||||
• Scheduled equipment cleanup management with auto-scheduling
|
||||
• Public portal form for facility staff to submit repair requests
|
||||
• Family/POA contact management for residents
|
||||
• Professional PDF reports (Nursing Station Repair Log, Repair Summary)
|
||||
• LTC Repair Sale Order report template
|
||||
• Integration with field service tasks (fusion_tasks)
|
||||
• Integration with sale orders for repair billing
|
||||
• Automatic cleanup scheduling via cron jobs
|
||||
|
||||
Copyright © 2024-2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'price': 0.00,
|
||||
'currency': 'CAD',
|
||||
'depends': [
|
||||
'base',
|
||||
'sale',
|
||||
'sale_management',
|
||||
'sales_team',
|
||||
'mail',
|
||||
'website',
|
||||
'fusion_claims',
|
||||
'fusion_tasks',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/ltc_data.xml',
|
||||
'wizard/ltc_repair_create_so_wizard_views.xml',
|
||||
'views/ltc_facility_views.xml',
|
||||
'views/ltc_repair_views.xml',
|
||||
'views/ltc_cleanup_views.xml',
|
||||
'views/ltc_form_submission_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/technician_task_views.xml',
|
||||
'views/menus.xml',
|
||||
'report/report_actions.xml',
|
||||
'report/report_ltc_nursing_station.xml',
|
||||
'report/sale_report_ltc_repair.xml',
|
||||
'data/ltc_report_data.xml',
|
||||
'controllers/portal_repair_form.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'fusion_ltc_management/static/src/js/google_address_facility_autocomplete.js',
|
||||
],
|
||||
},
|
||||
'images': ['static/description/icon.png'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
}
|
||||
BIN
fusion_ltc_management/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
fusion_ltc_management/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
5
fusion_ltc_management/controllers/__init__.py
Normal file
5
fusion_ltc_management/controllers/__init__.py
Normal 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
|
||||
Binary file not shown.
Binary file not shown.
182
fusion_ltc_management/controllers/portal_repair.py
Normal file
182
fusion_ltc_management/controllers/portal_repair.py
Normal 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')
|
||||
322
fusion_ltc_management/controllers/portal_repair_form.xml
Normal file
322
fusion_ltc_management/controllers/portal_repair_form.xml
Normal 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>
|
||||
112
fusion_ltc_management/data/ltc_data.xml
Normal file
112
fusion_ltc_management/data/ltc_data.xml
Normal file
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- SEQUENCES -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="seq_ltc_facility" model="ir.sequence">
|
||||
<field name="name">LTC Facility</field>
|
||||
<field name="code">fusion.ltc.facility</field>
|
||||
<field name="prefix">LTC-</field>
|
||||
<field name="padding">4</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_ltc_repair" model="ir.sequence">
|
||||
<field name="name">LTC Repair</field>
|
||||
<field name="code">fusion.ltc.repair</field>
|
||||
<field name="prefix">LTC-RPR-</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_ltc_cleanup" model="ir.sequence">
|
||||
<field name="name">LTC Cleanup</field>
|
||||
<field name="code">fusion.ltc.cleanup</field>
|
||||
<field name="prefix">LTC-CLN-</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="seq_ltc_form_submission" model="ir.sequence">
|
||||
<field name="name">LTC Form Submission</field>
|
||||
<field name="code">fusion.ltc.form.submission</field>
|
||||
<field name="prefix">LTC-SUB-</field>
|
||||
<field name="padding">5</field>
|
||||
<field name="number_next">1</field>
|
||||
<field name="number_increment">1</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- DEFAULT LTC REPAIR SERVICE PRODUCT -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="product_ltc_repair_service" model="product.template">
|
||||
<field name="name">REPAIRS AT LTC HOME</field>
|
||||
<field name="default_code">LTC-REPAIR</field>
|
||||
<field name="type">service</field>
|
||||
<field name="list_price">0.00</field>
|
||||
<field name="sale_ok" eval="True"/>
|
||||
<field name="purchase_ok" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- DEFAULT REPAIR STAGES -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="ltc_repair_stage_new" model="fusion.ltc.repair.stage">
|
||||
<field name="name">New</field>
|
||||
<field name="sequence">1</field>
|
||||
<field name="fold">False</field>
|
||||
<field name="description">Newly submitted repair requests awaiting review.</field>
|
||||
</record>
|
||||
|
||||
<record id="ltc_repair_stage_in_review" model="fusion.ltc.repair.stage">
|
||||
<field name="name">In Review</field>
|
||||
<field name="sequence">2</field>
|
||||
<field name="fold">False</field>
|
||||
<field name="description">Under review by office staff before assignment.</field>
|
||||
</record>
|
||||
|
||||
<record id="ltc_repair_stage_in_progress" model="fusion.ltc.repair.stage">
|
||||
<field name="name">In Progress</field>
|
||||
<field name="sequence">3</field>
|
||||
<field name="fold">False</field>
|
||||
<field name="description">Technician has been assigned and repair is underway.</field>
|
||||
</record>
|
||||
|
||||
<record id="ltc_repair_stage_completed" model="fusion.ltc.repair.stage">
|
||||
<field name="name">Completed</field>
|
||||
<field name="sequence">4</field>
|
||||
<field name="fold">True</field>
|
||||
<field name="description">Repair has been completed by the technician.</field>
|
||||
</record>
|
||||
|
||||
<record id="ltc_repair_stage_invoiced" model="fusion.ltc.repair.stage">
|
||||
<field name="name">Invoiced</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="fold">True</field>
|
||||
<field name="description">Sale order created and invoiced for this repair.</field>
|
||||
</record>
|
||||
|
||||
<record id="ltc_repair_stage_declined" model="fusion.ltc.repair.stage">
|
||||
<field name="name">Declined/No Response</field>
|
||||
<field name="sequence">6</field>
|
||||
<field name="fold">True</field>
|
||||
<field name="description">Repair was declined or no response was received.</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CRON: Cleanup scheduling -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="ir_cron_ltc_cleanup_schedule" model="ir.cron">
|
||||
<field name="name">LTC: Auto-Schedule Cleanups</field>
|
||||
<field name="model_id" ref="model_fusion_ltc_cleanup"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_schedule_cleanups()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
42
fusion_ltc_management/data/ltc_report_data.xml
Normal file
42
fusion_ltc_management/data/ltc_report_data.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Landscape paper format for nursing station report -->
|
||||
<record id="paperformat_ltc_nursing_station" model="report.paperformat">
|
||||
<field name="name">LTC Nursing Station (Landscape)</field>
|
||||
<field name="default">False</field>
|
||||
<field name="format">Letter</field>
|
||||
<field name="orientation">Landscape</field>
|
||||
<field name="margin_top">20</field>
|
||||
<field name="margin_bottom">15</field>
|
||||
<field name="margin_left">10</field>
|
||||
<field name="margin_right">10</field>
|
||||
<field name="header_line">False</field>
|
||||
<field name="header_spacing">10</field>
|
||||
<field name="dpi">90</field>
|
||||
</record>
|
||||
|
||||
<!-- Nursing Station Report action -->
|
||||
<record id="action_report_ltc_nursing_station" model="ir.actions.report">
|
||||
<field name="name">Nursing Station Repair Log</field>
|
||||
<field name="model">fusion.ltc.facility</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_ltc_management.report_ltc_nursing_station_document</field>
|
||||
<field name="report_file">fusion_ltc_management.report_ltc_nursing_station_document</field>
|
||||
<field name="binding_model_id" ref="model_fusion_ltc_facility"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="paperformat_ltc_nursing_station"/>
|
||||
</record>
|
||||
|
||||
<!-- Repair Summary Report action -->
|
||||
<record id="action_report_ltc_repairs_summary" model="ir.actions.report">
|
||||
<field name="name">Repair Summary</field>
|
||||
<field name="model">fusion.ltc.facility</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_ltc_management.report_ltc_repairs_summary_document</field>
|
||||
<field name="report_file">fusion_ltc_management.report_ltc_repairs_summary_document</field>
|
||||
<field name="binding_model_id" ref="model_fusion_ltc_facility"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
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
|
||||
32
fusion_ltc_management/report/report_actions.xml
Normal file
32
fusion_ltc_management/report/report_actions.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Landscape Paper Format for LTC Reports -->
|
||||
<record id="paperformat_ltc_landscape" model="report.paperformat">
|
||||
<field name="name">LTC Landscape</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="format">A4</field>
|
||||
<field name="orientation">Landscape</field>
|
||||
<field name="margin_top">20</field>
|
||||
<field name="margin_bottom">20</field>
|
||||
<field name="margin_left">7</field>
|
||||
<field name="margin_right">7</field>
|
||||
<field name="header_line">False</field>
|
||||
<field name="header_spacing">10</field>
|
||||
<field name="dpi">90</field>
|
||||
</record>
|
||||
|
||||
<!-- LTC Repair Order / Quotation Report (Landscape) -->
|
||||
<record id="action_report_saleorder_ltc_repair" model="ir.actions.report">
|
||||
<field name="name">LTC Repair Order / Quotation</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">fusion_ltc_management.report_saleorder_ltc_repair</field>
|
||||
<field name="report_file">fusion_ltc_management.report_saleorder_ltc_repair</field>
|
||||
<field name="print_report_name">'LTC Repair - %s - %s' % (object.name, object.partner_id.name)</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_type">report</field>
|
||||
<field name="paperformat_id" ref="paperformat_ltc_landscape"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
139
fusion_ltc_management/report/report_ltc_nursing_station.xml
Normal file
139
fusion_ltc_management/report/report_ltc_nursing_station.xml
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="report_ltc_nursing_station_document">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="facility">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page" style="font-size: 11px;">
|
||||
<h3 class="text-center mb-3">
|
||||
Equipment Repair Log - <t t-esc="facility.name"/>
|
||||
</h3>
|
||||
<p class="text-center text-muted mb-4">
|
||||
Generated: <t t-esc="context_timestamp(datetime.datetime.now()).strftime('%B %d, %Y')"/>
|
||||
</p>
|
||||
|
||||
<table class="table table-bordered" style="border: 1px solid #000;">
|
||||
<thead>
|
||||
<tr style="background-color: #f0f0f0; height: 20px;">
|
||||
<th style="border: 1px solid #000; width: 5%; text-align: center;">S/N</th>
|
||||
<th style="border: 1px solid #000; width: 18%;">Client Name</th>
|
||||
<th style="border: 1px solid #000; width: 8%; text-align: center;">Room #</th>
|
||||
<th style="border: 1px solid #000; width: 27%;">Issue Description</th>
|
||||
<th style="border: 1px solid #000; width: 12%; text-align: center;">Reported Date</th>
|
||||
<th style="border: 1px solid #000; width: 20%;">Resolution</th>
|
||||
<th style="border: 1px solid #000; width: 10%; text-align: center;">Fixed Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-set="counter" t-value="0"/>
|
||||
<t t-foreach="facility.repair_ids.sorted(key=lambda r: r.issue_reported_date or '', reverse=True)"
|
||||
t-as="repair">
|
||||
<t t-set="counter" t-value="counter + 1"/>
|
||||
<tr style="height: 20px;">
|
||||
<td style="border: 1px solid #000; text-align: center;">
|
||||
<t t-esc="counter"/>
|
||||
</td>
|
||||
<td style="border: 1px solid #000;">
|
||||
<t t-esc="repair.display_client_name"/>
|
||||
</td>
|
||||
<td style="border: 1px solid #000; text-align: center;">
|
||||
<t t-esc="repair.room_number"/>
|
||||
</td>
|
||||
<td style="border: 1px solid #000; font-size: 10px;">
|
||||
<t t-esc="repair.issue_description"/>
|
||||
</td>
|
||||
<td style="border: 1px solid #000; text-align: center;">
|
||||
<t t-if="repair.issue_reported_date">
|
||||
<t t-esc="repair.issue_reported_date" t-options='{"widget": "date"}'/>
|
||||
</t>
|
||||
</td>
|
||||
<td style="border: 1px solid #000; font-size: 10px;">
|
||||
<t t-esc="repair.resolution_description or ''"/>
|
||||
</td>
|
||||
<td style="border: 1px solid #000; text-align: center;">
|
||||
<t t-if="repair.issue_fixed_date">
|
||||
<t t-esc="repair.issue_fixed_date" t-options='{"widget": "date"}'/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_ltc_repairs_summary_document">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="facility">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h3 class="text-center mb-3">
|
||||
Repair Summary - <t t-esc="facility.name"/>
|
||||
</h3>
|
||||
<p class="text-center text-muted mb-4">
|
||||
Total Repairs: <t t-esc="len(facility.repair_ids)"/>
|
||||
</p>
|
||||
|
||||
<h5>Repairs by Stage</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stage</th>
|
||||
<th class="text-end">Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-set="stages" t-value="{}"/>
|
||||
<t t-foreach="facility.repair_ids" t-as="r">
|
||||
<t t-set="sname" t-value="r.stage_id.name or 'No Stage'"/>
|
||||
<t t-if="sname not in stages">
|
||||
<t t-set="_" t-value="stages.__setitem__(sname, 0)"/>
|
||||
</t>
|
||||
<t t-set="_" t-value="stages.__setitem__(sname, stages[sname] + 1)"/>
|
||||
</t>
|
||||
<t t-foreach="stages" t-as="stage_name">
|
||||
<tr>
|
||||
<td><t t-esc="stage_name"/></td>
|
||||
<td class="text-end"><t t-esc="stages[stage_name]"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h5 class="mt-4">Recent Repairs</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reference</th>
|
||||
<th>Client</th>
|
||||
<th>Room</th>
|
||||
<th>Reported</th>
|
||||
<th>Fixed</th>
|
||||
<th>Stage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="facility.repair_ids.sorted(key=lambda r: r.issue_reported_date or '', reverse=True)[:50]"
|
||||
t-as="repair">
|
||||
<tr>
|
||||
<td><t t-esc="repair.name"/></td>
|
||||
<td><t t-esc="repair.display_client_name"/></td>
|
||||
<td><t t-esc="repair.room_number"/></td>
|
||||
<td><t t-esc="repair.issue_reported_date" t-options='{"widget": "date"}'/></td>
|
||||
<td><t t-esc="repair.issue_fixed_date" t-options='{"widget": "date"}'/></td>
|
||||
<td><t t-esc="repair.stage_id.name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
279
fusion_ltc_management/report/sale_report_ltc_repair.xml
Normal file
279
fusion_ltc_management/report/sale_report_ltc_repair.xml
Normal file
@@ -0,0 +1,279 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2024-2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Landscape LTC Repair Order / Quotation Report Template
|
||||
-->
|
||||
<odoo>
|
||||
<template id="report_saleorder_ltc_repair">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="doc" t-value="doc.with_context(lang=doc.partner_id.lang)"/>
|
||||
<t t-set="repair" t-value="doc.x_fc_ltc_repair_id"/>
|
||||
|
||||
<style>
|
||||
.fc-ltc { font-family: Arial, sans-serif; font-size: 11pt; }
|
||||
.fc-ltc table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
|
||||
.fc-ltc table.bordered, .fc-ltc table.bordered th, .fc-ltc table.bordered td { border: 1px solid #000; }
|
||||
.fc-ltc th { background-color: #0066a1; color: white; padding: 8px 10px; font-weight: bold; font-size: 10pt; }
|
||||
.fc-ltc td { padding: 6px 8px; vertical-align: top; font-size: 10pt; }
|
||||
.fc-ltc .text-center { text-align: center; }
|
||||
.fc-ltc .text-end { text-align: right; }
|
||||
.fc-ltc .text-start { text-align: left; }
|
||||
.fc-ltc .repair-bg { background-color: #e8f5e9; }
|
||||
.fc-ltc .section-row { background-color: #f0f0f0; font-weight: bold; }
|
||||
.fc-ltc .note-row { font-style: italic; }
|
||||
.fc-ltc h2 { color: #0066a1; margin: 10px 0; font-size: 18pt; }
|
||||
.fc-ltc .info-table td { padding: 8px 12px; font-size: 11pt; }
|
||||
.fc-ltc .info-table th { background-color: #f5f5f5; color: #333; font-size: 10pt; padding: 6px 12px; }
|
||||
.fc-ltc .totals-table { border: 1px solid #000; }
|
||||
.fc-ltc .totals-table td { border: 1px solid #000; padding: 8px 12px; font-size: 11pt; }
|
||||
.fc-ltc .photo-grid { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
|
||||
.fc-ltc .photo-grid img { max-width: 220px; max-height: 180px; border: 1px solid #ccc; }
|
||||
.fc-ltc .photo-section { margin-top: 20px; page-break-inside: avoid; }
|
||||
.fc-ltc .photo-section h3 { color: #0066a1; font-size: 14pt; border-bottom: 2px solid #0066a1; padding-bottom: 4px; }
|
||||
</style>
|
||||
|
||||
<div class="fc-ltc">
|
||||
<div class="page">
|
||||
|
||||
<!-- Document Title -->
|
||||
<h2 style="text-align: left;">
|
||||
<span t-if="doc.state in ['draft','sent']">LTC Repair Quotation </span>
|
||||
<span t-else="">LTC Repair Order </span>
|
||||
<span t-field="doc.name"/>
|
||||
</h2>
|
||||
|
||||
<!-- Address Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 50%;">BILLING ADDRESS</th>
|
||||
<th style="width: 50%;">DELIVERY ADDRESS</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="height: 70px; font-size: 12pt;">
|
||||
<div t-field="doc.partner_invoice_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
<td style="height: 70px; font-size: 12pt;">
|
||||
<div t-field="doc.partner_shipping_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'address'], 'no_marker': True}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Order Info Table -->
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ORDER DATE</th>
|
||||
<th>SALES REP</th>
|
||||
<th>VALIDITY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.date_order" t-options="{'widget': 'date'}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.user_id"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="doc.validity_date"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- LTC Repair Info Table -->
|
||||
<t t-if="repair">
|
||||
<table class="bordered info-table">
|
||||
<thead>
|
||||
<tr class="repair-bg">
|
||||
<th style="background-color: #e8f5e9; color: #333;">REPAIR REF</th>
|
||||
<th style="background-color: #e8f5e9; color: #333;">TECHNICIAN</th>
|
||||
<th style="background-color: #e8f5e9; color: #333;">REPORTED DATE</th>
|
||||
<th style="background-color: #e8f5e9; color: #333;">SERIAL #</th>
|
||||
<th style="background-color: #e8f5e9; color: #333;">LTC LOCATION</th>
|
||||
<th style="background-color: #e8f5e9; color: #333;">ROOM #</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="repair-bg">
|
||||
<td class="text-center">
|
||||
<span t-esc="repair.name or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="repair.assigned_technician_id"
|
||||
t-esc="repair.assigned_technician_id.name"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-if="repair.issue_reported_date">
|
||||
<span t-field="repair.issue_reported_date"/>
|
||||
</t>
|
||||
<t t-else="">-</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="repair.product_serial or '-'"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="repair.facility_id"
|
||||
t-esc="repair.facility_id.name"/>
|
||||
<span t-else="">-</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="repair.room_number or '-'"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<!-- Order Lines Table -->
|
||||
<table class="bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-start" style="width: 40%;">DESCRIPTION</th>
|
||||
<th class="text-center" style="width: 10%;">QTY</th>
|
||||
<th class="text-center" style="width: 15%;">UNIT PRICE</th>
|
||||
<th class="text-center" style="width: 15%;">TAX</th>
|
||||
<th class="text-center" style="width: 20%;">TOTAL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.order_line" t-as="line">
|
||||
<!-- Section Header -->
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<tr class="section-row">
|
||||
<td colspan="5">
|
||||
<strong><span t-field="line.name"/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Note Line -->
|
||||
<t t-elif="line.display_type == 'line_note'">
|
||||
<tr class="note-row">
|
||||
<td colspan="5">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
<!-- Product Line -->
|
||||
<t t-elif="not line.display_type">
|
||||
<tr>
|
||||
<td>
|
||||
<t t-if="line.name">
|
||||
<t t-set="clean_name" t-value="line.name"/>
|
||||
<t t-if="'] ' in line.name">
|
||||
<t t-set="clean_name" t-value="line.name.split('] ', 1)[1]"/>
|
||||
</t>
|
||||
<t t-esc="clean_name"/>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-esc="int(line.product_uom_qty) if line.product_uom_qty == int(line.product_uom_qty) else line.product_uom_qty"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<t t-esc="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids]) or 'NO TAX SALE'"/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Payment Terms and Totals Row -->
|
||||
<div class="row" style="margin-top: 15px;">
|
||||
<div class="col-7">
|
||||
<t t-if="doc.payment_term_id.note">
|
||||
<strong>Payment Terms:</strong><br/>
|
||||
<span t-field="doc.payment_term_id.note"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-5" style="text-align: right;">
|
||||
<table class="totals-table" style="width: auto; margin-left: auto;">
|
||||
<tr>
|
||||
<td style="min-width: 200px;">Subtotal</td>
|
||||
<td class="text-end" style="min-width: 150px;">
|
||||
<span t-field="doc.amount_untaxed" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Taxes</td>
|
||||
<td class="text-end">
|
||||
<span t-field="doc.amount_tax" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Grand Total</strong></td>
|
||||
<td class="text-end"><strong>
|
||||
<span t-field="doc.amount_total" t-options='{"widget": "monetary", "display_currency": doc.currency_id}'/>
|
||||
</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms and Conditions -->
|
||||
<t t-if="doc.note">
|
||||
<div style="margin-top: 15px;">
|
||||
<strong>Terms and Conditions:</strong>
|
||||
<div t-field="doc.note"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Before Photos -->
|
||||
<t t-if="repair and repair.before_photo_ids">
|
||||
<div class="photo-section">
|
||||
<h3>Before Photos (Reported Condition)</h3>
|
||||
<div class="photo-grid">
|
||||
<t t-foreach="repair.before_photo_ids" t-as="photo">
|
||||
<img t-att-src="image_data_uri(photo.datas)"
|
||||
t-att-alt="photo.name"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- After Photos -->
|
||||
<t t-if="repair and repair.after_photo_ids">
|
||||
<div class="photo-section">
|
||||
<h3>After Photos (Completed Repair)</h3>
|
||||
<div class="photo-grid">
|
||||
<t t-foreach="repair.after_photo_ids" t-as="photo">
|
||||
<img t-att-src="image_data_uri(photo.datas)"
|
||||
t-att-alt="photo.name"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Signature -->
|
||||
<t t-if="doc.signature">
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<strong>Signature</strong><br/>
|
||||
<img t-att-src="image_data_uri(doc.signature)" style="max-height: 4cm; max-width: 8cm;"/><br/>
|
||||
<span t-field="doc.signed_by"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
19
fusion_ltc_management/security/ir.model.access.csv
Normal file
19
fusion_ltc_management/security/ir.model.access.csv
Normal file
@@ -0,0 +1,19 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_fusion_ltc_facility_user,fusion.ltc.facility.user,model_fusion_ltc_facility,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_ltc_facility_manager,fusion.ltc.facility.manager,model_fusion_ltc_facility,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_floor_user,fusion.ltc.floor.user,model_fusion_ltc_floor,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_ltc_floor_manager,fusion.ltc.floor.manager,model_fusion_ltc_floor,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_station_user,fusion.ltc.station.user,model_fusion_ltc_station,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_ltc_station_manager,fusion.ltc.station.manager,model_fusion_ltc_station,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_repair_user,fusion.ltc.repair.user,model_fusion_ltc_repair,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_ltc_repair_manager,fusion.ltc.repair.manager,model_fusion_ltc_repair,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_repair_stage_user,fusion.ltc.repair.stage.user,model_fusion_ltc_repair_stage,sales_team.group_sale_salesman,1,0,0,0
|
||||
access_fusion_ltc_repair_stage_manager,fusion.ltc.repair.stage.manager,model_fusion_ltc_repair_stage,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_cleanup_user,fusion.ltc.cleanup.user,model_fusion_ltc_cleanup,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_ltc_cleanup_manager,fusion.ltc.cleanup.manager,model_fusion_ltc_cleanup,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_family_contact_user,fusion.ltc.family.contact.user,model_fusion_ltc_family_contact,sales_team.group_sale_salesman,1,1,1,0
|
||||
access_fusion_ltc_family_contact_manager,fusion.ltc.family.contact.manager,model_fusion_ltc_family_contact,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_form_submission_user,fusion.ltc.form.submission.user,model_fusion_ltc_form_submission,sales_team.group_sale_salesman,1,1,0,0
|
||||
access_fusion_ltc_form_submission_manager,fusion.ltc.form.submission.manager,model_fusion_ltc_form_submission,sales_team.group_sale_manager,1,1,1,1
|
||||
access_fusion_ltc_repair_create_so_wizard_user,fusion.ltc.repair.create.so.wizard.user,model_fusion_ltc_repair_create_so_wizard,sales_team.group_sale_salesman,1,1,1,1
|
||||
access_fusion_ltc_repair_create_so_wizard_manager,fusion.ltc.repair.create.so.wizard.manager,model_fusion_ltc_repair_create_so_wizard,sales_team.group_sale_manager,1,1,1,1
|
||||
|
BIN
fusion_ltc_management/static/description/icon.png
Normal file
BIN
fusion_ltc_management/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,578 @@
|
||||
/** @odoo-module **/
|
||||
// Fusion LTC Management - Google Address Autocomplete for LTC Facilities
|
||||
// Copyright 2024-2026 Nexa Systems Inc.
|
||||
// License OPL-1
|
||||
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { onMounted, onWillUnmount } from "@odoo/owl";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
// Module-scoped state
|
||||
let googleMapsLoaded = false;
|
||||
let googleMapsLoading = false;
|
||||
let googleMapsApiKey = null;
|
||||
let globalOrm = null;
|
||||
const autocompleteInstances = new Map();
|
||||
|
||||
/**
|
||||
* Load Google Maps API dynamically
|
||||
*/
|
||||
async function loadGoogleMapsApi(apiKey) {
|
||||
if (googleMapsLoaded) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (googleMapsLoading) {
|
||||
return new Promise((resolve) => {
|
||||
const checkLoaded = setInterval(() => {
|
||||
if (googleMapsLoaded) {
|
||||
clearInterval(checkLoaded);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
googleMapsLoading = true;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check if already loaded by another module (e.g. fusion_claims)
|
||||
if (window.google && window.google.maps && window.google.maps.places) {
|
||||
googleMapsLoaded = true;
|
||||
googleMapsLoading = false;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if script tag already exists
|
||||
const existingScript = document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]');
|
||||
if (existingScript) {
|
||||
const checkReady = setInterval(() => {
|
||||
if (window.google && window.google.maps && window.google.maps.places) {
|
||||
googleMapsLoaded = true;
|
||||
googleMapsLoading = false;
|
||||
clearInterval(checkReady);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
window.initGoogleMapsAutocompleteLTC = () => {
|
||||
googleMapsLoaded = true;
|
||||
googleMapsLoading = false;
|
||||
resolve();
|
||||
};
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=initGoogleMapsAutocompleteLTC`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onerror = () => {
|
||||
googleMapsLoading = false;
|
||||
reject(new Error('Failed to load Google Maps API'));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from Odoo config (uses same key as fusion_claims)
|
||||
*/
|
||||
async function getGoogleMapsApiKey(orm) {
|
||||
if (googleMapsApiKey) {
|
||||
return googleMapsApiKey;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await orm.call(
|
||||
'ir.config_parameter',
|
||||
'get_param',
|
||||
['fusion_claims.google_maps_api_key']
|
||||
);
|
||||
googleMapsApiKey = result || null;
|
||||
return googleMapsApiKey;
|
||||
} catch (error) {
|
||||
console.warn('[LTC GooglePlaces] Could not fetch Google Maps API key:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate Many2One field selection by finding the widget and triggering its update
|
||||
*/
|
||||
async function simulateMany2OneSelection(formEl, fieldName, valueId, displayName) {
|
||||
const fieldSelectors = [
|
||||
`[name="${fieldName}"]`,
|
||||
`.o_field_widget[name="${fieldName}"]`,
|
||||
`div[name="${fieldName}"]`,
|
||||
];
|
||||
|
||||
let fieldContainer = null;
|
||||
for (const selector of fieldSelectors) {
|
||||
fieldContainer = formEl.querySelector(selector);
|
||||
if (fieldContainer) break;
|
||||
}
|
||||
|
||||
if (!fieldContainer) return false;
|
||||
|
||||
// Try OWL component approach
|
||||
let owlComponent = null;
|
||||
let el = fieldContainer;
|
||||
while (el && !owlComponent) {
|
||||
if (el.__owl__) {
|
||||
owlComponent = el.__owl__;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
|
||||
if (owlComponent && owlComponent.component) {
|
||||
const component = owlComponent.component;
|
||||
|
||||
if (component.props && component.props.record) {
|
||||
try {
|
||||
await component.props.record.update({ [fieldName]: valueId });
|
||||
return true;
|
||||
} catch (_) { /* fallthrough */ }
|
||||
}
|
||||
|
||||
if (typeof component.updateValue === 'function') {
|
||||
try {
|
||||
await component.updateValue(valueId);
|
||||
return true;
|
||||
} catch (_) { /* fallthrough */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: manipulate input directly
|
||||
const inputEl = fieldContainer.querySelector('input:not([type="hidden"])');
|
||||
if (inputEl) {
|
||||
inputEl.focus();
|
||||
inputEl.value = '';
|
||||
inputEl.value = displayName;
|
||||
|
||||
inputEl.dispatchEvent(new InputEvent('input', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
data: displayName,
|
||||
inputType: 'insertText'
|
||||
}));
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
|
||||
const dropdownSelectors = [
|
||||
'.o-autocomplete--dropdown-menu .o-autocomplete--dropdown-item',
|
||||
'.o_m2o_dropdown_option',
|
||||
'.dropdown-menu .dropdown-item',
|
||||
'.o-autocomplete .dropdown-item',
|
||||
'ul.ui-autocomplete li',
|
||||
'.o_field_many2one_selection li',
|
||||
];
|
||||
|
||||
let found = false;
|
||||
for (const selector of dropdownSelectors) {
|
||||
const dropdownItems = document.querySelectorAll(selector);
|
||||
for (const item of dropdownItems) {
|
||||
const itemText = item.textContent.trim();
|
||||
if (itemText.includes(displayName) || displayName.includes(itemText)) {
|
||||
item.click();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
inputEl.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true
|
||||
}));
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
inputEl.dispatchEvent(new KeyboardEvent('keydown', {
|
||||
key: 'Tab', code: 'Tab', keyCode: 9, bubbles: true
|
||||
}));
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
inputEl.blur();
|
||||
return found;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup autocomplete for LTC Facility form.
|
||||
* Attaches establishment search on the name field and address search on street.
|
||||
*/
|
||||
async function setupFacilityAutocomplete(el, model, orm) {
|
||||
globalOrm = orm;
|
||||
|
||||
const apiKey = await getGoogleMapsApiKey(orm);
|
||||
if (!apiKey) return;
|
||||
try { await loadGoogleMapsApi(apiKey); } catch (e) { return; }
|
||||
|
||||
// --- Name field: establishment autocomplete ---
|
||||
const nameSelectors = [
|
||||
'.oe_title [name="name"] input',
|
||||
'div[name="name"] input',
|
||||
'.o_field_widget[name="name"] input',
|
||||
'[name="name"] input',
|
||||
];
|
||||
|
||||
let nameInput = null;
|
||||
for (const sel of nameSelectors) {
|
||||
nameInput = el.querySelector(sel);
|
||||
if (nameInput) break;
|
||||
}
|
||||
|
||||
if (nameInput && !autocompleteInstances.has('facility_name_' + (nameInput.id || 'default'))) {
|
||||
_attachFacilityNameAutocomplete(nameInput, el, model);
|
||||
}
|
||||
|
||||
// --- Street field: address autocomplete ---
|
||||
const streetSelectors = [
|
||||
'div[name="street"] input',
|
||||
'.o_field_widget[name="street"] input',
|
||||
'[name="street"] input',
|
||||
];
|
||||
|
||||
let streetInput = null;
|
||||
for (const sel of streetSelectors) {
|
||||
streetInput = el.querySelector(sel);
|
||||
if (streetInput) break;
|
||||
}
|
||||
|
||||
if (streetInput && !autocompleteInstances.has(streetInput)) {
|
||||
_attachFacilityAddressAutocomplete(streetInput, el, model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach establishment (business) autocomplete on facility name field.
|
||||
* Selecting a business fills name, address, phone, email, and website.
|
||||
*/
|
||||
function _attachFacilityNameAutocomplete(input, el, model) {
|
||||
if (!input || !window.google?.maps?.places) return;
|
||||
|
||||
const instanceKey = 'facility_name_' + (input.id || 'default');
|
||||
if (autocompleteInstances.has(instanceKey)) return;
|
||||
|
||||
const autocomplete = new google.maps.places.Autocomplete(input, {
|
||||
componentRestrictions: { country: 'ca' },
|
||||
types: ['establishment'],
|
||||
fields: [
|
||||
'place_id', 'name', 'address_components', 'formatted_address',
|
||||
'formatted_phone_number', 'international_phone_number', 'website',
|
||||
],
|
||||
});
|
||||
|
||||
autocomplete.addListener('place_changed', async () => {
|
||||
let place = autocomplete.getPlace();
|
||||
if (!place.name && !place.place_id) return;
|
||||
|
||||
if (place.place_id && !place.formatted_phone_number && !place.website) {
|
||||
try {
|
||||
const service = new google.maps.places.PlacesService(document.createElement('div'));
|
||||
const details = await new Promise((resolve, reject) => {
|
||||
service.getDetails(
|
||||
{
|
||||
placeId: place.place_id,
|
||||
fields: ['formatted_phone_number', 'international_phone_number', 'website'],
|
||||
},
|
||||
(result, status) => {
|
||||
if (status === google.maps.places.PlacesServiceStatus.OK) resolve(result);
|
||||
else reject(new Error(status));
|
||||
}
|
||||
);
|
||||
});
|
||||
if (details.formatted_phone_number) place.formatted_phone_number = details.formatted_phone_number;
|
||||
if (details.international_phone_number) place.international_phone_number = details.international_phone_number;
|
||||
if (details.website) place.website = details.website;
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
let streetNumber = '', streetName = '', unitNumber = '';
|
||||
let city = '', province = '', postalCode = '', countryCode = '';
|
||||
|
||||
if (place.address_components) {
|
||||
for (const c of place.address_components) {
|
||||
const t = c.types;
|
||||
if (t.includes('street_number')) streetNumber = c.long_name;
|
||||
else if (t.includes('route')) streetName = c.long_name;
|
||||
else if (t.includes('subpremise')) unitNumber = c.long_name;
|
||||
else if (t.includes('floor') && !unitNumber) unitNumber = 'Floor ' + c.long_name;
|
||||
else if (t.includes('locality')) city = c.long_name;
|
||||
else if (t.includes('sublocality_level_1') && !city) city = c.long_name;
|
||||
else if (t.includes('administrative_area_level_1')) province = c.short_name;
|
||||
else if (t.includes('postal_code')) postalCode = c.long_name;
|
||||
else if (t.includes('country')) countryCode = c.short_name;
|
||||
}
|
||||
}
|
||||
|
||||
const street = streetNumber ? `${streetNumber} ${streetName}` : streetName;
|
||||
const phone = place.formatted_phone_number || place.international_phone_number || '';
|
||||
|
||||
if (!model?.root) return;
|
||||
const record = model.root;
|
||||
|
||||
let countryId = null, stateId = null;
|
||||
if (globalOrm && countryCode) {
|
||||
try {
|
||||
const [countries, states] = await Promise.all([
|
||||
globalOrm.searchRead('res.country', [['code', '=', countryCode]], ['id'], { limit: 1 }),
|
||||
province
|
||||
? globalOrm.searchRead('res.country.state', [['code', '=', province], ['country_id.code', '=', countryCode]], ['id'], { limit: 1 })
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
if (countries.length) countryId = countries[0].id;
|
||||
if (states.length) stateId = states[0].id;
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
if (record.resId && globalOrm) {
|
||||
try {
|
||||
const firstWrite = {};
|
||||
if (place.name) firstWrite.name = place.name;
|
||||
if (street) firstWrite.street = street;
|
||||
if (unitNumber) firstWrite.street2 = unitNumber;
|
||||
if (city) firstWrite.city = city;
|
||||
if (postalCode) firstWrite.zip = postalCode;
|
||||
if (phone) firstWrite.phone = phone;
|
||||
if (place.website) firstWrite.website = place.website;
|
||||
if (countryId) firstWrite.country_id = countryId;
|
||||
|
||||
await globalOrm.write('fusion.ltc.facility', [record.resId], firstWrite);
|
||||
|
||||
if (stateId) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
await globalOrm.write('fusion.ltc.facility', [record.resId], { state_id: stateId });
|
||||
}
|
||||
|
||||
await record.load();
|
||||
} catch (err) {
|
||||
console.error('[LTC GooglePlaces] Name autocomplete ORM write failed:', err);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const textUpdate = {};
|
||||
if (place.name) textUpdate.name = place.name;
|
||||
if (street) textUpdate.street = street;
|
||||
if (unitNumber) textUpdate.street2 = unitNumber;
|
||||
if (city) textUpdate.city = city;
|
||||
if (postalCode) textUpdate.zip = postalCode;
|
||||
if (phone) textUpdate.phone = phone;
|
||||
if (place.website) textUpdate.website = place.website;
|
||||
|
||||
await record.update(textUpdate);
|
||||
|
||||
if (countryId && globalOrm) {
|
||||
const formEl = input.closest('.o_form_view') || input.closest('.o_content') || document.body;
|
||||
const [countryData, stateData] = await Promise.all([
|
||||
globalOrm.read('res.country', [countryId], ['display_name']),
|
||||
stateId ? globalOrm.read('res.country.state', [stateId], ['display_name']) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
await simulateMany2OneSelection(formEl, 'country_id', countryId, countryData[0]?.display_name || 'Canada');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
if (stateId && stateData.length) {
|
||||
await simulateMany2OneSelection(formEl, 'state_id', stateId, stateData[0]?.display_name || province);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LTC GooglePlaces] Name autocomplete update failed:', err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
autocompleteInstances.set(instanceKey, autocomplete);
|
||||
|
||||
input.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%232196F3\'%3E%3Cpath d=\'M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z\'/%3E%3C/svg%3E")';
|
||||
input.style.backgroundRepeat = 'no-repeat';
|
||||
input.style.backgroundPosition = 'right 8px center';
|
||||
input.style.backgroundSize = '20px';
|
||||
input.style.paddingRight = '35px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach address autocomplete on facility street field.
|
||||
* Fills street, street2, city, state, zip, and country.
|
||||
*/
|
||||
function _attachFacilityAddressAutocomplete(input, el, model) {
|
||||
if (!input || !window.google?.maps?.places) return;
|
||||
if (autocompleteInstances.has(input)) return;
|
||||
|
||||
const autocomplete = new google.maps.places.Autocomplete(input, {
|
||||
componentRestrictions: { country: 'ca' },
|
||||
types: ['address'],
|
||||
fields: ['address_components', 'formatted_address'],
|
||||
});
|
||||
|
||||
autocomplete.addListener('place_changed', async () => {
|
||||
const place = autocomplete.getPlace();
|
||||
if (!place.address_components) return;
|
||||
|
||||
let streetNumber = '', streetName = '', unitNumber = '';
|
||||
let city = '', province = '', postalCode = '', countryCode = '';
|
||||
|
||||
for (const c of place.address_components) {
|
||||
const t = c.types;
|
||||
if (t.includes('street_number')) streetNumber = c.long_name;
|
||||
else if (t.includes('route')) streetName = c.long_name;
|
||||
else if (t.includes('subpremise')) unitNumber = c.long_name;
|
||||
else if (t.includes('floor') && !unitNumber) unitNumber = 'Floor ' + c.long_name;
|
||||
else if (t.includes('locality')) city = c.long_name;
|
||||
else if (t.includes('sublocality_level_1') && !city) city = c.long_name;
|
||||
else if (t.includes('administrative_area_level_1')) province = c.short_name;
|
||||
else if (t.includes('postal_code')) postalCode = c.long_name;
|
||||
else if (t.includes('country')) countryCode = c.short_name;
|
||||
}
|
||||
|
||||
const street = streetNumber ? `${streetNumber} ${streetName}` : streetName;
|
||||
|
||||
if (!model?.root) return;
|
||||
const record = model.root;
|
||||
|
||||
let countryId = null, stateId = null;
|
||||
if (globalOrm && countryCode) {
|
||||
try {
|
||||
const [countries, states] = await Promise.all([
|
||||
globalOrm.searchRead('res.country', [['code', '=', countryCode]], ['id'], { limit: 1 }),
|
||||
province
|
||||
? globalOrm.searchRead('res.country.state', [['code', '=', province], ['country_id.code', '=', countryCode]], ['id'], { limit: 1 })
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
if (countries.length) countryId = countries[0].id;
|
||||
if (states.length) stateId = states[0].id;
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
if (record.resId && globalOrm) {
|
||||
try {
|
||||
const firstWrite = {};
|
||||
if (street) firstWrite.street = street;
|
||||
if (unitNumber) firstWrite.street2 = unitNumber;
|
||||
if (city) firstWrite.city = city;
|
||||
if (postalCode) firstWrite.zip = postalCode;
|
||||
if (countryId) firstWrite.country_id = countryId;
|
||||
|
||||
await globalOrm.write('fusion.ltc.facility', [record.resId], firstWrite);
|
||||
|
||||
if (stateId) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
await globalOrm.write('fusion.ltc.facility', [record.resId], { state_id: stateId });
|
||||
}
|
||||
|
||||
await record.load();
|
||||
} catch (err) {
|
||||
console.error('[LTC GooglePlaces] Address ORM write failed:', err);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const textUpdate = {};
|
||||
if (street) textUpdate.street = street;
|
||||
if (unitNumber) textUpdate.street2 = unitNumber;
|
||||
if (city) textUpdate.city = city;
|
||||
if (postalCode) textUpdate.zip = postalCode;
|
||||
|
||||
await record.update(textUpdate);
|
||||
|
||||
if (countryId && globalOrm) {
|
||||
const formEl = input.closest('.o_form_view') || input.closest('.o_content') || document.body;
|
||||
const [countryData, stateData] = await Promise.all([
|
||||
globalOrm.read('res.country', [countryId], ['display_name']),
|
||||
stateId ? globalOrm.read('res.country.state', [stateId], ['display_name']) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
await simulateMany2OneSelection(formEl, 'country_id', countryId, countryData[0]?.display_name || 'Canada');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
|
||||
if (stateId && stateData.length) {
|
||||
await simulateMany2OneSelection(formEl, 'state_id', stateId, stateData[0]?.display_name || province);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[LTC GooglePlaces] Address autocomplete update failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => { _reattachFacilityAutocomplete(el, model); }, 400);
|
||||
});
|
||||
|
||||
autocompleteInstances.set(input, autocomplete);
|
||||
|
||||
input.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%234CAF50\'%3E%3Cpath d=\'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z\'/%3E%3C/svg%3E")';
|
||||
input.style.backgroundRepeat = 'no-repeat';
|
||||
input.style.backgroundPosition = 'right 8px center';
|
||||
input.style.backgroundSize = '20px';
|
||||
input.style.paddingRight = '35px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-attach facility autocomplete after OWL re-renders inputs.
|
||||
*/
|
||||
function _reattachFacilityAutocomplete(el, model) {
|
||||
const streetSelectors = [
|
||||
'div[name="street"] input',
|
||||
'.o_field_widget[name="street"] input',
|
||||
'[name="street"] input',
|
||||
];
|
||||
for (const sel of streetSelectors) {
|
||||
const inp = el.querySelector(sel);
|
||||
if (inp && !autocompleteInstances.has(inp)) {
|
||||
_attachFacilityAddressAutocomplete(inp, el, model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch FormController to add Google autocomplete for LTC Facility forms
|
||||
*/
|
||||
patch(FormController.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
|
||||
this.orm = useService("orm");
|
||||
|
||||
onMounted(() => {
|
||||
// LTC Facility form
|
||||
if (this.props.resModel === 'fusion.ltc.facility') {
|
||||
setTimeout(() => {
|
||||
if (this.rootRef && this.rootRef.el) {
|
||||
setupFacilityAutocomplete(this.rootRef.el, this.model, this.orm);
|
||||
}
|
||||
}, 800);
|
||||
|
||||
if (this.rootRef && this.rootRef.el) {
|
||||
this._facilityAddrObserver = new MutationObserver((mutations) => {
|
||||
const hasNewInputs = mutations.some(m =>
|
||||
m.addedNodes.length > 0 &&
|
||||
Array.from(m.addedNodes).some(n =>
|
||||
n.nodeType === 1 && (n.tagName === 'INPUT' || n.querySelector?.('input'))
|
||||
)
|
||||
);
|
||||
if (hasNewInputs) {
|
||||
setTimeout(() => {
|
||||
setupFacilityAutocomplete(this.rootRef.el, this.model, this.orm);
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
this._facilityAddrObserver.observe(this.rootRef.el, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (this._facilityAddrObserver) {
|
||||
this._facilityAddrObserver.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
180
fusion_ltc_management/views/ltc_cleanup_views.xml
Normal file
180
fusion_ltc_management/views/ltc_cleanup_views.xml
Normal file
@@ -0,0 +1,180 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CLEANUP - FORM VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_cleanup_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.cleanup.form</field>
|
||||
<field name="model">fusion.ltc.cleanup</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Cleanup Schedule">
|
||||
<header>
|
||||
<button name="action_start" type="object" string="Start"
|
||||
class="btn-primary"
|
||||
invisible="state != 'scheduled'"/>
|
||||
<button name="action_complete" type="object" string="Complete"
|
||||
class="btn-primary"
|
||||
invisible="state != 'in_progress'"/>
|
||||
<button name="action_cancel" type="object" string="Cancel"
|
||||
invisible="state in ('completed', 'cancelled')"/>
|
||||
<button name="action_reset" type="object" string="Reset to Scheduled"
|
||||
invisible="state not in ('cancelled', 'rescheduled')"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="scheduled,in_progress,completed"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_create_task" type="object"
|
||||
class="oe_stat_button" icon="fa-tasks">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Task</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Schedule">
|
||||
<field name="facility_id"/>
|
||||
<field name="scheduled_date"/>
|
||||
<field name="completed_date"/>
|
||||
<field name="technician_id"/>
|
||||
</group>
|
||||
<group string="Details">
|
||||
<field name="items_cleaned"/>
|
||||
<field name="task_id" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes" placeholder="Cleanup notes..."/>
|
||||
</page>
|
||||
<page string="Photos" name="photos">
|
||||
<field name="photo_ids" widget="many2many_binary"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CLEANUP - LIST VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_cleanup_list" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.cleanup.list</field>
|
||||
<field name="model">fusion.ltc.cleanup</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Cleanup Schedule" default_order="scheduled_date desc"
|
||||
decoration-success="state == 'completed'"
|
||||
decoration-muted="state in ('cancelled', 'rescheduled')">
|
||||
<field name="name"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="scheduled_date"/>
|
||||
<field name="completed_date" optional="show"/>
|
||||
<field name="technician_id" widget="many2one_avatar_user" optional="show"/>
|
||||
<field name="items_cleaned" optional="show"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-info="state == 'scheduled'"
|
||||
decoration-warning="state == 'in_progress'"
|
||||
decoration-success="state == 'completed'"
|
||||
decoration-danger="state == 'cancelled'"
|
||||
decoration-muted="state == 'rescheduled'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CLEANUP - KANBAN VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_cleanup_kanban" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.cleanup.kanban</field>
|
||||
<field name="model">fusion.ltc.cleanup</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="state" class="o_kanban_small_column">
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<div class="d-flex justify-content-between">
|
||||
<field name="name" class="fw-bold"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="facility_id" class="fw-bold"/>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
Scheduled: <field name="scheduled_date"/>
|
||||
</div>
|
||||
<div t-if="record.completed_date.raw_value" class="text-muted">
|
||||
Completed: <field name="completed_date"/>
|
||||
</div>
|
||||
<footer class="mt-2">
|
||||
<field name="technician_id" widget="many2one_avatar_user"/>
|
||||
</footer>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CLEANUP - SEARCH VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_cleanup_search" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.cleanup.search</field>
|
||||
<field name="model">fusion.ltc.cleanup</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Cleanups">
|
||||
<field name="facility_id"/>
|
||||
<field name="technician_id"/>
|
||||
<separator/>
|
||||
<filter string="Scheduled" name="filter_scheduled"
|
||||
domain="[('state', '=', 'scheduled')]"/>
|
||||
<filter string="In Progress" name="filter_in_progress"
|
||||
domain="[('state', '=', 'in_progress')]"/>
|
||||
<filter string="Completed" name="filter_completed"
|
||||
domain="[('state', '=', 'completed')]"/>
|
||||
<separator/>
|
||||
<filter string="Upcoming (7 Days)" name="filter_upcoming"
|
||||
domain="[('scheduled_date', '<=', (context_today() + datetime.timedelta(days=7)).strftime('%Y-%m-%d')),
|
||||
('scheduled_date', '>=', context_today().strftime('%Y-%m-%d')),
|
||||
('state', '=', 'scheduled')]"/>
|
||||
<filter string="Overdue" name="filter_overdue"
|
||||
domain="[('scheduled_date', '<', context_today().strftime('%Y-%m-%d')),
|
||||
('state', '=', 'scheduled')]"/>
|
||||
<separator/>
|
||||
<filter string="Facility" name="group_facility"
|
||||
context="{'group_by': 'facility_id'}"/>
|
||||
<filter string="Status" name="group_state"
|
||||
context="{'group_by': 'state'}"/>
|
||||
<filter string="Technician" name="group_technician"
|
||||
context="{'group_by': 'technician_id'}"/>
|
||||
<filter string="Scheduled Month" name="group_month"
|
||||
context="{'group_by': 'scheduled_date:month'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- ACTIONS -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_ltc_cleanups" model="ir.actions.act_window">
|
||||
<field name="name">Cleanup Schedule</field>
|
||||
<field name="res_model">fusion.ltc.cleanup</field>
|
||||
<field name="view_mode">list,kanban,form</field>
|
||||
<field name="search_view_id" ref="view_ltc_cleanup_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No cleanups scheduled yet
|
||||
</p>
|
||||
<p>Cleanups are auto-scheduled based on facility settings, or can be created manually.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
299
fusion_ltc_management/views/ltc_facility_views.xml
Normal file
299
fusion_ltc_management/views/ltc_facility_views.xml
Normal file
@@ -0,0 +1,299 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FACILITY - FORM VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_facility_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.facility.form</field>
|
||||
<field name="model">fusion.ltc.facility</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="LTC Facility">
|
||||
<header/>
|
||||
<sheet>
|
||||
<widget name="web_ribbon" text="Archived" bg_color="text-bg-danger"
|
||||
invisible="active"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_repairs" type="object"
|
||||
class="oe_stat_button" icon="fa-wrench">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="active_repair_count"/>
|
||||
</span>
|
||||
<span class="o_stat_text">Active Repairs</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_view_repairs" type="object"
|
||||
class="oe_stat_button" icon="fa-list">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="repair_count"/>
|
||||
</span>
|
||||
<span class="o_stat_text">Total Repairs</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_view_cleanups" type="object"
|
||||
class="oe_stat_button" icon="fa-refresh">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="cleanup_count"/>
|
||||
</span>
|
||||
<span class="o_stat_text">Cleanups</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<field name="image_1920" widget="image" class="oe_avatar"
|
||||
options="{'preview_image': 'image_128'}"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Facility Name"/>
|
||||
</h1>
|
||||
<field name="code" readonly="1"/>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Facility Details">
|
||||
<field name="partner_id"/>
|
||||
<field name="phone" widget="phone"/>
|
||||
<field name="email" widget="email"/>
|
||||
<field name="website" widget="url"/>
|
||||
</group>
|
||||
<group string="Address">
|
||||
<field name="street"/>
|
||||
<field name="street2"/>
|
||||
<field name="city"/>
|
||||
<field name="state_id"/>
|
||||
<field name="zip"/>
|
||||
<field name="country_id"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Key Contacts" name="contacts">
|
||||
<group>
|
||||
<group>
|
||||
<field name="director_of_care_id"/>
|
||||
<field name="service_supervisor_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="physiotherapist_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Floors & Stations" name="structure">
|
||||
<group>
|
||||
<field name="number_of_floors"/>
|
||||
</group>
|
||||
<field name="floor_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="head_nurse_id"/>
|
||||
<field name="physiotherapist_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
<page string="Nursing Stations" name="stations">
|
||||
<field name="floor_ids" mode="list">
|
||||
<list>
|
||||
<field name="name" string="Floor"/>
|
||||
<field name="station_ids" widget="many2many_tags" string="Stations"/>
|
||||
<field name="head_nurse_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
|
||||
<page string="Contract" name="contract">
|
||||
<group>
|
||||
<group>
|
||||
<field name="contract_start_date"/>
|
||||
<field name="contract_end_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="cleanup_frequency"/>
|
||||
<field name="cleanup_interval_days"
|
||||
invisible="cleanup_frequency != 'custom'"/>
|
||||
<field name="next_cleanup_date"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<separator string="Contract Document"/>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<field name="contract_file"
|
||||
filename="contract_file_filename"
|
||||
widget="binary" nolabel="1"/>
|
||||
<field name="contract_file_filename" invisible="1"/>
|
||||
<button name="action_preview_contract" type="object"
|
||||
class="btn btn-secondary"
|
||||
icon="fa-eye"
|
||||
string="Preview"
|
||||
invisible="not contract_file"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<field name="contract_notes" placeholder="Contract details and notes..."/>
|
||||
</page>
|
||||
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FACILITY - LIST VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_facility_list" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.facility.list</field>
|
||||
<field name="model">fusion.ltc.facility</field>
|
||||
<field name="arch" type="xml">
|
||||
<list default_order="name">
|
||||
<field name="code" optional="show"/>
|
||||
<field name="name"/>
|
||||
<field name="city"/>
|
||||
<field name="phone" optional="show"/>
|
||||
<field name="director_of_care_id" optional="show"/>
|
||||
<field name="service_supervisor_id" optional="hide"/>
|
||||
<field name="number_of_floors" optional="hide"/>
|
||||
<field name="contract_start_date" optional="hide"/>
|
||||
<field name="contract_end_date" optional="hide"/>
|
||||
<field name="cleanup_frequency" optional="show"/>
|
||||
<field name="next_cleanup_date" optional="show"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FACILITY - SEARCH VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_facility_search" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.facility.search</field>
|
||||
<field name="model">fusion.ltc.facility</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Facilities">
|
||||
<field name="name" string="Facility"/>
|
||||
<field name="code"/>
|
||||
<field name="city"/>
|
||||
<field name="director_of_care_id"/>
|
||||
<separator/>
|
||||
<filter string="Active" name="filter_active"
|
||||
domain="[('active', '=', True)]"/>
|
||||
<filter string="Archived" name="filter_archived"
|
||||
domain="[('active', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Has Repairs" name="filter_has_repairs"
|
||||
domain="[('repair_ids', '!=', False)]"/>
|
||||
<filter string="Cleanup Due" name="filter_cleanup_due"
|
||||
domain="[('next_cleanup_date', '<=', (context_today() + datetime.timedelta(days=7)).strftime('%Y-%m-%d')),
|
||||
('next_cleanup_date', '!=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="City" name="group_city" context="{'group_by': 'city'}"/>
|
||||
<filter string="Cleanup Frequency" name="group_frequency"
|
||||
context="{'group_by': 'cleanup_frequency'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- STATION - FORM VIEW (for inline editing within floors) -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_station_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.station.form</field>
|
||||
<field name="model">fusion.ltc.station</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Nursing Station">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="head_nurse_id"/>
|
||||
<field name="phone" widget="phone"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- STATION - LIST VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_station_list" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.station.list</field>
|
||||
<field name="model">fusion.ltc.station</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Nursing Stations" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="floor_id"/>
|
||||
<field name="name"/>
|
||||
<field name="head_nurse_id"/>
|
||||
<field name="phone"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FLOOR - FORM VIEW (with embedded stations) -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_floor_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.floor.form</field>
|
||||
<field name="model">fusion.ltc.floor</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Floor">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="facility_id" invisible="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="head_nurse_id"/>
|
||||
<field name="physiotherapist_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="station_ids">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="head_nurse_id"/>
|
||||
<field name="phone"/>
|
||||
</list>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- ACTIONS -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_ltc_facilities" model="ir.actions.act_window">
|
||||
<field name="name">LTC Facilities</field>
|
||||
<field name="res_model">fusion.ltc.facility</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_ltc_facility_search"/>
|
||||
<field name="context">{'search_default_filter_active': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Add your first LTC Facility
|
||||
</p>
|
||||
<p>Create a facility record to start managing repairs and cleanups.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
136
fusion_ltc_management/views/ltc_form_submission_views.xml
Normal file
136
fusion_ltc_management/views/ltc_form_submission_views.xml
Normal file
@@ -0,0 +1,136 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FORM SUBMISSION - LIST VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_form_submission_list" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.form.submission.list</field>
|
||||
<field name="model">fusion.ltc.form.submission</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Form Submissions" default_order="submitted_date desc"
|
||||
decoration-danger="is_emergency"
|
||||
decoration-info="status == 'submitted'"
|
||||
decoration-success="status == 'processed'">
|
||||
<field name="name"/>
|
||||
<field name="form_type"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="client_name"/>
|
||||
<field name="room_number"/>
|
||||
<field name="product_serial" optional="show"/>
|
||||
<field name="is_emergency"/>
|
||||
<field name="submitted_date"/>
|
||||
<field name="ip_address" optional="hide"/>
|
||||
<field name="repair_id" optional="show"/>
|
||||
<field name="status" widget="badge"
|
||||
decoration-info="status == 'submitted'"
|
||||
decoration-success="status == 'processed'"
|
||||
decoration-danger="status == 'rejected'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FORM SUBMISSION - FORM VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_form_submission_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.form.submission.form</field>
|
||||
<field name="model">fusion.ltc.form.submission</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Form Submission">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_repair" type="object"
|
||||
class="oe_stat_button" icon="fa-wrench"
|
||||
invisible="not repair_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Repair</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="form_type"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="client_name"/>
|
||||
<field name="room_number"/>
|
||||
<field name="product_serial"/>
|
||||
<field name="is_emergency"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="status"/>
|
||||
<field name="submitted_date"/>
|
||||
<field name="ip_address"/>
|
||||
<field name="repair_id"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FORM SUBMISSION - SEARCH VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_form_submission_search" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.form.submission.search</field>
|
||||
<field name="model">fusion.ltc.form.submission</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Submissions">
|
||||
<field name="name"/>
|
||||
<field name="client_name"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="room_number"/>
|
||||
<separator/>
|
||||
<filter string="Emergency" name="filter_emergency"
|
||||
domain="[('is_emergency', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Submitted" name="filter_submitted"
|
||||
domain="[('status', '=', 'submitted')]"/>
|
||||
<filter string="Processed" name="filter_processed"
|
||||
domain="[('status', '=', 'processed')]"/>
|
||||
<filter string="Rejected" name="filter_rejected"
|
||||
domain="[('status', '=', 'rejected')]"/>
|
||||
<separator/>
|
||||
<filter string="Today" name="filter_today"
|
||||
domain="[('submitted_date', '>=', (context_today()).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="This Week" name="filter_week"
|
||||
domain="[('submitted_date', '>=', (context_today() - datetime.timedelta(days=7)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="This Month" name="filter_month"
|
||||
domain="[('submitted_date', '>=', (context_today()).strftime('%Y-%m-01'))]"/>
|
||||
<separator/>
|
||||
<filter string="Facility" name="group_facility"
|
||||
context="{'group_by': 'facility_id'}"/>
|
||||
<filter string="Form Type" name="group_type"
|
||||
context="{'group_by': 'form_type'}"/>
|
||||
<filter string="Status" name="group_status"
|
||||
context="{'group_by': 'status'}"/>
|
||||
<filter string="Submitted Date" name="group_date"
|
||||
context="{'group_by': 'submitted_date:day'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- ACTIONS -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_ltc_form_submissions" model="ir.actions.act_window">
|
||||
<field name="name">Form Submissions</field>
|
||||
<field name="res_model">fusion.ltc.form.submission</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_ltc_form_submission_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No form submissions yet
|
||||
</p>
|
||||
<p>Submissions from portal repair forms will appear here.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
375
fusion_ltc_management/views/ltc_repair_views.xml
Normal file
375
fusion_ltc_management/views/ltc_repair_views.xml
Normal file
@@ -0,0 +1,375 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- REPAIR - FORM VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_repair_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.repair.form</field>
|
||||
<field name="model">fusion.ltc.repair</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Repair Request">
|
||||
<header>
|
||||
<button name="action_create_sale_order" type="object"
|
||||
string="Create Sale Order" class="btn-primary"
|
||||
invisible="sale_order_id"/>
|
||||
<field name="stage_id" widget="statusbar"
|
||||
options="{'clickable': '1'}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_sale_order" type="object"
|
||||
class="oe_stat_button" icon="fa-shopping-cart"
|
||||
invisible="not sale_order_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="sale_order_name"/>
|
||||
</span>
|
||||
<span class="o_stat_text">Sale Order</span>
|
||||
</div>
|
||||
</button>
|
||||
<button name="action_view_task" type="object"
|
||||
class="oe_stat_button" icon="fa-tasks"
|
||||
invisible="not task_id">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Task</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" text="Emergency" bg_color="text-bg-danger"
|
||||
invisible="not is_emergency"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Repair Details">
|
||||
<field name="facility_id"/>
|
||||
<field name="client_id"/>
|
||||
<field name="client_name" invisible="client_id"/>
|
||||
<field name="room_number"/>
|
||||
<field name="is_emergency"/>
|
||||
<field name="priority" widget="priority"/>
|
||||
</group>
|
||||
<group string="Product & Assignment">
|
||||
<field name="product_serial"/>
|
||||
<field name="product_id"/>
|
||||
<field name="assigned_technician_id"/>
|
||||
<field name="source"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<notebook>
|
||||
<page string="Issue" name="issue">
|
||||
<group>
|
||||
<group>
|
||||
<field name="issue_reported_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="issue_fixed_date"/>
|
||||
</group>
|
||||
</group>
|
||||
<label for="issue_description"/>
|
||||
<field name="issue_description"
|
||||
placeholder="Describe the issue in detail..."/>
|
||||
<label for="resolution_description"/>
|
||||
<field name="resolution_description"
|
||||
placeholder="How was the issue resolved?"/>
|
||||
</page>
|
||||
|
||||
<page string="Family/POA" name="poa">
|
||||
<group>
|
||||
<group>
|
||||
<field name="poa_name"/>
|
||||
<field name="poa_phone" widget="phone"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Financial" name="financial">
|
||||
<group>
|
||||
<group>
|
||||
<field name="sale_order_id"/>
|
||||
<field name="task_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="repair_value"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
<page string="Photos & Notes" name="photos">
|
||||
<div class="fc-gallery-content">
|
||||
<separator string="Before Photos (Reported Condition)"/>
|
||||
<field name="before_photo_ids" widget="many2many_binary"/>
|
||||
<separator string="After Photos (Completed Repair)"/>
|
||||
<field name="after_photo_ids" widget="many2many_binary"/>
|
||||
</div>
|
||||
<field name="photo_ids" invisible="1"/>
|
||||
<separator string="Internal Notes"/>
|
||||
<field name="notes" placeholder="Internal notes..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- REPAIR - KANBAN VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_repair_kanban" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.repair.kanban</field>
|
||||
<field name="model">fusion.ltc.repair</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="stage_id"
|
||||
default_order="is_emergency desc, issue_reported_date desc"
|
||||
highlight_color="color"
|
||||
class="o_kanban_small_column">
|
||||
<field name="color"/>
|
||||
<field name="is_emergency"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="kanban_state"/>
|
||||
<field name="stage_color"/>
|
||||
<progressbar field="kanban_state"
|
||||
colors='{"normal": "200", "done": "success", "blocked": "danger"}'/>
|
||||
<templates>
|
||||
<t t-name="menu">
|
||||
<a t-if="widget.editable" role="menuitem" type="open"
|
||||
class="dropdown-item">Edit</a>
|
||||
<a t-if="widget.deletable" role="menuitem" type="delete"
|
||||
class="dropdown-item">Delete</a>
|
||||
<field widget="kanban_color_picker" name="color"/>
|
||||
</t>
|
||||
<t t-name="card" class="flex-row">
|
||||
<main t-att-data-stage="record.stage_color.raw_value || 'secondary'"
|
||||
t-att-data-emergency="record.is_emergency.raw_value ? '1' : '0'"
|
||||
t-att-data-priority="record.priority.raw_value">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<field name="name" class="fw-bold text-primary"/>
|
||||
<span t-attf-style="#{record.priority.raw_value == '1' ? 'background: rgba(255,152,0,0.1); border-radius: 4px; padding: 1px 5px;' : ''}">
|
||||
<field name="priority" widget="priority"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<span class="fw-bold">
|
||||
<field name="display_client_name"/>
|
||||
</span>
|
||||
<span t-if="record.room_number.raw_value"
|
||||
class="ms-1 badge text-bg-light">
|
||||
Rm <field name="room_number"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<field name="facility_id"/>
|
||||
</div>
|
||||
<div t-if="record.is_emergency.raw_value"
|
||||
class="mt-1">
|
||||
<span class="badge text-bg-danger">
|
||||
<i class="fa fa-exclamation-triangle me-1"/>Emergency
|
||||
</span>
|
||||
</div>
|
||||
<div t-if="record.product_serial.raw_value"
|
||||
class="text-muted small mt-1">
|
||||
<i class="fa fa-barcode me-1"/>S/N: <field name="product_serial"/>
|
||||
</div>
|
||||
<footer class="mt-2 pt-1 border-top">
|
||||
<span class="text-muted small">
|
||||
<i class="fa fa-calendar me-1"/><field name="issue_reported_date"/>
|
||||
</span>
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
<field name="kanban_state" widget="state_selection"/>
|
||||
<field name="assigned_technician_id"
|
||||
widget="many2one_avatar_user"
|
||||
class="ms-1"/>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- REPAIR - LIST VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_repair_list" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.repair.list</field>
|
||||
<field name="model">fusion.ltc.repair</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Repair Requests" default_order="issue_reported_date desc"
|
||||
decoration-danger="is_emergency">
|
||||
<field name="stage_color" column_invisible="1"/>
|
||||
<field name="name"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="display_client_name" string="Client"/>
|
||||
<field name="room_number"/>
|
||||
<field name="product_serial" optional="show"/>
|
||||
<field name="issue_reported_date"/>
|
||||
<field name="issue_fixed_date" optional="show"/>
|
||||
<field name="stage_id" widget="badge"
|
||||
decoration-info="stage_color == 'info'"
|
||||
decoration-warning="stage_color == 'warning'"
|
||||
decoration-success="stage_color == 'success'"
|
||||
decoration-danger="stage_color == 'danger'"
|
||||
optional="show"/>
|
||||
<field name="is_emergency" string="Emergency" optional="show"
|
||||
widget="boolean_toggle"/>
|
||||
<field name="assigned_technician_id" widget="many2one_avatar_user"
|
||||
optional="show"/>
|
||||
<field name="sale_order_id" optional="hide"/>
|
||||
<field name="repair_value" optional="hide"/>
|
||||
<field name="source" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- REPAIR - SEARCH VIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_repair_search" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.repair.search</field>
|
||||
<field name="model">fusion.ltc.repair</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Repairs">
|
||||
<field name="name"/>
|
||||
<field name="display_client_name" string="Client"/>
|
||||
<field name="room_number"/>
|
||||
<field name="product_serial"/>
|
||||
<field name="facility_id"/>
|
||||
<field name="assigned_technician_id"/>
|
||||
<separator/>
|
||||
<filter string="Emergency" name="filter_emergency"
|
||||
domain="[('is_emergency', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="New" name="filter_new"
|
||||
domain="[('stage_id.sequence', '=', 1)]"/>
|
||||
<filter string="In Progress" name="filter_in_progress"
|
||||
domain="[('stage_id.fold', '=', False), ('stage_id.sequence', '>', 1)]"/>
|
||||
<filter string="Completed" name="filter_completed"
|
||||
domain="[('stage_id.fold', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Has Sale Order" name="filter_has_so"
|
||||
domain="[('sale_order_id', '!=', False)]"/>
|
||||
<filter string="No Sale Order" name="filter_no_so"
|
||||
domain="[('sale_order_id', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="This Month" name="filter_this_month"
|
||||
domain="[('issue_reported_date', '>=', (context_today()).strftime('%Y-%m-01'))]"/>
|
||||
<filter string="Last 30 Days" name="filter_30d"
|
||||
domain="[('issue_reported_date', '>=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
|
||||
<filter string="This Year" name="filter_this_year"
|
||||
domain="[('issue_reported_date', '>=', (context_today()).strftime('%Y-01-01'))]"/>
|
||||
<separator/>
|
||||
<filter string="Facility" name="group_facility"
|
||||
context="{'group_by': 'facility_id'}"/>
|
||||
<filter string="Stage" name="group_stage"
|
||||
context="{'group_by': 'stage_id'}"/>
|
||||
<filter string="Technician" name="group_technician"
|
||||
context="{'group_by': 'assigned_technician_id'}"/>
|
||||
<filter string="Source" name="group_source"
|
||||
context="{'group_by': 'source'}"/>
|
||||
<filter string="Reported Month" name="group_month"
|
||||
context="{'group_by': 'issue_reported_date:month'}"/>
|
||||
<filter string="Fixed Month" name="group_fixed_month"
|
||||
context="{'group_by': 'issue_fixed_date:month'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- REPAIR STAGE - LIST + FORM (Configuration) -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="view_ltc_repair_stage_list" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.repair.stage.list</field>
|
||||
<field name="model">fusion.ltc.repair.stage</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Repair Stages" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="color"/>
|
||||
<field name="fold"/>
|
||||
<field name="description" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_ltc_repair_stage_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.repair.stage.form</field>
|
||||
<field name="model">fusion.ltc.repair.stage</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Repair Stage">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="color"/>
|
||||
<field name="fold"/>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- ACTIONS -->
|
||||
<!-- ================================================================== -->
|
||||
<record id="action_ltc_repairs_kanban" model="ir.actions.act_window">
|
||||
<field name="name">Repair Overview</field>
|
||||
<field name="res_model">fusion.ltc.repair</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_ltc_repair_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No repair requests yet
|
||||
</p>
|
||||
<p>Repair requests submitted via the portal form or manually will appear here.</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_ltc_repairs_all" model="ir.actions.act_window">
|
||||
<field name="name">All Repairs</field>
|
||||
<field name="res_model">fusion.ltc.repair</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_ltc_repair_search"/>
|
||||
</record>
|
||||
|
||||
<record id="action_ltc_repairs_new" model="ir.actions.act_window">
|
||||
<field name="name">New / Pending</field>
|
||||
<field name="res_model">fusion.ltc.repair</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_ltc_repair_search"/>
|
||||
<field name="domain">[('stage_id.sequence', '<=', 2)]</field>
|
||||
</record>
|
||||
|
||||
<record id="action_ltc_repairs_in_progress" model="ir.actions.act_window">
|
||||
<field name="name">In Progress</field>
|
||||
<field name="res_model">fusion.ltc.repair</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_ltc_repair_search"/>
|
||||
<field name="domain">[('stage_id.fold', '=', False), ('stage_id.sequence', '>', 2)]</field>
|
||||
</record>
|
||||
|
||||
<record id="action_ltc_repairs_completed" model="ir.actions.act_window">
|
||||
<field name="name">Completed</field>
|
||||
<field name="res_model">fusion.ltc.repair</field>
|
||||
<field name="view_mode">kanban,list,form</field>
|
||||
<field name="search_view_id" ref="view_ltc_repair_search"/>
|
||||
<field name="domain">[('stage_id.fold', '=', True)]</field>
|
||||
</record>
|
||||
|
||||
<record id="action_ltc_repair_stages" model="ir.actions.act_window">
|
||||
<field name="name">Repair Stages</field>
|
||||
<field name="res_model">fusion.ltc.repair.stage</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
95
fusion_ltc_management/views/menus.xml
Normal file
95
fusion_ltc_management/views/menus.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- ROOT MENU (Application) -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_ltc_root"
|
||||
name="LTC Management"
|
||||
web_icon="fusion_ltc_management,static/description/icon.png"
|
||||
sequence="45"
|
||||
groups="sales_team.group_sale_salesman"/>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- OVERVIEW -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_ltc_overview"
|
||||
name="Overview"
|
||||
parent="menu_ltc_root"
|
||||
action="action_ltc_repairs_kanban"
|
||||
sequence="1"/>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- REPAIR REQUESTS -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_ltc_repairs"
|
||||
name="Repair Requests"
|
||||
parent="menu_ltc_root"
|
||||
sequence="10"/>
|
||||
<menuitem id="menu_ltc_repairs_all"
|
||||
name="All Repairs"
|
||||
parent="menu_ltc_repairs"
|
||||
action="action_ltc_repairs_all"
|
||||
sequence="1"/>
|
||||
<menuitem id="menu_ltc_repairs_new"
|
||||
name="New / Pending"
|
||||
parent="menu_ltc_repairs"
|
||||
action="action_ltc_repairs_new"
|
||||
sequence="2"/>
|
||||
<menuitem id="menu_ltc_repairs_progress"
|
||||
name="In Progress"
|
||||
parent="menu_ltc_repairs"
|
||||
action="action_ltc_repairs_in_progress"
|
||||
sequence="3"/>
|
||||
<menuitem id="menu_ltc_repairs_completed"
|
||||
name="Completed"
|
||||
parent="menu_ltc_repairs"
|
||||
action="action_ltc_repairs_completed"
|
||||
sequence="4"/>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CLEANUP SCHEDULE -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_ltc_cleanup"
|
||||
name="Cleanup Schedule"
|
||||
parent="menu_ltc_root"
|
||||
action="action_ltc_cleanups"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- LOCATIONS -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_ltc_locations"
|
||||
name="Locations"
|
||||
parent="menu_ltc_root"
|
||||
sequence="30"/>
|
||||
<menuitem id="menu_ltc_facilities"
|
||||
name="Facilities"
|
||||
parent="menu_ltc_locations"
|
||||
action="action_ltc_facilities"
|
||||
sequence="1"/>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- FORM SUBMISSIONS -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_ltc_form_submissions"
|
||||
name="Form Submissions"
|
||||
parent="menu_ltc_root"
|
||||
action="action_ltc_form_submissions"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- ================================================================== -->
|
||||
<!-- CONFIGURATION -->
|
||||
<!-- ================================================================== -->
|
||||
<menuitem id="menu_ltc_config"
|
||||
name="Configuration"
|
||||
parent="menu_ltc_root"
|
||||
sequence="90"
|
||||
groups="sales_team.group_sale_manager"/>
|
||||
<menuitem id="menu_ltc_repair_stages"
|
||||
name="Repair Stages"
|
||||
parent="menu_ltc_config"
|
||||
action="action_ltc_repair_stages"
|
||||
sequence="1"/>
|
||||
|
||||
</odoo>
|
||||
24
fusion_ltc_management/views/res_config_settings_views.xml
Normal file
24
fusion_ltc_management/views/res_config_settings_views.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form_ltc" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.ltc</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="LTC Management" string="LTC Management"
|
||||
data-key="fusion_ltc_management">
|
||||
<block title="Portal Forms">
|
||||
<setting string="LTC Form Access Password"
|
||||
help="Set a password to protect the public LTC repair form. Share this with facility staff so they can submit repair requests. Minimum 4 characters. Leave empty to allow unrestricted access.">
|
||||
<field name="fc_ltc_form_password"
|
||||
placeholder="e.g. 1234"/>
|
||||
</setting>
|
||||
</block>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
37
fusion_ltc_management/views/res_partner_views.xml
Normal file
37
fusion_ltc_management/views/res_partner_views.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Add LTC Home tab to Partner Form -->
|
||||
<record id="view_partner_form_ltc" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.ltc</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="LTC Home" name="ltc_info"
|
||||
invisible="x_fc_contact_type not in ('long_term_care_home',)">
|
||||
<group string="LTC Facility Information">
|
||||
<group>
|
||||
<field name="x_fc_ltc_facility_id"/>
|
||||
<field name="x_fc_ltc_room_number"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Family Contacts">
|
||||
<field name="x_fc_ltc_family_contact_ids" nolabel="1">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="relationship"/>
|
||||
<field name="phone"/>
|
||||
<field name="phone2"/>
|
||||
<field name="email"/>
|
||||
<field name="is_poa"/>
|
||||
<field name="notes" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
17
fusion_ltc_management/views/sale_order_views.xml
Normal file
17
fusion_ltc_management/views/sale_order_views.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Add LTC Repair fields to Sale Order form -->
|
||||
<record id="view_order_form_ltc" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.ltc</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="x_fc_ltc_repair_id" invisible="not x_fc_is_ltc_repair_sale"/>
|
||||
<field name="x_fc_is_ltc_repair_sale" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
17
fusion_ltc_management/views/technician_task_views.xml
Normal file
17
fusion_ltc_management/views/technician_task_views.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Add facility_id to technician task form -->
|
||||
<record id="view_technician_task_form_ltc" model="ir.ui.view">
|
||||
<field name="name">fusion.technician.task.form.ltc</field>
|
||||
<field name="model">fusion.technician.task</field>
|
||||
<field name="inherit_id" ref="fusion_tasks.view_technician_task_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='priority']" position="after">
|
||||
<field name="facility_id"
|
||||
invisible="task_type != 'ltc_visit'"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
5
fusion_ltc_management/wizard/__init__.py
Normal file
5
fusion_ltc_management/wizard/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2024-2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import ltc_repair_create_so_wizard
|
||||
Binary file not shown.
Binary file not shown.
48
fusion_ltc_management/wizard/ltc_repair_create_so_wizard.py
Normal file
48
fusion_ltc_management/wizard/ltc_repair_create_so_wizard.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- 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 LTCRepairCreateSOWizard(models.TransientModel):
|
||||
_name = 'fusion.ltc.repair.create.so.wizard'
|
||||
_description = 'LTC Repair - Link Contact & Create Sale Order'
|
||||
|
||||
repair_id = fields.Many2one(
|
||||
'fusion.ltc.repair',
|
||||
string='Repair Request',
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
client_name = fields.Char(
|
||||
string='Client Name',
|
||||
readonly=True,
|
||||
)
|
||||
action_type = fields.Selection([
|
||||
('create_new', 'Create New Contact'),
|
||||
('link_existing', 'Link to Existing Contact'),
|
||||
], string='Action', default='create_new', required=True)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Existing Contact',
|
||||
)
|
||||
|
||||
def action_confirm(self):
|
||||
self.ensure_one()
|
||||
repair = self.repair_id
|
||||
|
||||
if self.action_type == 'create_new':
|
||||
if not self.client_name:
|
||||
raise UserError(_('Client name is required to create a new contact.'))
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': self.client_name,
|
||||
})
|
||||
repair.client_id = partner
|
||||
elif self.action_type == 'link_existing':
|
||||
if not self.partner_id:
|
||||
raise UserError(_('Please select an existing contact.'))
|
||||
repair.client_id = self.partner_id
|
||||
|
||||
return repair._create_linked_sale_order()
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_ltc_repair_create_so_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fusion.ltc.repair.create.so.wizard.form</field>
|
||||
<field name="model">fusion.ltc.repair.create.so.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Link Contact">
|
||||
<group>
|
||||
<field name="repair_id" invisible="1"/>
|
||||
<field name="client_name"/>
|
||||
<field name="action_type" widget="radio"/>
|
||||
<field name="partner_id"
|
||||
invisible="action_type != 'link_existing'"
|
||||
required="action_type == 'link_existing'"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_confirm" type="object"
|
||||
string="Confirm & Create Sale Order"
|
||||
class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary"
|
||||
special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user