This commit is contained in:
gsinghpal
2026-03-09 15:21:22 -04:00
parent a3e85a23ef
commit acd3fc455e
243 changed files with 20459 additions and 4197 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
localhost FALSE / FALSE 1804532345 frontend_lang en_CA
#HttpOnly_localhost FALSE / FALSE 1773601137 session_id 70Wf5yZpnwpU0Izf5wBSbiWNk7UjsoGB1737H73bWDK16z05MJP0SJnNN-NhOfw8GbW6a-d_-y0opbJwcgbq

230
equipment_type=[^"]* Normal file
View File

@@ -0,0 +1,230 @@
<html>
<head>
<title>Internal Server Error</title>
<link rel="stylesheet" href="/web/static/lib/bootstrap/dist/css/bootstrap.css"/>
<script src="/web/static/lib/jquery/jquery.js" type="text/javascript"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/index.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/dom/data.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/dom/event-handler.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/dom/manipulator.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/dom/selector-engine.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/config.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/base-component.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/component-functions.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/backdrop.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/focustrap.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/util/scrollbar.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/modal.js"></script>
<script type="text/javascript" src="/web/static/lib/bootstrap/js/dist/collapse.js"></script>
<style>
html {
font-size: 14px;
}
</style>
<script>
$(document).ready(function() {
var button = $('.reset_templates_button');
button.click(function() {
$('#reset_templates_mode').val($(this).data('mode'));
var dialog = $('#reset_template_confirmation').modal('show');
var input = dialog.find('input[type="text"]').val('').focus();
var dialog_form = dialog.find('form');
dialog_form.submit(function() {
if (input.val() == dialog.find('.confirm_word').text()) {
dialog.modal('hide');
button.prop('disabled', true).text('Working...');
const id = document.querySelector('input[id="reset_templates_view_id"]').value;
const redirect = document.querySelector('input[name="redirect"]').value;
const mode = document.querySelector('input[id="reset_templates_mode"]').value;
fetch('/website/reset_template', {
method: "POST",
headers: {
"Content-Type": "application/json",
},
'body': JSON.stringify({'params': {'view_id': id, 'mode': mode}})
}).then(() => window.location = redirect);
} else {
input.val('').focus();
}
return false;
});
return false;
});
});
</script>
</head>
<body>
<div role="dialog" id="reset_template_confirmation" class="modal" tabindex="-1" data-oe-id="8693" data-oe-xpath="/data/xpath[3]/div" data-oe-model="ir.ui.view" data-oe-field="arch">
<div class="modal-dialog">
<form role="form">
<div class="modal-content">
<header class="modal-header">
<h4 class="modal-title">Reset templates</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</header>
<main class="modal-body">
<div class="row mb0">
<label for="page-name" class="col-md-12 col-form-label">
<p>The selected templates will be reset to their factory settings.</p>
</label>
</div>
<div class="row mb0">
<label for="page-name" class="col-md-9 col-form-label">
<p>Type '<i class="confirm_word">yes</i>' in the box below if you want to confirm.</p>
</label>
<div class="col-md-3 mt16">
<input type="text" id="page-name" class="form-control" required="required" placeholder="yes"/>
</div>
</div>
</main>
<footer class="modal-footer">
<button type="button" class="btn" data-bs-dismiss="modal" aria-label="Cancel">Cancel</button>
<input type="submit" value="Confirm" class="btn btn-primary"/>
</footer>
</div>
</form>
</div>
</div>
<div id="wrapwrap">
<header data-oe-model="ir.ui.view" data-oe-id="294" data-oe-field="arch" data-oe-xpath="/t[1]/html[1]/body[1]/div[1]/header[1]">
<div class="navbar navbar-expand-md navbar-light bg-light">
<div class="container">
<div class="collapse navbar-collapse navbar-top-collapse">
<ul class="navbar-nav ms-auto" id="top_menu">
<li class="nav-item"><a href="/" class="nav-link">Home</a></li>
<li class="nav-item"><a href="javascript: window.history.back()" class="nav-link">Back</a></li>
</ul>
</div>
</div>
</div>
</header>
<main>
<div id="error_message" class="oe_structure">
<h2 class="container mt32">500: Internal Server Error</h2>
</div>
<div class="container">
<div class="alert alert-danger" role="alert">
<h4 data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/h4[1]">Template fallback</h4>
<p>An error occurred while rendering the template <code>fusion_quotations.portal_quotation_list</code>.</p>
<p data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/p[2]">If this error is caused by a change of yours in the templates, you have the possibility to reset the template to its <strong>factory settings</strong>.</p>
<form action="#" method="post" id="reset_templates_form">
<ul>
<li>
<label>
Equipment Assessments List
</label>
</li>
</ul>
<input type="hidden" name="redirect" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/input[1]" value="/my/quotation/builder"/>
<input type="hidden" id="reset_templates_view_id" name="view_id" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/input[2]" value="13488"/>
<input type="hidden" id="reset_templates_mode" name="mode" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/input[3]"/>
<button data-mode="soft" class="reset_templates_button btn btn-info" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/button[1]">Restore previous version (soft reset).</button>
<button data-mode="hard" class="reset_templates_button btn btn-outline-danger" data-oe-model="ir.ui.view" data-oe-id="8693" data-oe-field="arch" data-oe-xpath="/data/xpath[4]/div/div[1]/form[1]/button[2]">Reset to initial version (hard reset).</button>
</form>
</div>
</div>
<div class="container accordion mb32 mt32" id="debug_infos">
<div class="card">
<h4 class="card-header" data-oe-model="ir.ui.view" data-oe-id="290" data-oe-field="arch" data-oe-xpath="/t[1]/div[1]/div[2]/h4[1]">
<a data-bs-toggle="collapse" href="#error_qweb">QWeb</a>
</h4>
<div id="error_qweb" class="collapse show">
<div class="card-body">
<p>
The error occurred while rendering the template <code>fusion_quotations.portal_quotation_list</code>
and evaluating the following expression: <code>&lt;t t-out=&#34;dict(a._fields[\&#39;equipment_type\&#39;].selection).get(a.equipment_type, a.equipment_type or \&#39;\&#39;)&#34;/&gt;</code>
</p>
<pre>Error while rendering the template:
ValueError: dictionary update sequence element #0 has length 1; 2 is required
Template: fusion_quotations.portal_quotation_list
Reference: 13488
Path: /t/t/div/t[2]/table/tbody/t/tr/td[3]/t
Element: &lt;t t-out=&#34;dict(a._fields[\&#39;equipment_type\&#39;].selection).get(a.equipment_type, a.equipment_type or \&#39;\&#39;)&#34;/&gt;
From: (13488, &#39;/t/t&#39;, &#39;&lt;t t-call=&#34;portal.portal_layout&#34;/&gt;&#39;)
(13488, &#39;/t/t/div/t[2]/table/tbody/t/tr/td[3]/t&#39;, &#39;&lt;t t-out=&#34;dict(a._fields[\\\&#39;equipment_type\\\&#39;].selection).get(a.equipment_type, a.equipment_type or \\\&#39;\\\&#39;)&#34;/&gt;&#39;)</pre>
</div>
</div>
</div>
<div class="card">
<h4 class="card-header" data-oe-model="ir.ui.view" data-oe-id="290" data-oe-field="arch" data-oe-xpath="/t[1]/div[1]/div[3]/h4[1]">
<a data-bs-toggle="collapse" href="#error_traceback">Traceback</a>
</h4>
<div id="error_traceback" class="collapse ">
<div class="card-body">
<pre id="exception_traceback">Traceback (most recent call last):
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 753, in _render_iterall
for item in frame.iterator:
File &#34;&lt;13488&gt;&#34;, line 152, in template_fusion_quotations_portal_quotation_list_13488_t_call_0
ValueError: dictionary update sequence element #0 has length 1; 2 is required
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 2275, in _serve_db
return service_model.retrying(serve_func, env=self.env)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/service/model.py&#34;, line 184, in retrying
result = func()
^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 2330, in _serve_ir_http
response = self.dispatcher.dispatch(rule.endpoint, args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 2452, in dispatch
return self.request.registry[&#39;ir.http&#39;]._dispatch(endpoint)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_http.py&#34;, line 357, in _dispatch
result.flatten()
File &#34;/usr/lib/python3/dist-packages/odoo/tools/facade.py&#34;, line 83, in wrap_func
func(self._wrapped__, *args, **kwargs)
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 1546, in flatten
self.response.append(self.render())
^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/http.py&#34;, line 1538, in render
return request.env[&#34;ir.ui.view&#34;]._render_template(self.template, self.qcontext)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/website/models/ir_ui_view.py&#34;, line 456, in _render_template
return super()._render_template(template, values=values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_ui_view.py&#34;, line 2531, in _render_template
return self.env[&#39;ir.qweb&#39;]._render(template, values)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/mnt/enterprise-addons/web_studio/models/ir_qweb.py&#34;, line 14, in _render
return super()._render(template, values, **options)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 725, in _render
return Markup(&#39;&#39;.join(iterator))
^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 753, in _render_iterall
for item in frame.iterator:
File &#34;&lt;13488&gt;&#34;, line 264, in template_fusion_quotations_portal_quotation_list_13488
File &#34;&lt;13488&gt;&#34;, line 250, in template_fusion_quotations_portal_quotation_list_13488_content
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 616, in __str__
self.html = &#39;&#39;.join(self.irQweb._render_iterall(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File &#34;/usr/lib/python3/dist-packages/odoo/addons/base/models/ir_qweb.py&#34;, line 847, in _render_iterall
raise QWebError(qweb_error_info) from error
odoo.addons.base.models.ir_qweb.QWebError: Error while rendering the template:
ValueError: dictionary update sequence element #0 has length 1; 2 is required
Template: fusion_quotations.portal_quotation_list
Reference: 13488
Path: /t/t/div/t[2]/table/tbody/t/tr/td[3]/t
Element: &lt;t t-out=&#34;dict(a._fields[\&#39;equipment_type\&#39;].selection).get(a.equipment_type, a.equipment_type or \&#39;\&#39;)&#34;/&gt;
From: (13488, &#39;/t/t&#39;, &#39;&lt;t t-call=&#34;portal.portal_layout&#34;/&gt;&#39;)
(13488, &#39;/t/t/div/t[2]/table/tbody/t/tr/td[3]/t&#39;, &#39;&lt;t t-out=&#34;dict(a._fields[\\\&#39;equipment_type\\\&#39;].selection).get(a.equipment_type, a.equipment_type or \\\&#39;\\\&#39;)&#34;/&gt;&#39;)
</pre>
</div>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -53,6 +53,7 @@ This module provides external portal access for:
'appointment', 'appointment',
'knowledge', 'knowledge',
'fusion_claims', 'fusion_claims',
'fusion_tasks',
], ],
'data': [ 'data': [
# Security # Security
@@ -80,6 +81,7 @@ This module provides external portal access for:
'views/portal_book_assessment.xml', 'views/portal_book_assessment.xml',
'views/portal_repair_form.xml', 'views/portal_repair_form.xml',
'views/portal_schedule.xml', 'views/portal_schedule.xml',
'views/portal_page11_sign_templates.xml',
], ],
'assets': { 'assets': {
'web.assets_backend': [ 'web.assets_backend': [

View File

@@ -4,4 +4,5 @@ from . import portal_main
from . import portal_assessment from . import portal_assessment
from . import pdf_editor from . import pdf_editor
from . import portal_repair from . import portal_repair
from . import portal_schedule from . import portal_schedule
from . import portal_page11_sign

View File

@@ -1501,6 +1501,13 @@ class AuthorizerPortal(CustomerPortal):
accuracy=accuracy, accuracy=accuracy,
) )
# Push location to remote instances for cross-instance visibility
try:
request.env['fusion.task.sync.config'].sudo()._push_technician_location(
user.id, latitude, longitude, accuracy or 0)
except Exception:
pass # Non-blocking: sync failure should not block task action
location_ctx = { location_ctx = {
'action_latitude': latitude, 'action_latitude': latitude,
'action_longitude': longitude, 'action_longitude': longitude,
@@ -1870,6 +1877,25 @@ class AuthorizerPortal(CustomerPortal):
_logger.warning(f"Location log error: {e}") _logger.warning(f"Location log error: {e}")
return {'success': False} return {'success': False}
@http.route('/my/technician/clock-status', type='json', auth='user', website=True)
def technician_clock_status(self, **kw):
"""Check if the current technician is clocked in.
Returns {clocked_in: bool} so the JS background logger can decide
whether to track location. Replaces the fixed 9-6 hour window.
"""
if not self._check_technician_access():
return {'clocked_in': False}
try:
emp = request.env['hr.employee'].sudo().search([
('user_id', '=', request.env.user.id),
], limit=1)
if emp and emp.attendance_state == 'checked_in':
return {'clocked_in': True}
except Exception:
pass
return {'clocked_in': False}
@http.route('/my/technician/settings/start-location', type='json', auth='user', website=True) @http.route('/my/technician/settings/start-location', type='json', auth='user', website=True)
def technician_save_start_location(self, address='', **kw): def technician_save_start_location(self, address='', **kw):
"""Save the technician's personal start location.""" """Save the technician's personal start location."""

View File

@@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import base64
import json
import logging
from odoo import http, fields, _
from odoo.http import request
_logger = logging.getLogger(__name__)
class Page11PublicSignController(http.Controller):
def _get_sign_request(self, token):
"""Look up and validate a signing request by token."""
req = request.env['fusion.page11.sign.request'].sudo().search([
('access_token', '=', token),
], limit=1)
if not req:
return None, 'not_found'
if req.state == 'signed':
return req, 'already_signed'
if req.state == 'cancelled':
return req, 'cancelled'
if req.state == 'expired' or (
req.expiry_date and req.expiry_date < fields.Datetime.now()
):
if req.state != 'expired':
req.state = 'expired'
return req, 'expired'
return req, 'ok'
@http.route('/page11/sign/<string:token>', type='http', auth='public',
website=True, sitemap=False)
def page11_sign_form(self, token, **kw):
"""Display the Page 11 signing form."""
sign_req, status = self._get_sign_request(token)
if status == 'not_found':
return request.render(
'fusion_authorizer_portal.portal_page11_sign_invalid', {}
)
if status in ('expired', 'cancelled'):
return request.render(
'fusion_authorizer_portal.portal_page11_sign_expired',
{'sign_request': sign_req},
)
if status == 'already_signed':
return request.render(
'fusion_authorizer_portal.portal_page11_sign_success',
{'sign_request': sign_req, 'token': token},
)
order = sign_req.sale_order_id
partner = order.partner_id
assessment = request.env['fusion.assessment'].sudo().search([
('sale_order_id', '=', order.id),
], limit=1, order='create_date desc')
ICP = request.env['ir.config_parameter'].sudo()
google_maps_api_key = ICP.get_param('fusion_claims.google_maps_api_key', '')
client_first_name = ''
client_last_name = ''
client_middle_name = ''
client_health_card = ''
client_health_card_version = ''
if assessment:
client_first_name = assessment.client_first_name or ''
client_last_name = assessment.client_last_name or ''
client_middle_name = assessment.client_middle_name or ''
client_health_card = assessment.client_health_card or ''
client_health_card_version = assessment.client_health_card_version or ''
else:
first, last = order._get_client_name_parts()
client_first_name = first
client_last_name = last
values = {
'sign_request': sign_req,
'order': order,
'partner': partner,
'assessment': assessment,
'company': order.company_id,
'token': token,
'signer_type': sign_req.signer_type,
'is_agent': sign_req.signer_type != 'client',
'google_maps_api_key': google_maps_api_key,
'client_first_name': client_first_name,
'client_last_name': client_last_name,
'client_middle_name': client_middle_name,
'client_health_card': client_health_card,
'client_health_card_version': client_health_card_version,
}
return request.render(
'fusion_authorizer_portal.portal_page11_public_sign', values,
)
@http.route('/page11/sign/<string:token>/submit', type='http',
auth='public', methods=['POST'], website=True,
csrf=True, sitemap=False)
def page11_sign_submit(self, token, **post):
"""Process the submitted Page 11 signature."""
sign_req, status = self._get_sign_request(token)
if status != 'ok':
return request.redirect(f'/page11/sign/{token}')
signature_data = post.get('signature_data', '')
if not signature_data:
return request.redirect(f'/page11/sign/{token}?error=no_signature')
if signature_data.startswith('data:image'):
signature_data = signature_data.split(',', 1)[1]
consent_accepted = post.get('consent_declaration', '') == 'on'
if not consent_accepted:
return request.redirect(f'/page11/sign/{token}?error=no_consent')
signer_name = post.get('signer_name', sign_req.signer_name or '')
chosen_signer_type = post.get('signer_type', sign_req.signer_type or 'client')
consent_signed_by = 'applicant' if chosen_signer_type == 'client' else 'agent'
signer_type_labels = {
'spouse': 'Spouse', 'parent': 'Parent',
'legal_guardian': 'Legal Guardian',
'poa': 'Power of Attorney',
'public_trustee': 'Public Trustee',
}
vals = {
'signature_data': signature_data,
'signer_name': signer_name,
'signer_type': chosen_signer_type,
'consent_declaration_accepted': True,
'consent_signed_by': consent_signed_by,
'signed_date': fields.Datetime.now(),
'state': 'signed',
'client_first_name': post.get('client_first_name', ''),
'client_last_name': post.get('client_last_name', ''),
'client_health_card': post.get('client_health_card', ''),
'client_health_card_version': post.get('client_health_card_version', ''),
}
if consent_signed_by == 'agent':
vals.update({
'agent_first_name': post.get('agent_first_name', ''),
'agent_last_name': post.get('agent_last_name', ''),
'agent_middle_initial': post.get('agent_middle_initial', ''),
'agent_phone': post.get('agent_phone', ''),
'agent_unit': post.get('agent_unit', ''),
'agent_street_number': post.get('agent_street_number', ''),
'agent_street': post.get('agent_street', ''),
'agent_city': post.get('agent_city', ''),
'agent_province': post.get('agent_province', 'Ontario'),
'agent_postal_code': post.get('agent_postal_code', ''),
'signer_relationship': signer_type_labels.get(chosen_signer_type, chosen_signer_type),
})
sign_req.sudo().write(vals)
try:
sign_req.sudo()._generate_signed_pdf()
except Exception as e:
_logger.error("PDF generation failed for sign request %s: %s", sign_req.id, e)
try:
sign_req.sudo()._update_sale_order()
except Exception as e:
_logger.error("Sale order update failed for sign request %s: %s", sign_req.id, e)
return request.render(
'fusion_authorizer_portal.portal_page11_sign_success',
{'sign_request': sign_req, 'token': token},
)
@http.route('/page11/sign/<string:token>/download', type='http',
auth='public', website=True, sitemap=False)
def page11_download_pdf(self, token, **kw):
"""Download the signed Page 11 PDF."""
sign_req = request.env['fusion.page11.sign.request'].sudo().search([
('access_token', '=', token),
('state', '=', 'signed'),
], limit=1)
if not sign_req or not sign_req.signed_pdf:
return request.redirect(f'/page11/sign/{token}')
pdf_content = base64.b64decode(sign_req.signed_pdf)
filename = sign_req.signed_pdf_filename or 'Page11_Signed.pdf'
return request.make_response(
pdf_content,
headers=[
('Content-Type', 'application/pdf'),
('Content-Disposition', f'attachment; filename="{filename}"'),
('Content-Length', str(len(pdf_content))),
],
)

View File

@@ -160,7 +160,7 @@ class ResPartner(models.Model):
if self.is_technician_portal: if self.is_technician_portal:
# Add Field Technician group # Add Field Technician group
g = self.env.ref('fusion_claims.group_field_technician', raise_if_not_found=False) g = self.env.ref('fusion_tasks.group_field_technician', raise_if_not_found=False)
if g and g not in internal_user.group_ids: if g and g not in internal_user.group_ids:
internal_user.sudo().write({'group_ids': [(4, g.id)]}) internal_user.sudo().write({'group_ids': [(4, g.id)]})
added.append('Field Technician') added.append('Field Technician')

View File

@@ -1,7 +1,7 @@
/** /**
* Technician Location Services * Technician Location Services
* *
* 1. Background logger -- logs GPS every 5 minutes during working hours. * 1. Background logger -- logs GPS every 5 minutes while the tech is clocked in.
* 2. getLocation() -- returns a Promise that resolves to {latitude, longitude, accuracy}. * 2. getLocation() -- returns a Promise that resolves to {latitude, longitude, accuracy}.
* If the user denies permission or the request times out a blocking modal is shown * If the user denies permission or the request times out a blocking modal is shown
* and the promise is rejected. * and the promise is rejected.
@@ -11,9 +11,10 @@
'use strict'; 'use strict';
var INTERVAL_MS = 5 * 60 * 1000; var INTERVAL_MS = 5 * 60 * 1000;
var STORE_OPEN_HOUR = 9; var CLOCK_CHECK_MS = 60 * 1000; // check clock status every 60s
var STORE_CLOSE_HOUR = 18;
var locationTimer = null; var locationTimer = null;
var clockCheckTimer = null;
var isClockedIn = false;
var permissionDenied = false; var permissionDenied = false;
// ===================================================================== // =====================================================================
@@ -137,21 +138,38 @@
window.openGoogleMapsNav = openGoogleMapsNav; window.openGoogleMapsNav = openGoogleMapsNav;
// ===================================================================== // =====================================================================
// BACKGROUND LOGGER // BACKGROUND LOGGER (tied to clock-in / clock-out status)
// ===================================================================== // =====================================================================
function isWorkingHours() {
var now = new Date();
var hour = now.getHours();
return hour >= STORE_OPEN_HOUR && hour < STORE_CLOSE_HOUR;
}
function isTechnicianPortal() { function isTechnicianPortal() {
return window.location.pathname.indexOf('/my/technician') !== -1; return window.location.pathname.indexOf('/my/technician') !== -1;
} }
function checkClockStatus() {
fetch('/my/technician/clock-status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: {} }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
var wasClocked = isClockedIn;
isClockedIn = !!(data.result && data.result.clocked_in);
if (isClockedIn && !wasClocked) {
// Just clocked in — start tracking immediately
startLocationTimer();
} else if (!isClockedIn && wasClocked) {
// Just clocked out — stop tracking
stopLocationTimer();
}
})
.catch(function () {
/* network error: keep current state */
});
}
function logLocation() { function logLocation() {
if (!isWorkingHours() || document.hidden || !navigator.geolocation) return; if (!isClockedIn || document.hidden || !navigator.geolocation) return;
getLocation().then(function (coords) { getLocation().then(function (coords) {
fetch('/my/technician/location/log', { fetch('/my/technician/location/log', {
@@ -181,16 +199,32 @@
}); });
} }
function startLocationTimer() {
if (locationTimer) return; // already running
logLocation(); // immediate first log
locationTimer = setInterval(logLocation, INTERVAL_MS);
}
function stopLocationTimer() {
if (locationTimer) {
clearInterval(locationTimer);
locationTimer = null;
}
}
function startLocationLogging() { function startLocationLogging() {
if (!isTechnicianPortal()) return; if (!isTechnicianPortal()) return;
logLocation();
locationTimer = setInterval(logLocation, INTERVAL_MS); // Check clock status immediately, then every 60s
checkClockStatus();
clockCheckTimer = setInterval(checkClockStatus, CLOCK_CHECK_MS);
// Pause/resume on tab visibility
document.addEventListener('visibilitychange', function () { document.addEventListener('visibilitychange', function () {
if (document.hidden) { if (document.hidden) {
if (locationTimer) { clearInterval(locationTimer); locationTimer = null; } stopLocationTimer();
} else { } else if (isClockedIn) {
logLocation(); startLocationTimer();
if (!locationTimer) { locationTimer = setInterval(logLocation, INTERVAL_MS); }
} }
}); });
} }

View File

@@ -51,19 +51,25 @@ class PDFTemplateFiller:
for page_idx in range(num_pages): for page_idx in range(num_pages):
page = original.getPage(page_idx) page = original.getPage(page_idx)
page_num = page_idx + 1 # 1-based page number page_num = page_idx + 1 # 1-based page number
page_w = float(page.mediaBox.getWidth()) mb = page.mediaBox
page_h = float(page.mediaBox.getHeight()) page_w = float(mb.getWidth())
page_h = float(mb.getHeight())
origin_x = float(mb.getLowerLeft_x())
origin_y = float(mb.getLowerLeft_y())
fields = fields_by_page.get(page_num, []) fields = fields_by_page.get(page_num, [])
if fields: if fields:
# Create a transparent overlay for this page
overlay_buf = BytesIO() overlay_buf = BytesIO()
c = canvas.Canvas(overlay_buf, pagesize=(page_w, page_h)) c = canvas.Canvas(
overlay_buf,
pagesize=(origin_x + page_w, origin_y + page_h),
)
for field in fields: for field in fields:
PDFTemplateFiller._draw_field( PDFTemplateFiller._draw_field(
c, field, context, signatures, page_w, page_h c, field, context, signatures,
page_w, page_h, origin_x, origin_y,
) )
c.save() c.save()
@@ -80,7 +86,8 @@ class PDFTemplateFiller:
return result.getvalue() return result.getvalue()
@staticmethod @staticmethod
def _draw_field(c, field, context, signatures, page_w, page_h): def _draw_field(c, field, context, signatures,
page_w, page_h, origin_x=0, origin_y=0):
"""Draw a single field onto the reportlab canvas. """Draw a single field onto the reportlab canvas.
Args: Args:
@@ -90,6 +97,8 @@ class PDFTemplateFiller:
signatures: dict of {field_key: binary} for signature fields signatures: dict of {field_key: binary} for signature fields
page_w: page width in PDF points page_w: page width in PDF points
page_h: page height in PDF points page_h: page height in PDF points
origin_x: mediaBox lower-left X (accounts for non-zero origin)
origin_y: mediaBox lower-left Y (accounts for non-zero origin)
""" """
field_key = field.get('field_key') or field.get('field_name', '') field_key = field.get('field_key') or field.get('field_name', '')
field_type = field.get('field_type', 'text') field_type = field.get('field_type', 'text')
@@ -98,11 +107,12 @@ class PDFTemplateFiller:
if not value and field_type != 'signature': if not value and field_type != 'signature':
return return
# Convert percentage positions to absolute PDF coordinates # Convert percentage positions to absolute PDF coordinates.
# pos_x/pos_y are 0.0-1.0 ratios from top-left # pos_x/pos_y are 0.0-1.0 ratios from top-left of the visible page.
# PDF coordinate system: origin at bottom-left, Y goes up # PDF coordinate system: origin at bottom-left, Y goes up.
abs_x = field['pos_x'] * page_w # origin_x/origin_y account for PDFs whose mediaBox doesn't start at (0,0).
abs_y = page_h - (field['pos_y'] * page_h) # flip Y axis abs_x = field['pos_x'] * page_w + origin_x
abs_y = (origin_y + page_h) - (field['pos_y'] * page_h)
font_name = field.get('font_name', 'Helvetica') font_name = field.get('font_name', 'Helvetica')
font_size = field.get('font_size', 10.0) font_size = field.get('font_size', 10.0)
@@ -124,10 +134,22 @@ class PDFTemplateFiller:
elif field_type == 'checkbox': elif field_type == 'checkbox':
if value: if value:
c.setFont('ZapfDingbats', font_size) # Draw a cross mark (✗) that fills the checkbox box
cb_w = field.get('width', 0.015) * page_w
cb_h = field.get('height', 0.018) * page_h cb_h = field.get('height', 0.018) * page_h
cb_y = abs_y - cb_h + (cb_h - font_size) / 2 # Inset slightly so the cross doesn't touch the box edges
c.drawString(abs_x, cb_y, '4') pad = min(cb_w, cb_h) * 0.15
x1 = abs_x + pad
y1 = abs_y - cb_h + pad
x2 = abs_x + cb_w - pad
y2 = abs_y - pad
c.saveState()
c.setStrokeColorRGB(0, 0, 0)
c.setLineWidth(1.5)
# Draw X (two diagonal lines)
c.line(x1, y1, x2, y2)
c.line(x1, y2, x2, y1)
c.restoreState()
elif field_type == 'signature': elif field_type == 'signature':
sig_data = signatures.get(field_key) sig_data = signatures.get(field_key)

View File

@@ -0,0 +1,413 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Page 11 Public Signing Form -->
<!-- ============================================================ -->
<template id="portal_page11_public_sign" name="Page 11 - Sign">
<t t-call="portal.frontend_layout">
<div class="container py-4" style="max-width:720px;">
<div class="text-center mb-4">
<t t-if="company.logo">
<img t-att-src="'/web/image/res.company/%s/logo/200x60' % company.id"
alt="Company Logo" style="max-height:60px;" class="mb-2"/>
</t>
<h3 class="mb-1">ADP Consent and Declaration</h3>
<p class="text-muted">Page 11 - Assistive Devices Program</p>
</div>
<t t-if="request.params.get('error') == 'no_signature'">
<div class="alert alert-danger">Please draw your signature before submitting.</div>
</t>
<t t-if="request.params.get('error') == 'no_consent'">
<div class="alert alert-danger">You must accept the consent declaration before signing.</div>
</t>
<!-- Consent Declaration -->
<form method="POST" t-att-action="'/page11/sign/%s/submit' % token" id="page11Form">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<!-- Applicant Information -->
<div class="card mb-3">
<div class="card-header"><strong>Applicant Information</strong></div>
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-4">
<label class="form-label">Last Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="client_last_name"
t-att-value="client_last_name or ''" required="required"/>
</div>
<div class="col-sm-4">
<label class="form-label">First Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="client_first_name"
t-att-value="client_first_name or ''" required="required"/>
</div>
<div class="col-sm-4">
<label class="form-label">Middle Name</label>
<input type="text" class="form-control" name="client_middle_name"
t-att-value="client_middle_name or ''"/>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">
<label class="form-label">Health Card Number (10 digits) <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="client_health_card"
t-att-value="client_health_card or ''" required="required"
maxlength="10" pattern="[0-9]{10}" title="10-digit health card number"
placeholder="e.g. 1234567890"/>
</div>
<div class="col-sm-3">
<label class="form-label">Version <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="client_health_card_version"
t-att-value="client_health_card_version or ''" required="required"
maxlength="2" placeholder="e.g. AB"/>
</div>
<div class="col-sm-3">
<label class="form-label">Case Ref</label>
<input type="text" class="form-control" readonly="readonly"
t-att-value="order.name"/>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header"><strong>Consent and Declaration</strong></div>
<div class="card-body">
<p class="small">
I consent to information being collected and used by the Ministry of Health and Long-Term Care,
and agents authorized by the Ministry, for the administration and enforcement of the
Assistive Devices Program. I understand this consent is voluntary and I may withdraw it
at any time. I declare that the information in this application is true and complete.
</p>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="consent_declaration"
name="consent_declaration" required="required"/>
<label class="form-check-label" for="consent_declaration">
<strong>I have read and accept the above declaration.</strong>
</label>
</div>
<div class="mb-3">
<label for="signer_type" class="form-label"><strong>I am signing as:</strong></label>
<select class="form-select" id="signer_type" name="signer_type" required="required"
onchange="toggleAgentFields()">
<option value="client" t-att-selected="signer_type == 'client' and 'selected'">Applicant (Client - Self)</option>
<option value="spouse" t-att-selected="signer_type == 'spouse' and 'selected'">Spouse</option>
<option value="parent" t-att-selected="signer_type == 'parent' and 'selected'">Parent</option>
<option value="legal_guardian" t-att-selected="signer_type == 'legal_guardian' and 'selected'">Legal Guardian</option>
<option value="poa" t-att-selected="signer_type == 'poa' and 'selected'">Power of Attorney</option>
<option value="public_trustee" t-att-selected="signer_type == 'public_trustee' and 'selected'">Public Trustee</option>
</select>
</div>
<div class="mb-3">
<label for="signer_name" class="form-label">Full Name</label>
<input type="text" class="form-control" id="signer_name" name="signer_name"
t-att-value="sign_request.signer_name or ''" required="required"/>
</div>
</div>
</div>
<!-- Agent Details (shown/hidden via JS based on signer type selection) -->
<div class="card mb-3" id="agent_details_card" t-att-style="'' if is_agent else 'display:none;'">
<div class="card-header"><strong>Agent Details</strong></div>
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-5">
<label class="form-label">Last Name</label>
<input type="text" class="form-control agent-field" name="agent_last_name"/>
</div>
<div class="col-sm-5">
<label class="form-label">First Name</label>
<input type="text" class="form-control agent-field" name="agent_first_name"/>
</div>
<div class="col-sm-2">
<label class="form-label">M.I.</label>
<input type="text" class="form-control" name="agent_middle_initial"
maxlength="2" placeholder="M"/>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-6">
<label class="form-label">Home Phone</label>
<input type="tel" class="form-control" name="agent_phone"/>
</div>
<div class="col-sm-6">
<label class="form-label">Business Phone</label>
<input type="tel" class="form-control" name="agent_business_phone"/>
</div>
</div>
<div class="mb-2">
<label class="form-label">Search Address</label>
<input type="text" class="form-control" id="agent_street_search"
placeholder="Start typing an address..." autocomplete="off"/>
</div>
<div class="row mb-2">
<div class="col-sm-3">
<label class="form-label">Unit #</label>
<input type="text" class="form-control" name="agent_unit" id="agent_unit"/>
</div>
<div class="col-sm-3">
<label class="form-label">Street #</label>
<input type="text" class="form-control" name="agent_street_number" id="agent_street_number"/>
</div>
<div class="col-sm-6">
<label class="form-label">Street Name</label>
<input type="text" class="form-control" name="agent_street" id="agent_street"/>
</div>
</div>
<div class="row mb-2">
<div class="col-sm-5">
<label class="form-label">City/Town</label>
<input type="text" class="form-control" name="agent_city" id="agent_city"/>
</div>
<div class="col-sm-4">
<label class="form-label">Province</label>
<input type="text" class="form-control" name="agent_province" id="agent_province" value="Ontario"/>
</div>
<div class="col-sm-3">
<label class="form-label">Postal Code</label>
<input type="text" class="form-control" name="agent_postal_code" id="agent_postal_code"/>
</div>
</div>
</div>
</div>
<!-- Signature Pad -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Signature</strong>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSignature()">Clear</button>
</div>
<div class="card-body p-2">
<canvas id="signature-canvas" width="660" height="200"
style="border:1px dashed rgba(128,128,128,0.35);border-radius:6px;width:100%;touch-action:none;cursor:crosshair;">
</canvas>
<input type="hidden" name="signature_data" id="signature_data"/>
</div>
</div>
<div class="text-center mb-4">
<button type="submit" class="btn btn-primary btn-lg px-5" onclick="return prepareSubmit()">
Submit Signature
</button>
</div>
</form>
<p class="text-center text-muted small">
<t t-out="company.name"/> &amp;middot;
<t t-if="company.phone"><t t-out="company.phone"/> &amp;middot; </t>
<t t-if="company.email"><t t-out="company.email"/></t>
</p>
</div>
<script type="text/javascript">
function toggleAgentFields() {
var sel = document.getElementById('signer_type');
var card = document.getElementById('agent_details_card');
var agentFields = card ? card.querySelectorAll('.agent-field') : [];
var isAgent = sel.value !== 'client';
if (card) card.style.display = isAgent ? '' : 'none';
agentFields.forEach(function(f) {
if (isAgent) { f.setAttribute('required', 'required'); }
else { f.removeAttribute('required'); f.value = ''; }
});
}
document.addEventListener('DOMContentLoaded', toggleAgentFields);
</script>
<script type="text/javascript">
(function() {
var canvas = document.getElementById('signature-canvas');
if (!canvas) return;
var ctx = canvas.getContext('2d');
var drawing = false;
var lastX = 0, lastY = 0;
var hasDrawn = false;
function resizeCanvas() {
var rect = canvas.getBoundingClientRect();
var dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
}
resizeCanvas();
function getPos(e) {
var rect = canvas.getBoundingClientRect();
var touch = e.touches ? e.touches[0] : e;
return {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top
};
}
function startDraw(e) {
e.preventDefault();
drawing = true;
var pos = getPos(e);
lastX = pos.x;
lastY = pos.y;
}
function draw(e) {
if (!drawing) return;
e.preventDefault();
var pos = getPos(e);
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
lastX = pos.x;
lastY = pos.y;
hasDrawn = true;
}
function stopDraw(e) {
if (e) e.preventDefault();
drawing = false;
}
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDraw);
canvas.addEventListener('mouseleave', stopDraw);
canvas.addEventListener('touchstart', startDraw, {passive: false});
canvas.addEventListener('touchmove', draw, {passive: false});
canvas.addEventListener('touchend', stopDraw, {passive: false});
window.clearSignature = function() {
var dpr = window.devicePixelRatio || 1;
ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
hasDrawn = false;
document.getElementById('signature_data').value = '';
};
window.prepareSubmit = function() {
if (!hasDrawn) {
alert('Please draw your signature before submitting.');
return false;
}
var dataUrl = canvas.toDataURL('image/png');
document.getElementById('signature_data').value = dataUrl;
return true;
};
})();
</script>
<t t-if="google_maps_api_key">
<script t-attf-src="https://maps.googleapis.com/maps/api/js?key=#{google_maps_api_key}&amp;libraries=places&amp;callback=initPage11AddressAutocomplete" async="async" defer="defer"></script>
<script type="text/javascript">
function initPage11AddressAutocomplete() {
var searchInput = document.getElementById('agent_street_search');
if (!searchInput) return;
var autocomplete = new google.maps.places.Autocomplete(searchInput, {
types: ['address'],
componentRestrictions: { country: 'ca' }
});
autocomplete.setFields(['address_components', 'formatted_address']);
autocomplete.addListener('place_changed', function() {
var place = autocomplete.getPlace();
if (!place || !place.address_components) return;
var street_number = '', route = '', city = '', province = '', postal = '', unit = '';
place.address_components.forEach(function(c) {
var t = c.types;
if (t.indexOf('street_number') >= 0) street_number = c.long_name;
else if (t.indexOf('route') >= 0) route = c.long_name;
else if (t.indexOf('locality') >= 0) city = c.long_name;
else if (t.indexOf('sublocality') >= 0 &amp;&amp; !city) city = c.long_name;
else if (t.indexOf('administrative_area_level_1') >= 0) province = c.long_name;
else if (t.indexOf('postal_code') >= 0) postal = c.long_name;
else if (t.indexOf('subpremise') >= 0) unit = c.long_name;
});
document.getElementById('agent_street_number').value = street_number;
document.getElementById('agent_street').value = route;
document.getElementById('agent_city').value = city;
document.getElementById('agent_province').value = province;
document.getElementById('agent_postal_code').value = postal;
if (unit) document.getElementById('agent_unit').value = unit;
});
}
</script>
</t>
</t>
</template>
<!-- ============================================================ -->
<!-- Success Page -->
<!-- ============================================================ -->
<template id="portal_page11_sign_success" name="Page 11 - Signed Successfully">
<t t-call="portal.frontend_layout">
<div class="container py-5" style="max-width:600px;">
<div class="text-center">
<div class="mb-4">
<i class="fa fa-check-circle text-success" style="font-size:64px;"/>
</div>
<h3>Signature Submitted Successfully</h3>
<p class="text-muted mt-3">
Thank you for signing the ADP Consent and Declaration form.
Your signature has been recorded and the document has been updated.
</p>
<t t-if="sign_request and sign_request.sale_order_id">
<p class="text-muted">
Case Reference: <strong><t t-out="sign_request.sale_order_id.name"/></strong>
</p>
</t>
<t t-if="sign_request and sign_request.signed_pdf and token">
<a t-attf-href="/page11/sign/#{token}/download"
class="btn btn-outline-primary mt-3">
<i class="fa fa-download"/> Download Signed PDF
</a>
</t>
<p class="text-muted mt-4 small">You may close this window.</p>
</div>
</div>
</t>
</template>
<!-- ============================================================ -->
<!-- Expired / Cancelled Page -->
<!-- ============================================================ -->
<template id="portal_page11_sign_expired" name="Page 11 - Link Expired">
<t t-call="portal.frontend_layout">
<div class="container py-5" style="max-width:600px;">
<div class="text-center">
<div class="mb-4">
<i class="fa fa-clock-o text-warning" style="font-size:64px;"/>
</div>
<h3>Signing Link Expired</h3>
<p class="text-muted mt-3">
This signing link is no longer valid. It may have expired or been cancelled.
</p>
<p class="text-muted">
Please contact the office to request a new signing link.
</p>
</div>
</div>
</t>
</template>
<!-- ============================================================ -->
<!-- Invalid / Not Found Page -->
<!-- ============================================================ -->
<template id="portal_page11_sign_invalid" name="Page 11 - Invalid Link">
<t t-call="portal.frontend_layout">
<div class="container py-5" style="max-width:600px;">
<div class="text-center">
<div class="mb-4">
<i class="fa fa-exclamation-triangle text-danger" style="font-size:64px;"/>
</div>
<h3>Invalid Link</h3>
<p class="text-muted mt-3">
This signing link is not valid. Please check that you are using the correct link
from the email you received.
</p>
</div>
</div>
</t>
</template>
</odoo>

View File

@@ -1594,7 +1594,7 @@
<div class="container-fluid py-3"> <div class="container-fluid py-3">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h3><i class="fa fa-map-marker"/> Technician Locations</h3> <h3><i class="fa fa-map-marker"/> Technician Locations</h3>
<a href="/web#action=fusion_claims.action_technician_locations" class="btn btn-secondary btn-sm"> <a href="/web#action=fusion_tasks.action_technician_locations" class="btn btn-secondary btn-sm">
<i class="fa fa-list"/> View History <i class="fa fa-list"/> View History
</a> </a>
</div> </div>

View File

@@ -84,6 +84,7 @@
'calendar', 'calendar',
'ai', 'ai',
'fusion_ringcentral', 'fusion_ringcentral',
'fusion_tasks',
], ],
'external_dependencies': { 'external_dependencies': {
'python': ['pdf2image', 'PIL'], 'python': ['pdf2image', 'PIL'],
@@ -128,6 +129,7 @@
'wizard/odsp_pre_approved_wizard_views.xml', 'wizard/odsp_pre_approved_wizard_views.xml',
'wizard/odsp_ready_delivery_wizard_views.xml', 'wizard/odsp_ready_delivery_wizard_views.xml',
'wizard/ltc_repair_create_so_wizard_views.xml', 'wizard/ltc_repair_create_so_wizard_views.xml',
'wizard/send_page11_wizard_views.xml',
'views/res_partner_views.xml', 'views/res_partner_views.xml',
'views/pdf_template_inherit_views.xml', 'views/pdf_template_inherit_views.xml',
'views/dashboard_views.xml', 'views/dashboard_views.xml',
@@ -140,9 +142,8 @@
'views/adp_claims_views.xml', 'views/adp_claims_views.xml',
'views/submission_history_views.xml', 'views/submission_history_views.xml',
'views/fusion_loaner_views.xml', 'views/fusion_loaner_views.xml',
'views/page11_sign_request_views.xml',
'views/technician_task_views.xml', 'views/technician_task_views.xml',
'views/task_sync_views.xml',
'views/technician_location_views.xml',
'report/report_actions.xml', 'report/report_actions.xml',
'report/report_templates.xml', 'report/report_templates.xml',
'report/sale_report_portrait.xml', 'report/sale_report_portrait.xml',
@@ -168,7 +169,6 @@
'assets': { 'assets': {
'web.assets_backend': [ 'web.assets_backend': [
'fusion_claims/static/src/scss/fusion_claims.scss', 'fusion_claims/static/src/scss/fusion_claims.scss',
'fusion_claims/static/src/css/fusion_task_map_view.scss',
'fusion_claims/static/src/js/chatter_resize.js', 'fusion_claims/static/src/js/chatter_resize.js',
'fusion_claims/static/src/js/document_preview.js', 'fusion_claims/static/src/js/document_preview.js',
'fusion_claims/static/src/js/preview_button_widget.js', 'fusion_claims/static/src/js/preview_button_widget.js',
@@ -177,11 +177,9 @@
'fusion_claims/static/src/js/tax_totals_patch.js', 'fusion_claims/static/src/js/tax_totals_patch.js',
'fusion_claims/static/src/js/google_address_autocomplete.js', 'fusion_claims/static/src/js/google_address_autocomplete.js',
'fusion_claims/static/src/js/calendar_store_hours.js', 'fusion_claims/static/src/js/calendar_store_hours.js',
'fusion_claims/static/src/js/fusion_task_map_view.js',
'fusion_claims/static/src/js/attachment_image_compress.js', 'fusion_claims/static/src/js/attachment_image_compress.js',
'fusion_claims/static/src/js/debug_required_fields.js', 'fusion_claims/static/src/js/debug_required_fields.js',
'fusion_claims/static/src/xml/document_preview.xml', 'fusion_claims/static/src/xml/document_preview.xml',
'fusion_claims/static/src/xml/fusion_task_map_view.xml',
], ],
}, },
'images': ['static/description/icon.png'], 'images': ['static/description/icon.png'],

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Server Action: Sync ADP Fields to Invoices --> <data/>
<!-- This appears in the Action menu on Sale Orders -->
<record id="action_sync_adp_fields_server" model="ir.actions.server">
<field name="name">Sync to Invoices</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="binding_view_types">form,list</field>
<field name="state">code</field>
<field name="code">
if records:
# Filter to only ADP sales
adp_records = records.filtered(lambda r: r.x_fc_is_adp_sale and r.state == 'sale')
if adp_records:
action = adp_records.action_sync_adp_fields()
else:
action = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'No ADP Sales',
'message': 'Selected orders are not confirmed ADP sales.',
'type': 'warning',
'sticky': False,
}
}
</field>
</record>
</odoo> </odoo>

View File

@@ -45,26 +45,6 @@
<field name="value">True</field> <field name="value">True</field>
</record> </record>
<!-- Technician / Field Service -->
<record id="config_store_open_hour" model="ir.config_parameter">
<field name="key">fusion_claims.store_open_hour</field>
<field name="value">9.0</field>
</record>
<record id="config_store_close_hour" model="ir.config_parameter">
<field name="key">fusion_claims.store_close_hour</field>
<field name="value">18.0</field>
</record>
<!-- Push Notifications -->
<record id="config_push_enabled" model="ir.config_parameter">
<field name="key">fusion_claims.push_enabled</field>
<field name="value">False</field>
</record>
<record id="config_push_advance_minutes" model="ir.config_parameter">
<field name="key">fusion_claims.push_advance_minutes</field>
<field name="value">30</field>
</record>
<!-- Field Mappings (defaults for fresh installs) --> <!-- Field Mappings (defaults for fresh installs) -->
<record id="config_field_sale_type" model="ir.config_parameter"> <record id="config_field_sale_type" model="ir.config_parameter">
<field name="key">fusion_claims.field_sale_type</field> <field name="key">fusion_claims.field_sale_type</field>
@@ -147,12 +127,6 @@
<field name="value">1-888-222-5099</field> <field name="value">1-888-222-5099</field>
</record> </record>
<!-- Cross-instance task sync: unique ID for this Odoo instance -->
<record id="config_sync_instance_id" model="ir.config_parameter">
<field name="key">fusion_claims.sync_instance_id</field>
<field name="value"></field>
</record>
<!-- LTC Portal Form Password --> <!-- LTC Portal Form Password -->
<record id="config_ltc_form_password" model="ir.config_parameter"> <record id="config_ltc_form_password" model="ir.config_parameter">
<field name="key">fusion_claims.ltc_form_password</field> <field name="key">fusion_claims.ltc_form_password</field>

View File

@@ -6,16 +6,6 @@
--> -->
<odoo> <odoo>
<data> <data>
<!-- Cron Job: Sync ADP Fields from Sale Orders to Invoices -->
<record id="ir_cron_sync_adp_fields" model="ir.cron">
<field name="name">Fusion Claims: Sync ADP Fields</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="state">code</field>
<field name="code">model._cron_sync_adp_fields()</field>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
</record>
<!-- Cron Job: Renew ADP Delivery Reminders --> <!-- Cron Job: Renew ADP Delivery Reminders -->
<record id="ir_cron_renew_delivery_reminders" model="ir.cron"> <record id="ir_cron_renew_delivery_reminders" model="ir.cron">
<field name="name">Fusion Claims: Renew Delivery Reminders</field> <field name="name">Fusion Claims: Renew Delivery Reminders</field>
@@ -134,50 +124,17 @@
<field name="nextcall" eval="DateTime.now().replace(hour=10, minute=0, second=0)"/> <field name="nextcall" eval="DateTime.now().replace(hour=10, minute=0, second=0)"/>
</record> </record>
<!-- Cron Job: Calculate Travel Times for Technician Tasks --> <!-- Cron Job: Expire Old Page 11 Signing Requests -->
<record id="ir_cron_technician_travel_times" model="ir.cron"> <record id="ir_cron_expire_page11_requests" model="ir.cron">
<field name="name">Fusion Claims: Calculate Technician Travel Times</field> <field name="name">Fusion Claims: Expire Page 11 Signing Requests</field>
<field name="model_id" ref="model_fusion_technician_task"/> <field name="model_id" ref="model_fusion_page11_sign_request"/>
<field name="state">code</field> <field name="state">code</field>
<field name="code">model._cron_calculate_travel_times()</field> <field name="code">model._cron_expire_requests()</field>
<field name="interval_number">1</field> <field name="interval_number">1</field>
<field name="interval_type">days</field> <field name="interval_type">days</field>
<field name="active">True</field> <field name="active">True</field>
<field name="nextcall" eval="DateTime.now().replace(hour=5, minute=0, second=0)"/> <field name="nextcall" eval="DateTime.now().replace(hour=2, minute=0, second=0)"/>
</record> </record>
<!-- Cron Job: Send Push Notifications for Upcoming Tasks -->
<record id="ir_cron_technician_push_notifications" model="ir.cron">
<field name="name">Fusion Claims: Technician Push Notifications</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="state">code</field>
<field name="code">model._cron_send_push_notifications()</field>
<field name="interval_number">15</field>
<field name="interval_type">minutes</field>
<field name="active">True</field>
</record>
<!-- Cron Job: Pull Remote Technician Tasks (cross-instance sync) -->
<record id="ir_cron_task_sync_pull" model="ir.cron">
<field name="name">Fusion Claims: Sync Remote Tasks (Pull)</field>
<field name="model_id" ref="model_fusion_task_sync_config"/>
<field name="state">code</field>
<field name="code">model._cron_pull_remote_tasks()</field>
<field name="interval_number">2</field>
<field name="interval_type">minutes</field>
<field name="active">True</field>
</record>
<!-- Cron Job: Cleanup Old Shadow Tasks (30+ days) -->
<record id="ir_cron_task_sync_cleanup" model="ir.cron">
<field name="name">Fusion Claims: Cleanup Old Shadow Tasks</field>
<field name="model_id" ref="model_fusion_task_sync_config"/>
<field name="state">code</field>
<field name="code">model._cron_cleanup_old_shadows()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active">True</field>
<field name="nextcall" eval="DateTime.now().replace(hour=3, minute=0, second=0)"/>
</record>
</data> </data>
</odoo> </odoo>

View File

@@ -20,34 +20,34 @@
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field> <field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field> <field name="partner_to">{{ object.partner_id.id }}</field>
<field name="body_html" type="html"> <field name="body_html" type="html">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;max-width:600px;margin:0 auto;color:#2d3748;"> <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="height:4px;background-color:#2B6CB0;"></div> <div style="height:4px;background-color:#2B6CB0;"></div>
<div style="background:#ffffff;padding:32px 28px;border:1px solid #e2e8f0;border-top:none;"> <div style="padding:32px 28px;">
<p style="color:#2B6CB0;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;"> <p style="color:#2B6CB0;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;">
<t t-out="object.company_id.name"/> <t t-out="object.company_id.name"/>
</p> </p>
<h2 style="color:#1a202c;font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">ADP Quotation</h2> <h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">ADP Quotation</h2>
<p style="color:#718096;font-size:15px;line-height:1.5;margin:0 0 24px 0;"> <p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
Please find attached your quotation <strong style="color:#2d3748;"><t t-out="object.name"/></strong>. Please find attached your quotation <strong><t t-out="object.name"/></strong>.
</p> </p>
<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;"> <table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">
<tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;color:#718096;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid #e2e8f0;">Quotation Details</td></tr> <tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;opacity:0.55;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid rgba(128,128,128,0.25);">Quotation Details</td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;width:35%;">Reference</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.name"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);width:35%;">Reference</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.name"/></td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Date</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.date_order" t-options="{'widget': 'date'}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Date</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.date_order" t-options="{'widget': 'date'}"/></td></tr>
<t t-if="object.x_fc_authorizer_id"> <t t-if="object.x_fc_authorizer_id">
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Authorizer</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.x_fc_authorizer_id.name"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Authorizer</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.x_fc_authorizer_id.name"/></td></tr>
</t> </t>
<t t-if="object.x_fc_client_type == 'REG'"> <t t-if="object.x_fc_client_type == 'REG'">
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Client Portion (25%)</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.x_fc_client_portion_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Client Portion (25%)</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.x_fc_client_portion_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">ADP Portion (75%)</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.x_fc_adp_portion_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">ADP Portion (75%)</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.x_fc_adp_portion_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr>
</t> </t>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;font-weight:600;border-top:2px solid #e2e8f0;">Total</td><td style="padding:10px 14px;color:#2B6CB0;font-size:14px;font-weight:700;border-top:2px solid #e2e8f0;"><t t-out="object.amount_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;font-weight:600;border-top:2px solid rgba(128,128,128,0.25);">Total</td><td style="padding:10px 14px;color:#2B6CB0;font-size:14px;font-weight:700;border-top:2px solid rgba(128,128,128,0.25);"><t t-out="object.amount_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr>
</table> </table>
<div style="padding:10px 14px;border:1px dashed #e2e8f0;border-radius:6px;margin:0 0 24px 0;"> <div style="padding:10px 14px;border:1px dashed rgba(128,128,128,0.35);border-radius:6px;margin:0 0 24px 0;">
<p style="margin:0;font-size:13px;color:#718096;"><strong style="color:#2d3748;">Attached:</strong> ADP Quotation (PDF)</p> <p style="margin:0;font-size:13px;opacity:0.65;"><strong style="opacity:1;">Attached:</strong> ADP Quotation (PDF)</p>
</div> </div>
<div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;background:#f7fafc;"> <div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;">
<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;">Please review the attached quotation. If you have any questions or need assistance, do not hesitate to contact us.</p> <p style="margin:0;font-size:14px;line-height:1.5;">Please review the attached quotation. If you have any questions or need assistance, do not hesitate to contact us.</p>
</div> </div>
<t t-if="not is_html_empty(object.user_id.signature)" data-o-mail-quote-container="1"> <t t-if="not is_html_empty(object.user_id.signature)" data-o-mail-quote-container="1">
<div data-o-mail-quote="1">--<br data-o-mail-quote="1"/><t t-out="object.user_id.signature or ''" data-o-mail-quote="1"/></div> <div data-o-mail-quote="1">--<br data-o-mail-quote="1"/><t t-out="object.user_id.signature or ''" data-o-mail-quote="1"/></div>
@@ -70,34 +70,34 @@
<field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field> <field name="email_from">{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field> <field name="partner_to">{{ object.partner_id.id }}</field>
<field name="body_html" type="html"> <field name="body_html" type="html">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;max-width:600px;margin:0 auto;color:#2d3748;"> <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="height:4px;background-color:#38a169;"></div> <div style="height:4px;background-color:#38a169;"></div>
<div style="background:#ffffff;padding:32px 28px;border:1px solid #e2e8f0;border-top:none;"> <div style="padding:32px 28px;">
<p style="color:#38a169;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;"> <p style="color:#38a169;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;">
<t t-out="object.company_id.name"/> <t t-out="object.company_id.name"/>
</p> </p>
<h2 style="color:#1a202c;font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">Order Confirmed</h2> <h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">Order Confirmed</h2>
<p style="color:#718096;font-size:15px;line-height:1.5;margin:0 0 24px 0;"> <p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
Your ADP sales order <strong style="color:#2d3748;"><t t-out="object.name"/></strong> has been confirmed. Your ADP sales order <strong><t t-out="object.name"/></strong> has been confirmed.
</p> </p>
<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;"> <table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">
<tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;color:#718096;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid #e2e8f0;">Order Details</td></tr> <tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;opacity:0.55;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid rgba(128,128,128,0.25);">Order Details</td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;width:35%;">Reference</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.name"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);width:35%;">Reference</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.name"/></td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Date</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.date_order" t-options="{'widget': 'date'}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Date</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.date_order" t-options="{'widget': 'date'}"/></td></tr>
<t t-if="object.x_fc_authorizer_id"> <t t-if="object.x_fc_authorizer_id">
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Authorizer</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.x_fc_authorizer_id.name"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Authorizer</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.x_fc_authorizer_id.name"/></td></tr>
</t> </t>
<t t-if="object.x_fc_client_type == 'REG'"> <t t-if="object.x_fc_client_type == 'REG'">
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Client Portion (25%)</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.x_fc_client_portion_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Client Portion (25%)</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.x_fc_client_portion_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">ADP Portion (75%)</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.x_fc_adp_portion_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">ADP Portion (75%)</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.x_fc_adp_portion_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr>
</t> </t>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;font-weight:600;border-top:2px solid #e2e8f0;">Total</td><td style="padding:10px 14px;color:#38a169;font-size:14px;font-weight:700;border-top:2px solid #e2e8f0;"><t t-out="object.amount_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;font-weight:600;border-top:2px solid rgba(128,128,128,0.25);">Total</td><td style="padding:10px 14px;color:#38a169;font-size:14px;font-weight:700;border-top:2px solid rgba(128,128,128,0.25);"><t t-out="object.amount_total" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr>
</table> </table>
<div style="padding:10px 14px;border:1px dashed #e2e8f0;border-radius:6px;margin:0 0 24px 0;"> <div style="padding:10px 14px;border:1px dashed rgba(128,128,128,0.35);border-radius:6px;margin:0 0 24px 0;">
<p style="margin:0;font-size:13px;color:#718096;"><strong style="color:#2d3748;">Attached:</strong> Sales Order Confirmation (PDF)</p> <p style="margin:0;font-size:13px;opacity:0.65;"><strong style="opacity:1;">Attached:</strong> Sales Order Confirmation (PDF)</p>
</div> </div>
<div style="border-left:3px solid #38a169;padding:12px 16px;margin:0 0 24px 0;background:#f7fafc;"> <div style="border-left:3px solid #38a169;padding:12px 16px;margin:0 0 24px 0;">
<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;">Your order is being processed. We will keep you updated on the delivery status and any updates from the Assistive Devices Program.</p> <p style="margin:0;font-size:14px;line-height:1.5;">Your order is being processed. We will keep you updated on the delivery status and any updates from the Assistive Devices Program.</p>
</div> </div>
<t t-if="not is_html_empty(object.user_id.signature)" data-o-mail-quote-container="1"> <t t-if="not is_html_empty(object.user_id.signature)" data-o-mail-quote-container="1">
<div data-o-mail-quote="1">--<br data-o-mail-quote="1"/><t t-out="object.user_id.signature or ''" data-o-mail-quote="1"/></div> <div data-o-mail-quote="1">--<br data-o-mail-quote="1"/><t t-out="object.user_id.signature or ''" data-o-mail-quote="1"/></div>
@@ -120,42 +120,42 @@
<field name="email_from">{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field> <field name="email_from">{{ (object.invoice_user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field> <field name="partner_to">{{ object.partner_id.id }}</field>
<field name="body_html" type="html"> <field name="body_html" type="html">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;max-width:600px;margin:0 auto;color:#2d3748;"> <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;max-width:600px;margin:0 auto;">
<div style="height:4px;background-color:#2B6CB0;"></div> <div style="height:4px;background-color:#2B6CB0;"></div>
<div style="background:#ffffff;padding:32px 28px;border:1px solid #e2e8f0;border-top:none;"> <div style="padding:32px 28px;">
<p style="color:#2B6CB0;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;"> <p style="color:#2B6CB0;font-size:13px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;margin:0 0 24px 0;">
<t t-out="object.company_id.name"/> <t t-out="object.company_id.name"/>
</p> </p>
<h2 style="color:#1a202c;font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">Invoice</h2> <h2 style="font-size:22px;font-weight:700;margin:0 0 6px 0;line-height:1.3;">Invoice</h2>
<p style="color:#718096;font-size:15px;line-height:1.5;margin:0 0 24px 0;"> <p style="opacity:0.65;font-size:15px;line-height:1.5;margin:0 0 24px 0;">
Please find attached your invoice <strong style="color:#2d3748;"><t t-out="object.name or 'Draft'"/></strong>. Please find attached your invoice <strong><t t-out="object.name or 'Draft'"/></strong>.
</p> </p>
<table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;"> <table style="width:100%;border-collapse:collapse;margin:0 0 24px 0;">
<tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;color:#718096;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid #e2e8f0;">Invoice Details</td></tr> <tr><td colspan="2" style="padding:10px 14px;font-size:12px;font-weight:600;opacity:0.55;text-transform:uppercase;letter-spacing:0.5px;border-bottom:2px solid rgba(128,128,128,0.25);">Invoice Details</td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;width:35%;">Invoice</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.name or 'Draft'"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);width:35%;">Invoice</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.name or 'Draft'"/></td></tr>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Date</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.invoice_date" t-options="{'widget': 'date'}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Date</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.invoice_date" t-options="{'widget': 'date'}"/></td></tr>
<t t-if="object.invoice_date_due"> <t t-if="object.invoice_date_due">
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Due Date</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"><t t-out="object.invoice_date_due" t-options="{'widget': 'date'}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Due Date</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);"><t t-out="object.invoice_date_due" t-options="{'widget': 'date'}"/></td></tr>
</t> </t>
<t t-if="object.x_fc_adp_invoice_portion"> <t t-if="object.x_fc_adp_invoice_portion">
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;border-bottom:1px solid #f0f0f0;">Type</td><td style="padding:10px 14px;color:#2d3748;font-size:14px;border-bottom:1px solid #f0f0f0;"> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">Type</td><td style="padding:10px 14px;font-size:14px;border-bottom:1px solid rgba(128,128,128,0.15);">
<t t-if="object.x_fc_adp_invoice_portion == 'client'">Client Portion</t> <t t-if="object.x_fc_adp_invoice_portion == 'client'">Client Portion</t>
<t t-if="object.x_fc_adp_invoice_portion == 'adp'">ADP Portion</t> <t t-if="object.x_fc_adp_invoice_portion == 'adp'">ADP Portion</t>
</td></tr> </td></tr>
</t> </t>
<tr><td style="padding:10px 14px;color:#718096;font-size:14px;font-weight:600;border-top:2px solid #e2e8f0;">Amount Due</td><td style="padding:10px 14px;color:#2B6CB0;font-size:14px;font-weight:700;border-top:2px solid #e2e8f0;"><t t-out="object.amount_residual" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr> <tr><td style="padding:10px 14px;opacity:0.6;font-size:14px;font-weight:600;border-top:2px solid rgba(128,128,128,0.25);">Amount Due</td><td style="padding:10px 14px;color:#2B6CB0;font-size:14px;font-weight:700;border-top:2px solid rgba(128,128,128,0.25);"><t t-out="object.amount_residual" t-options="{'widget': 'monetary', 'display_currency': object.currency_id}"/></td></tr>
</table> </table>
<div style="padding:10px 14px;border:1px dashed #e2e8f0;border-radius:6px;margin:0 0 24px 0;"> <div style="padding:10px 14px;border:1px dashed rgba(128,128,128,0.35);border-radius:6px;margin:0 0 24px 0;">
<p style="margin:0;font-size:13px;color:#718096;"><strong style="color:#2d3748;">Attached:</strong> Invoice (PDF)</p> <p style="margin:0;font-size:13px;opacity:0.65;"><strong style="opacity:1;">Attached:</strong> Invoice (PDF)</p>
</div> </div>
<t t-if="object.x_fc_adp_invoice_portion == 'client'"> <t t-if="object.x_fc_adp_invoice_portion == 'client'">
<div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;background:#f7fafc;"> <div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;">
<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;">This invoice represents your client portion for the ADP-funded equipment. The remaining amount will be billed directly to the Assistive Devices Program.</p> <p style="margin:0;font-size:14px;line-height:1.5;">This invoice represents your client portion for the ADP-funded equipment. The remaining amount will be billed directly to the Assistive Devices Program.</p>
</div> </div>
</t> </t>
<t t-else=""> <t t-else="">
<div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;background:#f7fafc;"> <div style="border-left:3px solid #2B6CB0;padding:12px 16px;margin:0 0 24px 0;">
<p style="margin:0;font-size:14px;line-height:1.5;color:#2d3748;">Please review the attached invoice and process payment at your earliest convenience. Contact us if you have any questions.</p> <p style="margin:0;font-size:14px;line-height:1.5;">Please review the attached invoice and process payment at your earliest convenience. Contact us if you have any questions.</p>
</div> </div>
</t> </t>
<t t-set="sig" t-value="object.invoice_user_id.signature or object.user_id.signature"/> <t t-set="sig" t-value="object.invoice_user_id.signature or object.user_id.signature"/>

View File

@@ -3,7 +3,6 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Claim Assistant product family. # Part of the Fusion Claim Assistant product family.
from . import email_builder_mixin
from . import adp_posting_schedule from . import adp_posting_schedule
from . import res_company from . import res_company
from . import res_config_settings from . import res_config_settings
@@ -27,12 +26,9 @@ from . import client_chat
from . import ai_agent_ext from . import ai_agent_ext
from . import dashboard from . import dashboard
from . import res_partner from . import res_partner
from . import res_users
from . import technician_task from . import technician_task
from . import task_sync
from . import technician_location
from . import push_subscription
from . import ltc_facility from . import ltc_facility
from . import ltc_repair from . import ltc_repair
from . import ltc_cleanup from . import ltc_cleanup
from . import ltc_form_submission from . import ltc_form_submission
from . import page11_sign_request

View File

@@ -57,6 +57,12 @@ class FusionADPDeviceCode(models.Model):
index=True, index=True,
help='Device manufacturer', help='Device manufacturer',
) )
build_type = fields.Selection(
[('modular', 'Modular'), ('custom_fabricated', 'Custom Fabricated')],
string='Build Type',
index=True,
help='Build type for positioning/seating devices: Modular or Custom Fabricated',
)
device_description = fields.Char( device_description = fields.Char(
string='Device Description', string='Device Description',
help='Detailed device description from mobility manual', help='Detailed device description from mobility manual',
@@ -242,6 +248,16 @@ class FusionADPDeviceCode(models.Model):
device_type = self._clean_text(item.get('Device Type', '') or item.get('device_type', '')) device_type = self._clean_text(item.get('Device Type', '') or item.get('device_type', ''))
manufacturer = self._clean_text(item.get('Manufacturer', '') or item.get('manufacturer', '')) manufacturer = self._clean_text(item.get('Manufacturer', '') or item.get('manufacturer', ''))
device_description = self._clean_text(item.get('Device Description', '') or item.get('device_description', '')) device_description = self._clean_text(item.get('Device Description', '') or item.get('device_description', ''))
# Parse build type (Modular / Custom Fabricated)
build_type_raw = self._clean_text(item.get('Build Type', '') or item.get('build_type', ''))
build_type = False
if build_type_raw:
bt_lower = build_type_raw.lower().strip()
if bt_lower in ('modular', 'mod'):
build_type = 'modular'
elif bt_lower in ('custom fabricated', 'custom_fabricated', 'custom'):
build_type = 'custom_fabricated'
# Parse quantity # Parse quantity
qty_val = item.get('Quantity', 1) or item.get('Qty', 1) or item.get('quantity', 1) qty_val = item.get('Quantity', 1) or item.get('Qty', 1) or item.get('quantity', 1)
@@ -277,6 +293,8 @@ class FusionADPDeviceCode(models.Model):
'last_updated': fields.Datetime.now(), 'last_updated': fields.Datetime.now(),
'active': True, 'active': True,
} }
if build_type:
vals['build_type'] = build_type
if existing: if existing:
existing.write(vals) existing.write(vals)

View File

@@ -0,0 +1,389 @@
# -*- coding: utf-8 -*-
# Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
import base64
import logging
import uuid
from datetime import timedelta
from markupsafe import Markup
from odoo import models, fields, api, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
SIGNER_TYPE_SELECTION = [
('client', 'Client (Self)'),
('spouse', 'Spouse'),
('parent', 'Parent'),
('legal_guardian', 'Legal Guardian'),
('poa', 'Power of Attorney'),
('public_trustee', 'Public Trustee'),
]
SIGNER_TYPE_TO_RELATIONSHIP = {
'spouse': 'Spouse',
'parent': 'Parent',
'legal_guardian': 'Legal Guardian',
'poa': 'Power of Attorney',
'public_trustee': 'Public Trustee',
}
class Page11SignRequest(models.Model):
_name = 'fusion.page11.sign.request'
_description = 'ADP Page 11 Remote Signing Request'
_inherit = ['fusion.email.builder.mixin']
_order = 'create_date desc'
sale_order_id = fields.Many2one(
'sale.order', string='Sale Order',
required=True, ondelete='cascade', index=True,
)
access_token = fields.Char(
string='Access Token', required=True, copy=False,
default=lambda self: str(uuid.uuid4()), index=True,
)
state = fields.Selection([
('draft', 'Draft'),
('sent', 'Sent'),
('signed', 'Signed'),
('expired', 'Expired'),
('cancelled', 'Cancelled'),
], string='Status', default='draft', required=True, tracking=True)
signer_email = fields.Char(string='Recipient Email', required=True)
signer_type = fields.Selection(
SIGNER_TYPE_SELECTION, string='Signer Type',
default='client', required=True,
)
signer_name = fields.Char(string='Signer Name')
signer_relationship = fields.Char(string='Relationship to Client')
signature_data = fields.Binary(string='Signature', attachment=True)
signed_pdf = fields.Binary(string='Signed PDF', attachment=True)
signed_pdf_filename = fields.Char(string='Signed PDF Filename')
signed_date = fields.Datetime(string='Signed Date')
sent_date = fields.Datetime(string='Sent Date')
expiry_date = fields.Datetime(string='Expiry Date')
consent_declaration_accepted = fields.Boolean(string='Declaration Accepted')
consent_signed_by = fields.Selection([
('applicant', 'Applicant'),
('agent', 'Agent'),
], string='Signed By')
client_first_name = fields.Char(string='Client First Name')
client_last_name = fields.Char(string='Client Last Name')
client_health_card = fields.Char(string='Health Card Number')
client_health_card_version = fields.Char(string='Health Card Version')
agent_first_name = fields.Char(string='Agent First Name')
agent_last_name = fields.Char(string='Agent Last Name')
agent_middle_initial = fields.Char(string='Agent Middle Initial')
agent_phone = fields.Char(string='Agent Phone')
agent_unit = fields.Char(string='Agent Unit Number')
agent_street_number = fields.Char(string='Agent Street Number')
agent_street = fields.Char(string='Agent Street Name')
agent_city = fields.Char(string='Agent City')
agent_province = fields.Char(string='Agent Province', default='Ontario')
agent_postal_code = fields.Char(string='Agent Postal Code')
custom_message = fields.Text(string='Custom Message')
company_id = fields.Many2one(
'res.company', string='Company',
related='sale_order_id.company_id', store=True,
)
def name_get(self):
return [
(r.id, f"Page 11 - {r.sale_order_id.name} ({r.state})")
for r in self
]
def _send_signing_email(self):
"""Build and send the signing request email."""
self.ensure_one()
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
sign_url = f'{base_url}/page11/sign/{self.access_token}'
order = self.sale_order_id
client_name = order.partner_id.name or 'N/A'
sections = [
('Case Details', [
('Client', client_name),
('Case Reference', order.name),
]),
]
if order.x_fc_authorizer_id:
sections[0][1].append(('Authorizer', order.x_fc_authorizer_id.name))
if order.x_fc_assessment_start_date:
sections[0][1].append((
'Assessment Date',
order.x_fc_assessment_start_date.strftime('%B %d, %Y'),
))
note_parts = []
if self.custom_message:
note_parts.append(self.custom_message)
days_left = 7
if self.expiry_date:
delta = self.expiry_date - fields.Datetime.now()
days_left = max(1, delta.days)
note_parts.append(
f'This link will expire in {days_left} days. '
'Please complete the signing at your earliest convenience.'
)
note_text = '<br/><br/>'.join(note_parts)
body_html = self._email_build(
title='Page 11 Signature Required',
summary=(
f'{order.company_id.name} requires your signature on the '
f'ADP Consent and Declaration form for <strong>{client_name}</strong>.'
),
sections=sections,
note=note_text,
email_type='info',
button_url=sign_url,
button_text='Sign Now',
sender_name=self.env.user.name,
)
mail_values = {
'subject': f'{order.company_id.name} - Page 11 Signature Required ({order.name})',
'body_html': body_html,
'email_to': self.signer_email,
'email_from': (
self.env.user.email_formatted
or order.company_id.email_formatted
),
'auto_delete': True,
}
mail = self.env['mail.mail'].sudo().create(mail_values)
mail.send()
self.write({
'state': 'sent',
'sent_date': fields.Datetime.now(),
})
signer_display = self.signer_name or self.signer_email
order.message_post(
body=Markup(
'Page 11 signing request sent to <strong>%s</strong> (%s).'
) % (signer_display, self.signer_email),
message_type='notification',
subtype_xmlid='mail.mt_note',
)
def _generate_signed_pdf(self):
"""Generate the signed Page 11 PDF using the PDF template engine."""
self.ensure_one()
order = self.sale_order_id
assessment = self.env['fusion.assessment'].search([
('sale_order_id', '=', order.id),
], limit=1, order='create_date desc')
if assessment:
ctx = assessment._get_pdf_context()
else:
ctx = self._build_pdf_context_from_order()
if self.client_first_name:
ctx['client_first_name'] = self.client_first_name
if self.client_last_name:
ctx['client_last_name'] = self.client_last_name
if self.client_health_card:
ctx['client_health_card'] = self.client_health_card
if self.client_health_card_version:
ctx['client_health_card_version'] = self.client_health_card_version
ctx.update({
'consent_signed_by': self.consent_signed_by or '',
'consent_applicant': self.consent_signed_by == 'applicant',
'consent_agent': self.consent_signed_by == 'agent',
'consent_declaration_accepted': self.consent_declaration_accepted,
'consent_date': str(fields.Date.today()),
})
if self.consent_signed_by == 'agent':
ctx.update({
'agent_first_name': self.agent_first_name or '',
'agent_last_name': self.agent_last_name or '',
'agent_middle_initial': self.agent_middle_initial or '',
'agent_unit': self.agent_unit or '',
'agent_street_number': self.agent_street_number or '',
'agent_street_name': self.agent_street or '',
'agent_city': self.agent_city or '',
'agent_province': self.agent_province or '',
'agent_postal_code': self.agent_postal_code or '',
'agent_home_phone': self.agent_phone or '',
'agent_relationship': self.signer_relationship or '',
'agent_rel_spouse': self.signer_type == 'spouse',
'agent_rel_parent': self.signer_type == 'parent',
'agent_rel_poa': self.signer_type == 'poa',
'agent_rel_guardian': self.signer_type in ('legal_guardian', 'public_trustee'),
})
signatures = {}
if self.signature_data:
signatures['signature_page_11'] = base64.b64decode(self.signature_data)
template = self.env['fusion.pdf.template'].search([
('state', '=', 'active'),
('name', 'ilike', 'adp_page_11'),
], limit=1)
if not template:
template = self.env['fusion.pdf.template'].search([
('state', '=', 'active'),
('name', 'ilike', 'page 11'),
], limit=1)
if not template:
_logger.warning("No active PDF template found for Page 11")
return None
try:
pdf_bytes = template.generate_filled_pdf(ctx, signatures)
if pdf_bytes:
first, last = order._get_client_name_parts()
filename = f'{first}_{last}_Page11_Signed.pdf'
self.write({
'signed_pdf': base64.b64encode(pdf_bytes),
'signed_pdf_filename': filename,
})
return pdf_bytes
except Exception as e:
_logger.error("Failed to generate Page 11 PDF: %s", e)
return None
def _build_pdf_context_from_order(self):
"""Build a PDF context dict from the sale order when no assessment exists."""
order = self.sale_order_id
partner = order.partner_id
first, last = order._get_client_name_parts()
return {
'client_first_name': first,
'client_last_name': last,
'client_name': partner.name or '',
'client_street': partner.street or '',
'client_city': partner.city or '',
'client_state': partner.state_id.name if partner.state_id else 'Ontario',
'client_postal_code': partner.zip or '',
'client_phone': partner.phone or partner.mobile or '',
'client_email': partner.email or '',
'client_type': order.x_fc_client_type or '',
'client_type_reg': order.x_fc_client_type == 'REG',
'client_type_ods': order.x_fc_client_type == 'ODS',
'client_type_acs': order.x_fc_client_type == 'ACS',
'client_type_owp': order.x_fc_client_type == 'OWP',
'reference': order.name or '',
'authorizer_name': order.x_fc_authorizer_id.name if order.x_fc_authorizer_id else '',
'authorizer_phone': order.x_fc_authorizer_id.phone if order.x_fc_authorizer_id else '',
'authorizer_email': order.x_fc_authorizer_id.email if order.x_fc_authorizer_id else '',
'claim_authorization_date': str(order.x_fc_claim_authorization_date) if order.x_fc_claim_authorization_date else '',
'assessment_start_date': str(order.x_fc_assessment_start_date) if order.x_fc_assessment_start_date else '',
'assessment_end_date': str(order.x_fc_assessment_end_date) if order.x_fc_assessment_end_date else '',
}
def _update_sale_order(self):
"""Copy signing data from this request to the sale order."""
self.ensure_one()
order = self.sale_order_id
vals = {
'x_fc_page11_signer_type': self.signer_type,
'x_fc_page11_signer_name': self.signer_name,
'x_fc_page11_signed_date': fields.Date.today(),
}
if self.signer_type != 'client':
vals['x_fc_page11_signer_relationship'] = (
self.signer_relationship
or SIGNER_TYPE_TO_RELATIONSHIP.get(self.signer_type, '')
)
if self.signed_pdf:
vals['x_fc_signed_pages_11_12'] = self.signed_pdf
vals['x_fc_signed_pages_filename'] = self.signed_pdf_filename
order.with_context(
skip_page11_check=True,
skip_document_chatter=True,
).write(vals)
signer_display = self.signer_name or 'N/A'
if self.signed_pdf:
att = self.env['ir.attachment'].sudo().create({
'name': self.signed_pdf_filename or 'Page11_Signed.pdf',
'datas': self.signed_pdf,
'res_model': 'sale.order',
'res_id': order.id,
'mimetype': 'application/pdf',
})
order.message_post(
body=Markup(
'Page 11 has been signed by <strong>%s</strong> (%s).'
) % (signer_display, self.signer_email),
attachment_ids=[att.id],
message_type='notification',
subtype_xmlid='mail.mt_note',
)
else:
order.message_post(
body=Markup(
'Page 11 has been signed by <strong>%s</strong> (%s). '
'PDF generation was not available.'
) % (signer_display, self.signer_email),
message_type='notification',
subtype_xmlid='mail.mt_note',
)
def action_cancel(self):
"""Cancel a pending signing request."""
for rec in self:
if rec.state in ('draft', 'sent'):
rec.state = 'cancelled'
def action_resend(self):
"""Resend the signing email."""
for rec in self:
if rec.state in ('sent', 'expired'):
rec.expiry_date = fields.Datetime.now() + timedelta(days=7)
rec.access_token = str(uuid.uuid4())
rec._send_signing_email()
def action_request_new_signature(self):
"""Create a new signing request (e.g. to re-sign after corrections)."""
self.ensure_one()
if self.state == 'signed':
self.state = 'cancelled'
return {
'type': 'ir.actions.act_window',
'name': 'Request Page 11 Signature',
'res_model': 'fusion_claims.send.page11.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_sale_order_id': self.sale_order_id.id,
'default_signer_email': self.signer_email,
'default_signer_name': self.signer_name,
'default_signer_type': self.signer_type,
},
}
@api.model
def _cron_expire_requests(self):
"""Mark expired unsigned requests."""
expired = self.search([
('state', '=', 'sent'),
('expiry_date', '<', fields.Datetime.now()),
])
if expired:
expired.write({'state': 'expired'})
_logger.info("Expired %d Page 11 signing requests", len(expired))

View File

@@ -317,16 +317,6 @@ class ResConfigSettings(models.TransientModel):
help='The user who signs Page 12 on behalf of the company', help='The user who signs Page 12 on behalf of the company',
) )
# =========================================================================
# GOOGLE MAPS API SETTINGS
# =========================================================================
fc_google_maps_api_key = fields.Char(
string='Google Maps API Key',
config_parameter='fusion_claims.google_maps_api_key',
help='API key for Google Maps Places autocomplete in address fields',
)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# AI CLIENT INTELLIGENCE # AI CLIENT INTELLIGENCE
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -349,62 +339,6 @@ class ResConfigSettings(models.TransientModel):
help='Automatically parse ADP XML files when uploaded and create/update client profiles', help='Automatically parse ADP XML files when uploaded and create/update client profiles',
) )
# ------------------------------------------------------------------
# TECHNICIAN MANAGEMENT
# ------------------------------------------------------------------
fc_store_open_hour = fields.Float(
string='Store Open Time',
config_parameter='fusion_claims.store_open_hour',
help='Store opening time for technician scheduling (e.g. 9.0 = 9:00 AM)',
)
fc_store_close_hour = fields.Float(
string='Store Close Time',
config_parameter='fusion_claims.store_close_hour',
help='Store closing time for technician scheduling (e.g. 18.0 = 6:00 PM)',
)
fc_google_distance_matrix_enabled = fields.Boolean(
string='Enable Distance Matrix',
config_parameter='fusion_claims.google_distance_matrix_enabled',
help='Enable Google Distance Matrix API for travel time calculations between technician tasks',
)
fc_technician_start_address = fields.Char(
string='Technician Start Address',
config_parameter='fusion_claims.technician_start_address',
help='Default start location for technician travel calculations (e.g. warehouse/office address)',
)
fc_location_retention_days = fields.Char(
string='Location History Retention (Days)',
config_parameter='fusion_claims.location_retention_days',
help='How many days to keep technician location history. '
'Leave empty = 30 days (1 month). '
'0 = delete at end of each day. '
'1+ = keep for that many days.',
)
# ------------------------------------------------------------------
# WEB PUSH NOTIFICATIONS
# ------------------------------------------------------------------
fc_push_enabled = fields.Boolean(
string='Enable Push Notifications',
config_parameter='fusion_claims.push_enabled',
help='Enable web push notifications for technician tasks',
)
fc_vapid_public_key = fields.Char(
string='VAPID Public Key',
config_parameter='fusion_claims.vapid_public_key',
help='Public key for Web Push VAPID authentication (auto-generated)',
)
fc_vapid_private_key = fields.Char(
string='VAPID Private Key',
config_parameter='fusion_claims.vapid_private_key',
help='Private key for Web Push VAPID authentication (auto-generated)',
)
fc_push_advance_minutes = fields.Integer(
string='Notification Advance (min)',
config_parameter='fusion_claims.push_advance_minutes',
help='Send push notifications this many minutes before a scheduled task',
)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# TWILIO SMS SETTINGS # TWILIO SMS SETTINGS
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -609,15 +543,11 @@ class ResConfigSettings(models.TransientModel):
# an existing non-empty value (e.g. API keys, user-customized settings). # an existing non-empty value (e.g. API keys, user-customized settings).
_protected_keys = [ _protected_keys = [
'fusion_claims.ai_api_key', 'fusion_claims.ai_api_key',
'fusion_claims.google_maps_api_key',
'fusion_claims.vendor_code', 'fusion_claims.vendor_code',
'fusion_claims.ai_model', 'fusion_claims.ai_model',
'fusion_claims.adp_posting_base_date', 'fusion_claims.adp_posting_base_date',
'fusion_claims.application_reminder_days', 'fusion_claims.application_reminder_days',
'fusion_claims.application_reminder_2_days', 'fusion_claims.application_reminder_2_days',
'fusion_claims.store_open_hour',
'fusion_claims.store_close_hour',
'fusion_claims.technician_start_address',
] ]
# Snapshot existing values BEFORE super().set_values() runs # Snapshot existing values BEFORE super().set_values() runs
_existing = {} _existing = {}

View File

@@ -2,82 +2,12 @@
# Copyright 2024-2026 Nexa Systems Inc. # Copyright 2024-2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
import logging
import requests
from odoo import models, fields, api from odoo import models, fields, api
_logger = logging.getLogger(__name__)
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
x_fc_start_address = fields.Char(
string='Start Location',
help='Technician daily start location (home, warehouse, etc.). '
'Used as origin for first travel time calculation. '
'If empty, the company default HQ address is used.',
)
x_fc_start_address_lat = fields.Float(
string='Start Latitude', digits=(10, 7),
)
x_fc_start_address_lng = fields.Float(
string='Start Longitude', digits=(10, 7),
)
def _geocode_start_address(self, address):
if not address or not address.strip():
return 0.0, 0.0
api_key = self.env['ir.config_parameter'].sudo().get_param(
'fusion_claims.google_maps_api_key', '')
if not api_key:
return 0.0, 0.0
try:
resp = requests.get(
'https://maps.googleapis.com/maps/api/geocode/json',
params={'address': address.strip(), 'key': api_key, 'region': 'ca'},
timeout=10,
)
data = resp.json()
if data.get('status') == 'OK' and data.get('results'):
loc = data['results'][0]['geometry']['location']
return loc['lat'], loc['lng']
except Exception as e:
_logger.warning("Start address geocoding failed for '%s': %s", address, e)
return 0.0, 0.0
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for rec, vals in zip(records, vals_list):
addr = vals.get('x_fc_start_address')
if addr:
lat, lng = rec._geocode_start_address(addr)
if lat and lng:
rec.write({
'x_fc_start_address_lat': lat,
'x_fc_start_address_lng': lng,
})
return records
def write(self, vals):
res = super().write(vals)
if 'x_fc_start_address' in vals:
addr = vals['x_fc_start_address']
if addr and addr.strip():
lat, lng = self._geocode_start_address(addr)
if lat and lng:
super().write({
'x_fc_start_address_lat': lat,
'x_fc_start_address_lng': lng,
})
else:
super().write({
'x_fc_start_address_lat': 0.0,
'x_fc_start_address_lng': 0.0,
})
return res
# ========================================================================== # ==========================================================================
# CONTACT TYPE # CONTACT TYPE
# ========================================================================== # ==========================================================================

View File

@@ -1862,6 +1862,10 @@ class SaleOrder(models.Model):
string='Previous Status Before Hold', string='Previous Status Before Hold',
help='Status before the application was put on hold (for resuming)', help='Status before the application was put on hold (for resuming)',
) )
x_fc_previous_status_before_withdrawal = fields.Char(
string='Status Before Withdrawal',
help='Records the status before withdrawal for audit trail.',
)
x_fc_status_before_delivery = fields.Char( x_fc_status_before_delivery = fields.Char(
string='Status Before Delivery', string='Status Before Delivery',
@@ -2327,6 +2331,20 @@ class SaleOrder(models.Model):
help='Date when Page 11 was signed', help='Date when Page 11 was signed',
) )
page11_sign_request_ids = fields.One2many(
'fusion.page11.sign.request', 'sale_order_id',
string='Page 11 Signing Requests',
)
page11_sign_request_count = fields.Integer(
compute='_compute_page11_sign_request_count',
string='Signing Requests',
)
page11_sign_status = fields.Selection([
('none', 'Not Requested'),
('sent', 'Pending Signature'),
('signed', 'Signed'),
], compute='_compute_page11_sign_request_count', string='Page 11 Remote Status')
# ========================================================================== # ==========================================================================
# PAGE 12 SIGNATURE TRACKING (Authorizer + Vendor Signature) # PAGE 12 SIGNATURE TRACKING (Authorizer + Vendor Signature)
# Page 12 must be signed by: Authorizer (OT) and Vendor (our company) # Page 12 must be signed by: Authorizer (OT) and Vendor (our company)
@@ -3120,11 +3138,49 @@ class SaleOrder(models.Model):
self.ensure_one() self.ensure_one()
return self._action_open_document('x_fc_original_application', 'Original ADP Application') return self._action_open_document('x_fc_original_application', 'Original ADP Application')
@api.depends('page11_sign_request_ids', 'page11_sign_request_ids.state')
def _compute_page11_sign_request_count(self):
for order in self:
requests = order.page11_sign_request_ids
order.page11_sign_request_count = len(requests)
signed = requests.filtered(lambda r: r.state == 'signed')
pending = requests.filtered(lambda r: r.state == 'sent')
if signed:
order.page11_sign_status = 'signed'
elif pending:
order.page11_sign_status = 'sent'
else:
order.page11_sign_status = 'none'
def action_open_signed_pages(self): def action_open_signed_pages(self):
"""Open the Page 11 & 12 PDF.""" """Open the Page 11 & 12 PDF."""
self.ensure_one() self.ensure_one()
return self._action_open_document('x_fc_signed_pages_11_12', 'Page 11 & 12 (Signed)') return self._action_open_document('x_fc_signed_pages_11_12', 'Page 11 & 12 (Signed)')
def action_request_page11_signature(self):
"""Open the wizard to send Page 11 for remote signing."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Request Page 11 Signature',
'res_model': 'fusion_claims.send.page11.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_sale_order_id': self.id},
}
def action_view_page11_requests(self):
"""Open the list of Page 11 signing requests."""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': 'Page 11 Signing Requests',
'res_model': 'fusion.page11.sign.request',
'view_mode': 'list,form',
'domain': [('sale_order_id', '=', self.id)],
'context': {'default_sale_order_id': self.id},
}
def action_open_final_application(self): def action_open_final_application(self):
"""Open the Final Submitted Application PDF.""" """Open the Final Submitted Application PDF."""
self.ensure_one() self.ensure_one()
@@ -3686,6 +3742,41 @@ class SaleOrder(models.Model):
return True return True
def action_resubmit_from_withdrawn(self):
"""Return a withdrawn application to Ready for Submission for correction and resubmission."""
self.ensure_one()
if self.x_fc_adp_application_status != 'withdrawn':
raise UserError("This action is only available for withdrawn applications.")
self.with_context(skip_status_validation=True).write({
'x_fc_adp_application_status': 'ready_submission',
})
user_name = self.env.user.name
resubmit_date = fields.Date.today().strftime('%B %d, %Y')
message_body = f'''
<div class="alert alert-info" role="alert">
<h5 class="alert-heading"><i class="fa fa-repeat"></i> Application Returned for Resubmission</h5>
<ul>
<li><strong>Returned By:</strong> {user_name}</li>
<li><strong>Date:</strong> {resubmit_date}</li>
<li><strong>Status Returned To:</strong> Ready for Submission</li>
</ul>
<hr>
<p class="mb-0"><i class="fa fa-info-circle"></i> Make corrections and click <strong>Submit Application</strong> to resubmit.</p>
</div>
'''
self.message_post(
body=Markup(message_body),
message_type='notification',
subtype_xmlid='mail.mt_note',
)
return True
def action_set_ready_to_bill(self): def action_set_ready_to_bill(self):
"""Open the Ready to Bill wizard to collect POD and delivery date. """Open the Ready to Bill wizard to collect POD and delivery date.
@@ -4520,6 +4611,12 @@ class SaleOrder(models.Model):
if 'x_fc_device_placement' in self.env['account.move.line']._fields: if 'x_fc_device_placement' in self.env['account.move.line']._fields:
line_vals['x_fc_device_placement'] = line.x_fc_device_placement line_vals['x_fc_device_placement'] = line.x_fc_device_placement
# Copy deduction fields so export verification can recalculate correctly
if 'x_fc_deduction_type' in self.env['account.move.line']._fields:
line_vals['x_fc_deduction_type'] = line.x_fc_deduction_type or 'none'
if 'x_fc_deduction_value' in self.env['account.move.line']._fields:
line_vals['x_fc_deduction_value'] = line.x_fc_deduction_value or 0
# Store BOTH portions on invoice line (for display) # Store BOTH portions on invoice line (for display)
if 'x_fc_adp_portion' in self.env['account.move.line']._fields: if 'x_fc_adp_portion' in self.env['account.move.line']._fields:
line_vals['x_fc_adp_portion'] = adp_portion line_vals['x_fc_adp_portion'] = adp_portion
@@ -5170,13 +5267,13 @@ class SaleOrder(models.Model):
f'border-bottom:2px solid #4a5568;{font}"' f'border-bottom:2px solid #4a5568;{font}"'
) )
cell_style = ( cell_style = (
'style="padding:7px 10px;font-size:12px;color:#2d3748;' 'style="padding:7px 10px;font-size:12px;'
'border-bottom:1px solid #e2e8f0;"' 'border-bottom:1px solid rgba(128,128,128,0.15);"'
) )
alt_row = 'style="background:#f7fafc;"' alt_row = 'style="background:rgba(128,128,128,0.06);"'
amt_style = ( amt_style = (
'style="padding:7px 10px;font-size:12px;color:#2d3748;' 'style="padding:7px 10px;font-size:12px;'
'border-bottom:1px solid #e2e8f0;text-align:right;"' 'border-bottom:1px solid rgba(128,128,128,0.15);text-align:right;"'
) )
hdr_r = hdr_style.replace('text-align:left', 'text-align:right') hdr_r = hdr_style.replace('text-align:left', 'text-align:right')
@@ -5187,9 +5284,9 @@ class SaleOrder(models.Model):
html = ( html = (
'<div style="margin:20px 0;">' '<div style="margin:20px 0;">'
f'<h3 style="color:#1a202c;font-size:15px;font-weight:700;' f'<h3 style="font-size:15px;font-weight:700;'
f'margin:0 0 10px 0;{font}">Approved Items</h3>' f'margin:0 0 10px 0;{font}">Approved Items</h3>'
'<table style="width:100%;border-collapse:collapse;border:1px solid #e2e8f0;">' '<table style="width:100%;border-collapse:collapse;border:1px solid rgba(128,128,128,0.25);">'
'<thead><tr>' '<thead><tr>'
f'<th {hdr_style}>S/N</th>' f'<th {hdr_style}>S/N</th>'
f'<th {hdr_style}>ADP Code</th>' f'<th {hdr_style}>ADP Code</th>'
@@ -5241,13 +5338,13 @@ class SaleOrder(models.Model):
colspan = 5 colspan = 5
total_style = ( total_style = (
'style="padding:8px 10px;font-size:12px;font-weight:700;' 'style="padding:8px 10px;font-size:12px;font-weight:700;'
'color:#1a202c;border-top:2px solid #2d3748;text-align:right;"' 'border-top:2px solid rgba(128,128,128,0.3);text-align:right;"'
) )
total_label_style = ( total_label_style = (
f'style="padding:8px 10px;font-size:12px;font-weight:700;' 'style="padding:8px 10px;font-size:12px;font-weight:700;'
f'color:#1a202c;border-top:2px solid #2d3748;text-align:right;"' 'border-top:2px solid rgba(128,128,128,0.3);text-align:right;"'
) )
html += f'<tr style="background:#edf2f7;">' html += '<tr style="background:rgba(128,128,128,0.08);">'
html += f'<td colspan="{colspan}" {total_label_style}>Total</td>' html += f'<td colspan="{colspan}" {total_label_style}>Total</td>'
html += f'<td {total_style}>${total_adp:,.2f}</td>' html += f'<td {total_style}>${total_adp:,.2f}</td>'
html += f'<td {total_style}>${total_client:,.2f}</td>' html += f'<td {total_style}>${total_client:,.2f}</td>'
@@ -5529,8 +5626,13 @@ class SaleOrder(models.Model):
_logger.error(f"Failed to send case closed email for {self.name}: {e}") _logger.error(f"Failed to send case closed email for {self.name}: {e}")
return False return False
def _send_withdrawal_email(self, reason=None): def _send_withdrawal_email(self, reason=None, intent=None):
"""Send notification when application is withdrawn.""" """Send notification when application is withdrawn.
Args:
reason: Free-text reason for withdrawal.
intent: 'cancel' or 'resubmit' — determines email wording.
"""
self.ensure_one() self.ensure_one()
if not self._is_email_notifications_enabled(): if not self._is_email_notifications_enabled():
return False return False
@@ -5542,17 +5644,34 @@ class SaleOrder(models.Model):
client_name = (recipients.get('client') or self.partner_id).name or 'Client' client_name = (recipients.get('client') or self.partner_id).name or 'Client'
sales_rep_name = (recipients.get('sales_rep') or self.env.user).name sales_rep_name = (recipients.get('sales_rep') or self.env.user).name
note_text = 'This application has been withdrawn from the Assistive Devices Program.' if intent == 'cancel':
note_text = ('This application has been permanently withdrawn and cancelled. '
'The sale order and all related invoices have been cancelled.')
title = 'Application Withdrawn & Cancelled'
subject_suffix = 'Withdrawn & Cancelled'
note_color = '#dc3545'
elif intent == 'resubmit':
note_text = ('This application has been withdrawn for correction and will be resubmitted. '
'The application has been returned to Ready for Submission status.')
title = 'Application Withdrawn for Correction'
subject_suffix = 'Withdrawn for Correction'
note_color = '#d69e2e'
else:
note_text = 'This application has been withdrawn from the Assistive Devices Program.'
title = 'Application Withdrawn'
subject_suffix = 'Withdrawn'
note_color = '#d69e2e'
if reason: if reason:
note_text += f'<br/><strong>Reason:</strong> {reason}' note_text += f'<br/><strong>Reason:</strong> {reason}'
body_html = self._email_build( body_html = self._email_build(
title='Application Withdrawn', title=title,
summary=f'The ADP application for <strong>{client_name}</strong> has been withdrawn.', summary=f'The ADP application for <strong>{client_name}</strong> has been withdrawn.',
email_type='attention', email_type='attention',
sections=[('Case Details', self._build_case_detail_rows())], sections=[('Case Details', self._build_case_detail_rows())],
note=note_text, note=note_text,
note_color='#d69e2e', note_color=note_color,
button_url=f'{self.get_base_url()}/web#id={self.id}&model=sale.order&view_type=form', button_url=f'{self.get_base_url()}/web#id={self.id}&model=sale.order&view_type=form',
sender_name=sales_rep_name, sender_name=sales_rep_name,
) )
@@ -5560,12 +5679,12 @@ class SaleOrder(models.Model):
email_cc = ', '.join(cc_emails) if to_emails else ', '.join(cc_emails[1:]) email_cc = ', '.join(cc_emails) if to_emails else ', '.join(cc_emails[1:])
try: try:
self.env['mail.mail'].sudo().create({ self.env['mail.mail'].sudo().create({
'subject': f'Application Withdrawn - {client_name} - {self.name}', 'subject': f'Application {subject_suffix} - {client_name} - {self.name}',
'body_html': body_html, 'body_html': body_html,
'email_to': email_to, 'email_cc': email_cc, 'email_to': email_to, 'email_cc': email_cc,
'model': 'sale.order', 'res_id': self.id, 'model': 'sale.order', 'res_id': self.id,
}).send() }).send()
self._email_chatter_log('Application Withdrawn email sent', email_to, email_cc) self._email_chatter_log(f'{title} email sent', email_to, email_cc)
return True return True
except Exception as e: except Exception as e:
_logger.error(f"Failed to send withdrawal email for {self.name}: {e}") _logger.error(f"Failed to send withdrawal email for {self.name}: {e}")
@@ -5862,7 +5981,10 @@ class SaleOrder(models.Model):
'x_fc_proof_of_delivery', 'x_fc_proof_of_delivery',
'x_fc_approval_letter', 'x_fc_approval_letter',
] ]
doc_changes = {f: vals.get(f) for f in document_fields if f in vals and vals.get(f)} if self.env.context.get('skip_document_chatter'):
doc_changes = {}
else:
doc_changes = {f: vals.get(f) for f in document_fields if f in vals and vals.get(f)}
# Preserve old documents in chatter BEFORE they get replaced or deleted # Preserve old documents in chatter BEFORE they get replaced or deleted
# This ensures document history is maintained for audit purposes # This ensures document history is maintained for audit purposes
@@ -5885,7 +6007,7 @@ class SaleOrder(models.Model):
for order in self: for order in self:
for field_name in document_fields: for field_name in document_fields:
if field_name in vals and field_name not in correction_handled: if field_name in vals and field_name not in correction_handled and not self.env.context.get('skip_document_chatter'):
old_data = getattr(order, field_name, None) old_data = getattr(order, field_name, None)
new_data = vals.get(field_name) new_data = vals.get(field_name)
label = document_labels.get(field_name, field_name) label = document_labels.get(field_name, field_name)
@@ -6584,96 +6706,6 @@ class SaleOrder(models.Model):
except Exception as e: except Exception as e:
_logger.error(f" Failed to sync serial to invoice line {inv_line.id}: {e}") _logger.error(f" Failed to sync serial to invoice line {inv_line.id}: {e}")
def action_sync_adp_fields(self):
"""Manual action to sync all ADP fields to invoices."""
synced_invoices = 0
for order in self:
# First sync Studio fields to FC fields on the SO itself
order._sync_studio_to_fc_fields()
# Then sync to invoices
invoices = order.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
if invoices:
order._sync_fields_to_invoices()
synced_invoices += len(invoices)
# Force refresh of the view
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Fields Synchronized',
'message': f'Synced ADP fields from {len(self)} sale order(s) to {synced_invoices} invoice(s). Please refresh the page to see updated values.',
'type': 'success',
'sticky': False,
}
}
@api.model
def _cron_sync_adp_fields(self):
"""Cron job to sync ADP fields from Sale Orders to Invoices.
Processes all ADP sales created/modified in the last 7 days.
Uses dynamic field mappings from Settings.
"""
from datetime import timedelta
cutoff_date = fields.Datetime.now() - timedelta(days=7)
# Get field mappings
mappings = self._get_field_mappings()
sale_type_field = self.env['ir.config_parameter'].sudo().get_param(
'fusion_claims.field_sale_type', 'x_fc_sale_type'
)
# Build domain - check FC sale type fields
domain = [('write_date', '>=', cutoff_date)]
or_conditions = []
# Check FC sale type field
if sale_type_field in self._fields:
or_conditions.append((sale_type_field, 'in', ['adp', 'adp_odsp', 'ADP', 'ADP/ODSP']))
# Check claim number fields
claim_field = mappings.get('so_claim_number', 'x_fc_claim_number')
if claim_field in self._fields:
or_conditions.append((claim_field, '!=', False))
# Combine with OR - each '|' must be a separate element in the domain list
if or_conditions:
# Add (n-1) OR operators for n conditions
for _ in range(len(or_conditions) - 1):
domain.append('|')
# Add all conditions
for cond in or_conditions:
domain.append(cond)
try:
orders = self.search(domain)
except Exception as e:
_logger.error(f"Error searching for ADP orders: {e}")
# Fallback to simpler search
orders = self.search([
('write_date', '>=', cutoff_date),
('invoice_ids', '!=', False),
])
synced_count = 0
error_count = 0
for order in orders:
try:
# Only sync if it's an ADP sale
if order._is_adp_sale() or order.x_fc_claim_number:
order._sync_studio_to_fc_fields()
order._sync_fields_to_invoices()
synced_count += 1
except Exception as e:
error_count += 1
_logger.warning(f"Failed to sync order {order.name}: {e}")
_logger.info(f"Fusion Claims sync complete: {synced_count} orders synced, {error_count} errors")
return synced_count
# ========================================================================== # ==========================================================================
# EMAIL SEND OVERRIDE (Use ADP templates for ADP sales) # EMAIL SEND OVERRIDE (Use ADP templates for ADP sales)
# ========================================================================== # ==========================================================================

File diff suppressed because it is too large Load Diff

View File

@@ -36,15 +36,6 @@ access_fusion_client_chat_message_user,fusion.client.chat.message.user,model_fus
access_fusion_client_chat_message_manager,fusion.client.chat.message.manager,model_fusion_client_chat_message,sales_team.group_sale_manager,1,1,1,1 access_fusion_client_chat_message_manager,fusion.client.chat.message.manager,model_fusion_client_chat_message,sales_team.group_sale_manager,1,1,1,1
access_fusion_xml_import_wizard,fusion.xml.import.wizard.user,model_fusion_xml_import_wizard,sales_team.group_sale_manager,1,1,1,1 access_fusion_xml_import_wizard,fusion.xml.import.wizard.user,model_fusion_xml_import_wizard,sales_team.group_sale_manager,1,1,1,1
access_fusion_claims_dashboard_user,fusion.claims.dashboard.user,model_fusion_claims_dashboard,sales_team.group_sale_salesman,1,1,1,1 access_fusion_claims_dashboard_user,fusion.claims.dashboard.user,model_fusion_claims_dashboard,sales_team.group_sale_salesman,1,1,1,1
access_fusion_technician_task_user,fusion.technician.task.user,model_fusion_technician_task,sales_team.group_sale_salesman,1,1,1,0
access_fusion_technician_task_manager,fusion.technician.task.manager,model_fusion_technician_task,sales_team.group_sale_manager,1,1,1,1
access_fusion_technician_task_technician,fusion.technician.task.technician,model_fusion_technician_task,fusion_claims.group_field_technician,1,1,0,0
access_fusion_technician_task_portal,fusion.technician.task.portal,model_fusion_technician_task,base.group_portal,1,0,0,0
access_fusion_push_subscription_user,fusion.push.subscription.user,model_fusion_push_subscription,base.group_user,1,1,1,0
access_fusion_push_subscription_portal,fusion.push.subscription.portal,model_fusion_push_subscription,base.group_portal,1,1,1,0
access_fusion_technician_location_manager,fusion.technician.location.manager,model_fusion_technician_location,sales_team.group_sale_manager,1,1,1,1
access_fusion_technician_location_user,fusion.technician.location.user,model_fusion_technician_location,sales_team.group_sale_salesman,1,0,0,0
access_fusion_technician_location_portal,fusion.technician.location.portal,model_fusion_technician_location,base.group_portal,0,0,1,0
access_fusion_send_to_mod_wizard_user,fusion_claims.send.to.mod.wizard.user,model_fusion_claims_send_to_mod_wizard,sales_team.group_sale_salesman,1,1,1,0 access_fusion_send_to_mod_wizard_user,fusion_claims.send.to.mod.wizard.user,model_fusion_claims_send_to_mod_wizard,sales_team.group_sale_salesman,1,1,1,0
access_fusion_send_to_mod_wizard_manager,fusion_claims.send.to.mod.wizard.manager,model_fusion_claims_send_to_mod_wizard,sales_team.group_sale_manager,1,1,1,1 access_fusion_send_to_mod_wizard_manager,fusion_claims.send.to.mod.wizard.manager,model_fusion_claims_send_to_mod_wizard,sales_team.group_sale_manager,1,1,1,1
access_fusion_mod_awaiting_wizard_user,fusion_claims.mod.awaiting.funding.wizard.user,model_fusion_claims_mod_awaiting_funding_wizard,sales_team.group_sale_salesman,1,1,1,0 access_fusion_mod_awaiting_wizard_user,fusion_claims.mod.awaiting.funding.wizard.user,model_fusion_claims_mod_awaiting_funding_wizard,sales_team.group_sale_salesman,1,1,1,0
@@ -71,8 +62,6 @@ access_fusion_odsp_ready_delivery_wizard_user,fusion_claims.odsp.ready.delivery.
access_fusion_odsp_ready_delivery_wizard_manager,fusion_claims.odsp.ready.delivery.wizard.manager,model_fusion_claims_odsp_ready_delivery_wizard,sales_team.group_sale_manager,1,1,1,1 access_fusion_odsp_ready_delivery_wizard_manager,fusion_claims.odsp.ready.delivery.wizard.manager,model_fusion_claims_odsp_ready_delivery_wizard,sales_team.group_sale_manager,1,1,1,1
access_fusion_submit_to_odsp_wizard_user,fusion_claims.submit.to.odsp.wizard.user,model_fusion_claims_submit_to_odsp_wizard,sales_team.group_sale_salesman,1,1,1,0 access_fusion_submit_to_odsp_wizard_user,fusion_claims.submit.to.odsp.wizard.user,model_fusion_claims_submit_to_odsp_wizard,sales_team.group_sale_salesman,1,1,1,0
access_fusion_submit_to_odsp_wizard_manager,fusion_claims.submit.to.odsp.wizard.manager,model_fusion_claims_submit_to_odsp_wizard,sales_team.group_sale_manager,1,1,1,1 access_fusion_submit_to_odsp_wizard_manager,fusion_claims.submit.to.odsp.wizard.manager,model_fusion_claims_submit_to_odsp_wizard,sales_team.group_sale_manager,1,1,1,1
access_fusion_task_sync_config_manager,fusion.task.sync.config.manager,model_fusion_task_sync_config,sales_team.group_sale_manager,1,1,1,1
access_fusion_task_sync_config_user,fusion.task.sync.config.user,model_fusion_task_sync_config,sales_team.group_sale_salesman,1,0,0,0
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_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_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_user,fusion.ltc.floor.user,model_fusion_ltc_floor,sales_team.group_sale_salesman,1,1,1,0
@@ -90,4 +79,9 @@ access_fusion_ltc_family_contact_manager,fusion.ltc.family.contact.manager,model
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_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_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_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 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
access_fusion_page11_sign_request_user,fusion.page11.sign.request.user,model_fusion_page11_sign_request,sales_team.group_sale_salesman,1,1,1,0
access_fusion_page11_sign_request_manager,fusion.page11.sign.request.manager,model_fusion_page11_sign_request,sales_team.group_sale_manager,1,1,1,1
access_fusion_page11_sign_request_public,fusion.page11.sign.request.public,model_fusion_page11_sign_request,base.group_public,1,0,0,0
access_fusion_send_page11_wizard_user,fusion_claims.send.page11.wizard.user,model_fusion_claims_send_page11_wizard,sales_team.group_sale_salesman,1,1,1,1
access_fusion_send_page11_wizard_manager,fusion_claims.send.page11.wizard.manager,model_fusion_claims_send_page11_wizard,sales_team.group_sale_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
36 access_fusion_client_chat_message_manager fusion.client.chat.message.manager model_fusion_client_chat_message sales_team.group_sale_manager 1 1 1 1
37 access_fusion_xml_import_wizard fusion.xml.import.wizard.user model_fusion_xml_import_wizard sales_team.group_sale_manager 1 1 1 1
38 access_fusion_claims_dashboard_user fusion.claims.dashboard.user model_fusion_claims_dashboard sales_team.group_sale_salesman 1 1 1 1
access_fusion_technician_task_user fusion.technician.task.user model_fusion_technician_task sales_team.group_sale_salesman 1 1 1 0
access_fusion_technician_task_manager fusion.technician.task.manager model_fusion_technician_task sales_team.group_sale_manager 1 1 1 1
access_fusion_technician_task_technician fusion.technician.task.technician model_fusion_technician_task fusion_claims.group_field_technician 1 1 0 0
access_fusion_technician_task_portal fusion.technician.task.portal model_fusion_technician_task base.group_portal 1 0 0 0
access_fusion_push_subscription_user fusion.push.subscription.user model_fusion_push_subscription base.group_user 1 1 1 0
access_fusion_push_subscription_portal fusion.push.subscription.portal model_fusion_push_subscription base.group_portal 1 1 1 0
access_fusion_technician_location_manager fusion.technician.location.manager model_fusion_technician_location sales_team.group_sale_manager 1 1 1 1
access_fusion_technician_location_user fusion.technician.location.user model_fusion_technician_location sales_team.group_sale_salesman 1 0 0 0
access_fusion_technician_location_portal fusion.technician.location.portal model_fusion_technician_location base.group_portal 0 0 1 0
39 access_fusion_send_to_mod_wizard_user fusion_claims.send.to.mod.wizard.user model_fusion_claims_send_to_mod_wizard sales_team.group_sale_salesman 1 1 1 0
40 access_fusion_send_to_mod_wizard_manager fusion_claims.send.to.mod.wizard.manager model_fusion_claims_send_to_mod_wizard sales_team.group_sale_manager 1 1 1 1
41 access_fusion_mod_awaiting_wizard_user fusion_claims.mod.awaiting.funding.wizard.user model_fusion_claims_mod_awaiting_funding_wizard sales_team.group_sale_salesman 1 1 1 0
62 access_fusion_odsp_ready_delivery_wizard_manager fusion_claims.odsp.ready.delivery.wizard.manager model_fusion_claims_odsp_ready_delivery_wizard sales_team.group_sale_manager 1 1 1 1
63 access_fusion_submit_to_odsp_wizard_user fusion_claims.submit.to.odsp.wizard.user model_fusion_claims_submit_to_odsp_wizard sales_team.group_sale_salesman 1 1 1 0
64 access_fusion_submit_to_odsp_wizard_manager fusion_claims.submit.to.odsp.wizard.manager model_fusion_claims_submit_to_odsp_wizard sales_team.group_sale_manager 1 1 1 1
access_fusion_task_sync_config_manager fusion.task.sync.config.manager model_fusion_task_sync_config sales_team.group_sale_manager 1 1 1 1
access_fusion_task_sync_config_user fusion.task.sync.config.user model_fusion_task_sync_config sales_team.group_sale_salesman 1 0 0 0
65 access_fusion_ltc_facility_user fusion.ltc.facility.user model_fusion_ltc_facility sales_team.group_sale_salesman 1 1 1 0
66 access_fusion_ltc_facility_manager fusion.ltc.facility.manager model_fusion_ltc_facility sales_team.group_sale_manager 1 1 1 1
67 access_fusion_ltc_floor_user fusion.ltc.floor.user model_fusion_ltc_floor sales_team.group_sale_salesman 1 1 1 0
79 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
80 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
81 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
82 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
83 access_fusion_page11_sign_request_user fusion.page11.sign.request.user model_fusion_page11_sign_request sales_team.group_sale_salesman 1 1 1 0
84 access_fusion_page11_sign_request_manager fusion.page11.sign.request.manager model_fusion_page11_sign_request sales_team.group_sale_manager 1 1 1 1
85 access_fusion_page11_sign_request_public fusion.page11.sign.request.public model_fusion_page11_sign_request base.group_public 1 0 0 0
86 access_fusion_send_page11_wizard_user fusion_claims.send.page11.wizard.user model_fusion_claims_send_page11_wizard sales_team.group_sale_salesman 1 1 1 1
87 access_fusion_send_page11_wizard_manager fusion_claims.send.page11.wizard.manager model_fusion_claims_send_page11_wizard sales_team.group_sale_manager 1 1 1 1

View File

@@ -54,88 +54,5 @@
<field name="comment">Temporary permission for editing locked documents on old/legacy cases. Requires the "Allow Document Lock Override" setting to be enabled in Fusion Claims Settings. Once all legacy cases are handled, disable the setting and remove this permission from users.</field> <field name="comment">Temporary permission for editing locked documents on old/legacy cases. Requires the "Allow Document Lock Override" setting to be enabled in Fusion Claims Settings. Once all legacy cases are handled, disable the setting and remove this permission from users.</field>
</record> </record>
<!-- ================================================================== -->
<!-- FIELD TECHNICIAN GROUP -->
<!-- Standalone group safe for both portal and internal users. -->
<!-- Do NOT imply group_fusion_claims_user — that chain leads to -->
<!-- base.group_user which conflicts with portal users (share=True). -->
<!-- Menu visibility is handled via comma-separated groups= on menus. -->
<!-- ================================================================== -->
<record id="group_field_technician" model="res.groups">
<field name="name">Field Technician</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_claims"/>
</record>
<!-- ================================================================== -->
<!-- TECHNICIAN TASK RECORD RULES -->
<!-- ================================================================== -->
<!-- Managers: full access to all tasks -->
<record id="rule_technician_task_manager" model="ir.rule">
<field name="name">Technician Task: Manager Full Access</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<!-- Sales users: read/write all tasks, create tasks -->
<record id="rule_technician_task_sales_user" model="ir.rule">
<field name="name">Technician Task: Sales User Access</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- Field Technicians (internal): own tasks only -->
<record id="rule_technician_task_technician" model="ir.rule">
<field name="name">Technician Task: Technician Own Tasks</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="domain_force">[('technician_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_field_technician'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- Portal technicians: own tasks only, read + limited write -->
<record id="rule_technician_task_portal" model="ir.rule">
<field name="name">Technician Task: Portal Technician Access</field>
<field name="model_id" ref="model_fusion_technician_task"/>
<field name="domain_force">[('technician_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- ================================================================== -->
<!-- PUSH SUBSCRIPTION RECORD RULES -->
<!-- ================================================================== -->
<!-- Users: own subscriptions only -->
<record id="rule_push_subscription_user" model="ir.rule">
<field name="name">Push Subscription: Own Only</field>
<field name="model_id" ref="model_fusion_push_subscription"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- Portal: own subscriptions only -->
<record id="rule_push_subscription_portal" model="ir.rule">
<field name="name">Push Subscription: Portal Own Only</field>
<field name="model_id" ref="model_fusion_push_subscription"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</odoo> </odoo>

View File

@@ -138,6 +138,75 @@ $transition-speed: .25s;
font-weight: 500; font-weight: 500;
} }
// ── Technician filter chips ─────────────────────────────────────────
.fc_tech_filters {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.fc_tech_chip {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px 3px 4px;
font-size: 11px;
font-weight: 600;
border: 1px solid $border-color;
border-radius: 14px;
background: transparent;
color: $text-muted;
cursor: pointer;
transition: all .15s;
line-height: 18px;
max-width: 100%;
overflow: hidden;
&:hover {
border-color: rgba($primary, .35);
color: $body-color;
background: rgba($primary, .06);
}
&--active {
background: $primary !important;
color: #fff !important;
border-color: $primary !important;
.fc_tech_chip_avatar {
background: rgba(#fff, .25);
color: #fff;
}
}
&--all {
padding: 3px 10px;
color: $body-color;
font-weight: 500;
&:hover { background: rgba($primary, .1); }
}
}
.fc_tech_chip_avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba($secondary, .15);
color: $body-color;
font-size: 9px;
font-weight: 700;
flex-shrink: 0;
}
.fc_tech_chip_name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Collapsed toggle button (floating) // Collapsed toggle button (floating)
.fc_sidebar_toggle_btn { .fc_sidebar_toggle_btn {
position: absolute; position: absolute;

View File

@@ -180,9 +180,22 @@ const SOURCE_COLORS = {
mobility: "#198754", mobility: "#198754",
}; };
/** Extract unique technicians from task data, sorted by name */
function extractTechnicians(tasksData) {
const map = {};
for (const t of tasksData) {
if (t.technician_id) {
const [id, name] = t.technician_id;
if (!map[id]) {
map[id] = { id, name, initials: initialsOf(name) };
}
}
}
return Object.values(map).sort((a, b) => a.name.localeCompare(b.name));
}
/** Group + sort tasks, returning { groupKey: { label, tasks[], count } } */ /** Group + sort tasks, returning { groupKey: { label, tasks[], count } } */
function groupTasks(tasksData, localInstanceId) { function groupTasks(tasksData, localInstanceId, visibleTechIds) {
// Sort by date ASC, time ASC
const sorted = [...tasksData].sort((a, b) => { const sorted = [...tasksData].sort((a, b) => {
const da = a.scheduled_date || ""; const da = a.scheduled_date || "";
const db = b.scheduled_date || ""; const db = b.scheduled_date || "";
@@ -190,6 +203,8 @@ function groupTasks(tasksData, localInstanceId) {
return (a.time_start || 0) - (b.time_start || 0); return (a.time_start || 0) - (b.time_start || 0);
}); });
const hasTechFilter = visibleTechIds && Object.keys(visibleTechIds).length > 0;
const groups = {}; const groups = {};
const order = [GROUP_PENDING, GROUP_YESTERDAY, GROUP_TODAY, GROUP_TOMORROW, GROUP_THIS_WEEK, GROUP_LATER]; const order = [GROUP_PENDING, GROUP_YESTERDAY, GROUP_TODAY, GROUP_TOMORROW, GROUP_THIS_WEEK, GROUP_LATER];
for (const key of order) { for (const key of order) {
@@ -205,12 +220,15 @@ function groupTasks(tasksData, localInstanceId) {
const dayCounters = {}; const dayCounters = {};
for (const task of sorted) { for (const task of sorted) {
const techId = task.technician_id ? task.technician_id[0] : 0;
if (hasTechFilter && !visibleTechIds[techId]) continue;
const g = classifyTask(task); const g = classifyTask(task);
const dayKey = task.scheduled_date || "none"; const dayKey = task.scheduled_date || "none";
dayCounters[dayKey] = (dayCounters[dayKey] || 0) + 1; dayCounters[dayKey] = (dayCounters[dayKey] || 0) + 1;
task._scheduleNum = dayCounters[dayKey]; task._scheduleNum = dayCounters[dayKey];
task._group = g; task._group = g;
task._dayColor = DAY_COLORS[g] || "#6b7280"; // Pin colour by day task._dayColor = DAY_COLORS[g] || "#6b7280";
task._statusColor = STATUS_COLORS[task.status] || "#6b7280"; task._statusColor = STATUS_COLORS[task.status] || "#6b7280";
task._statusLabel = STATUS_LABELS[task.status] || task.status || ""; task._statusLabel = STATUS_LABELS[task.status] || task.status || "";
task._statusIcon = STATUS_ICONS[task.status] || "fa-circle"; task._statusIcon = STATUS_ICONS[task.status] || "fa-circle";
@@ -228,7 +246,6 @@ function groupTasks(tasksData, localInstanceId) {
groups[g].count++; groups[g].count++;
} }
// Return only non-empty groups in order
return order.map((k) => groups[k]).filter((g) => g.count > 0); return order.map((k) => groups[k]).filter((g) => g.count > 0);
} }
@@ -259,12 +276,10 @@ export class FusionTaskMapController extends Component {
showRoute: true, showRoute: true,
taskCount: 0, taskCount: 0,
techCount: 0, techCount: 0,
// Sidebar
sidebarOpen: true, sidebarOpen: true,
groups: [], // [{key, label, tasks[], count}] groups: [],
collapsedGroups: {}, // {groupKey: true} collapsedGroups: {},
activeTaskId: null, // Highlighted task activeTaskId: null,
// Day filters for map pins (which groups show on map)
visibleGroups: { visibleGroups: {
[GROUP_YESTERDAY]: false, [GROUP_YESTERDAY]: false,
[GROUP_TODAY]: true, [GROUP_TODAY]: true,
@@ -272,6 +287,8 @@ export class FusionTaskMapController extends Component {
[GROUP_THIS_WEEK]: false, [GROUP_THIS_WEEK]: false,
[GROUP_LATER]: false, [GROUP_LATER]: false,
}, },
allTechnicians: [],
visibleTechIds: {},
}); });
// Yesterday collapsed by default in sidebar list // Yesterday collapsed by default in sidebar list
@@ -339,9 +356,17 @@ export class FusionTaskMapController extends Component {
this.tasksData = result.tasks || []; this.tasksData = result.tasks || [];
this.locationsData = result.locations || []; this.locationsData = result.locations || [];
this.techStartLocations = result.tech_start_locations || {}; this.techStartLocations = result.tech_start_locations || {};
this.state.taskCount = this.tasksData.length; this.state.allTechnicians = extractTechnicians(this.tasksData);
this._rebuildGroups();
}
_rebuildGroups() {
this.state.groups = groupTasks(
this.tasksData, this.localInstanceId, this.state.visibleTechIds,
);
const filteredCount = this.state.groups.reduce((s, g) => s + g.count, 0);
this.state.taskCount = filteredCount;
this.state.techCount = this.locationsData.length; this.state.techCount = this.locationsData.length;
this.state.groups = groupTasks(this.tasksData, this.localInstanceId);
} }
async _loadAndRender() { async _loadAndRender() {
@@ -1008,6 +1033,28 @@ export class FusionTaskMapController extends Component {
this._renderMarkers(); this._renderMarkers();
} }
// ── Technician filter ─────────────────────────────────────────────
toggleTechFilter(techId) {
if (this.state.visibleTechIds[techId]) {
delete this.state.visibleTechIds[techId];
} else {
this.state.visibleTechIds[techId] = true;
}
this._rebuildGroups();
this._renderMarkers();
}
isTechVisible(techId) {
const hasFilter = Object.keys(this.state.visibleTechIds).length > 0;
return !hasFilter || !!this.state.visibleTechIds[techId];
}
showAllTechs() {
this.state.visibleTechIds = {};
this._rebuildGroups();
this._renderMarkers();
}
// ── Top bar actions ───────────────────────────────────────────── // ── Top bar actions ─────────────────────────────────────────────
toggleTraffic() { toggleTraffic() {
this.state.showTraffic = !this.state.showTraffic; this.state.showTraffic = !this.state.showTraffic;

View File

@@ -52,6 +52,22 @@
<button class="fc_day_chip fc_day_chip--all" t-on-click="showAllDays" <button class="fc_day_chip fc_day_chip--all" t-on-click="showAllDays"
title="Show all">All</button> title="Show all">All</button>
</div> </div>
<!-- Technician filter -->
<t t-if="state.allTechnicians.length > 1">
<div class="fc_tech_filters mt-2">
<t t-foreach="state.allTechnicians" t-as="tech" t-key="tech.id">
<button t-att-class="'fc_tech_chip' + (isTechVisible(tech.id) ? ' fc_tech_chip--active' : '')"
t-on-click="() => this.toggleTechFilter(tech.id)"
t-att-title="tech.name">
<span class="fc_tech_chip_avatar" t-esc="tech.initials"/>
<span class="fc_tech_chip_name" t-esc="tech.name"/>
</button>
</t>
<button class="fc_tech_chip fc_tech_chip--all" t-on-click="showAllTechs"
title="Show all technicians">All</button>
</div>
</t>
</div> </div>
<!-- Sidebar body: grouped task list --> <!-- Sidebar body: grouped task list -->

View File

@@ -341,6 +341,27 @@
</field> </field>
</record> </record>
<!-- ===================================================================== -->
<!-- INVOICE LIST: Custom Columns -->
<!-- ===================================================================== -->
<record id="view_out_invoice_tree_fusion_claims" model="ir.ui.view">
<field name="name">account.move.list.fusion.central</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_invoice_tree"/>
<field name="priority">80</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount_untaxed_in_currency_signed']" position="before">
<field name="x_fc_invoice_type" string="Invoice Type" optional="hide"/>
<field name="x_fc_client_type" string="Client Type" optional="hide"/>
<field name="x_fc_claim_number" string="Claim #" optional="hide"/>
<field name="x_fc_client_ref_1" string="Client Ref 1" optional="hide"/>
<field name="x_fc_client_ref_2" string="Client Ref 2" optional="hide"/>
<field name="partner_shipping_id" string="Delivery Address" optional="hide"/>
<field name="x_fc_adp_invoice_portion" string="Portion" optional="hide" widget="badge"/>
</xpath>
</field>
</record>
<!-- ===================================================================== --> <!-- ===================================================================== -->
<!-- INVOICE SEARCH: Filters --> <!-- INVOICE SEARCH: Filters -->
<!-- ===================================================================== --> <!-- ===================================================================== -->
@@ -350,24 +371,59 @@
<field name="inherit_id" ref="account.view_account_invoice_filter"/> <field name="inherit_id" ref="account.view_account_invoice_filter"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//search" position="inside"> <xpath expr="//search" position="inside">
<!-- Search Fields -->
<field name="partner_shipping_id" string="Delivery Address"/>
<field name="x_fc_claim_number" string="Claim Number"/>
<field name="x_fc_client_ref_1" string="Client Reference 1"/>
<field name="x_fc_client_ref_2" string="Client Reference 2"/>
<field name="x_fc_invoice_type" string="Invoice Type"/>
<field name="x_fc_client_type" string="Client Type"/>
<separator/> <separator/>
<filter string="ADP Invoices" name="adp_invoices" <!-- Sale Type Filters -->
<filter string="ADP" name="type_adp"
domain="[('x_fc_invoice_type', 'in', ['adp', 'adp_odsp'])]"/> domain="[('x_fc_invoice_type', 'in', ['adp', 'adp_odsp'])]"/>
<filter string="ADP Client" name="type_adp_client"
domain="[('x_fc_invoice_type', '=', 'adp_client')]"/>
<filter string="ODSP" name="type_odsp"
domain="[('x_fc_invoice_type', 'in', ['odsp', 'adp_odsp'])]"/>
<filter string="MOD" name="type_mod"
domain="[('x_fc_invoice_type', '=', 'march_of_dimes')]"/>
<filter string="WSIB" name="type_wsib"
domain="[('x_fc_invoice_type', '=', 'wsib')]"/>
<filter string="Insurance" name="type_insurance"
domain="[('x_fc_invoice_type', '=', 'insurance')]"/>
<filter string="Direct/Private" name="type_direct_private"
domain="[('x_fc_invoice_type', '=', 'direct_private')]"/>
<filter string="Hardship" name="type_hardship"
domain="[('x_fc_invoice_type', '=', 'hardship')]"/>
<filter string="Rentals" name="type_rental"
domain="[('x_fc_invoice_type', '=', 'rental')]"/>
<filter string="Muscular Dystrophy" name="type_muscular_dystrophy"
domain="[('x_fc_invoice_type', '=', 'muscular_dystrophy')]"/>
<filter string="Others" name="type_other"
domain="[('x_fc_invoice_type', '=', 'other')]"/>
<filter string="Regular" name="type_regular"
domain="[('x_fc_invoice_type', '=', 'regular')]"/>
<separator/>
<!-- ADP Export Filters -->
<filter string="ADP Exported" name="adp_exported" <filter string="ADP Exported" name="adp_exported"
domain="[('adp_exported', '=', True)]"/> domain="[('adp_exported', '=', True)]"/>
<filter string="Not ADP Exported" name="not_adp_exported" <filter string="Not ADP Exported" name="not_adp_exported"
domain="[('adp_exported', '=', False), ('x_fc_invoice_type', 'in', ['adp', 'adp_odsp']), ('move_type', 'in', ['out_invoice', 'out_refund'])]"/> domain="[('adp_exported', '=', False), ('x_fc_invoice_type', 'in', ['adp', 'adp_odsp']), ('move_type', 'in', ['out_invoice', 'out_refund'])]"/>
<separator/> <separator/>
<!-- Client Type Filters -->
<filter string="REG Clients" name="reg_clients" <filter string="REG Clients" name="reg_clients"
domain="[('x_fc_client_type', '=', 'REG')]"/> domain="[('x_fc_client_type', '=', 'REG')]"/>
<filter string="ODS/OWP/ACS" name="full_funding" <filter string="ODS/OWP/ACS" name="full_funding"
domain="[('x_fc_client_type', 'in', ['ODS', 'OWP', 'ACS'])]"/> domain="[('x_fc_client_type', 'in', ['ODS', 'OWP', 'ACS'])]"/>
<separator/> <separator/>
<!-- Invoice Portion Filters -->
<filter string="Client Invoices (25%)" name="client_invoices" <filter string="Client Invoices (25%)" name="client_invoices"
domain="[('x_fc_adp_invoice_portion', '=', 'client')]"/> domain="[('x_fc_adp_invoice_portion', '=', 'client')]"/>
<filter string="ADP Invoices (75%)" name="adp_portion_invoices" <filter string="ADP Invoices (75%)" name="adp_portion_invoices"
domain="[('x_fc_adp_invoice_portion', '=', 'adp')]"/> domain="[('x_fc_adp_invoice_portion', '=', 'adp')]"/>
<separator/> <separator/>
<!-- ADP Billing Status Filters -->
<filter string="Billing: Waiting" name="billing_waiting" <filter string="Billing: Waiting" name="billing_waiting"
domain="[('x_fc_adp_billing_status', '=', 'waiting')]"/> domain="[('x_fc_adp_billing_status', '=', 'waiting')]"/>
<filter string="Billing: Submitted" name="billing_submitted" <filter string="Billing: Submitted" name="billing_submitted"
@@ -376,6 +432,16 @@
domain="[('x_fc_adp_billing_status', '=', 'need_correction')]"/> domain="[('x_fc_adp_billing_status', '=', 'need_correction')]"/>
<filter string="Billing: Payment Issued" name="billing_payment_issued" <filter string="Billing: Payment Issued" name="billing_payment_issued"
domain="[('x_fc_adp_billing_status', '=', 'payment_issued')]"/> domain="[('x_fc_adp_billing_status', '=', 'payment_issued')]"/>
<separator/>
<!-- Group By -->
<filter string="Invoice Type" name="group_invoice_type"
context="{'group_by': 'x_fc_invoice_type'}"/>
<filter string="Client Type" name="group_client_type"
context="{'group_by': 'x_fc_client_type'}"/>
<filter string="Invoice Portion" name="group_invoice_portion"
context="{'group_by': 'x_fc_adp_invoice_portion'}"/>
<filter string="Billing Status" name="group_billing_status"
context="{'group_by': 'x_fc_adp_billing_status'}"/>
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@@ -18,6 +18,7 @@
<field name="device_code"/> <field name="device_code"/>
<field name="device_type"/> <field name="device_type"/>
<field name="manufacturer" optional="show"/> <field name="manufacturer" optional="show"/>
<field name="build_type" optional="show"/>
<field name="device_description" optional="hide"/> <field name="device_description" optional="hide"/>
<field name="adp_price"/> <field name="adp_price"/>
<field name="max_quantity"/> <field name="max_quantity"/>
@@ -44,6 +45,7 @@
<group string="Device Information"> <group string="Device Information">
<field name="device_type"/> <field name="device_type"/>
<field name="manufacturer"/> <field name="manufacturer"/>
<field name="build_type"/>
<field name="device_description"/> <field name="device_description"/>
</group> </group>
<group string="Pricing"> <group string="Pricing">
@@ -77,9 +79,12 @@
<field name="device_description"/> <field name="device_description"/>
<separator/> <separator/>
<filter string="Serial Required" name="sn_required" domain="[('sn_required', '=', True)]"/> <filter string="Serial Required" name="sn_required" domain="[('sn_required', '=', True)]"/>
<filter string="Modular" name="filter_modular" domain="[('build_type', '=', 'modular')]"/>
<filter string="Custom Fabricated" name="filter_custom" domain="[('build_type', '=', 'custom_fabricated')]"/>
<separator/> <separator/>
<filter string="Device Type" name="group_device_type" context="{'group_by': 'device_type'}"/> <filter string="Device Type" name="group_device_type" context="{'group_by': 'device_type'}"/>
<filter string="Manufacturer" name="group_manufacturer" context="{'group_by': 'manufacturer'}"/> <filter string="Manufacturer" name="group_manufacturer" context="{'group_by': 'manufacturer'}"/>
<filter string="Build Type" name="group_build_type" context="{'group_by': 'build_type'}"/>
</search> </search>
</field> </field>
</record> </record>
@@ -778,6 +783,348 @@
<field name="help" type="html"><p class="o_view_nocontent_smiling_face">No Ontario Works cases yet</p></field> <field name="help" type="html"><p class="o_view_nocontent_smiling_face">No Ontario Works cases yet</p></field>
</record> </record>
<!-- ===================================================================== -->
<!-- ODSP STANDARD: PER-STATUS ACTIONS -->
<!-- ===================================================================== -->
<record id="action_odsp_std_quotation" model="ir.actions.act_window">
<field name="name">Quotation</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'quotation')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_submitted" model="ir.actions.act_window">
<field name="name">Submitted to ODSP</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'submitted_to_odsp')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_pre_approved" model="ir.actions.act_window">
<field name="name">Pre-Approved</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'pre_approved')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_ready_delivery" model="ir.actions.act_window">
<field name="name">Ready for Delivery</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'ready_delivery')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_delivered" model="ir.actions.act_window">
<field name="name">Delivered</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'delivered')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_pod_submitted" model="ir.actions.act_window">
<field name="name">POD Submitted</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'pod_submitted')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_payment_received" model="ir.actions.act_window">
<field name="name">Payment Received</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'payment_received')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_case_closed" model="ir.actions.act_window">
<field name="name">Case Closed</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'case_closed')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_on_hold" model="ir.actions.act_window">
<field name="name">On Hold</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'on_hold')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_denied" model="ir.actions.act_window">
<field name="name">Denied</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'denied')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<record id="action_odsp_std_cancelled" model="ir.actions.act_window">
<field name="name">Cancelled</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'standard'), ('x_fc_odsp_std_status', '=', 'cancelled')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'standard'}</field>
</record>
<!-- ===================================================================== -->
<!-- SA MOBILITY: PER-STATUS ACTIONS -->
<!-- ===================================================================== -->
<record id="action_odsp_sa_quotation" model="ir.actions.act_window">
<field name="name">Quotation</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'quotation')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_form_ready" model="ir.actions.act_window">
<field name="name">SA Form Ready</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'form_ready')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_submitted" model="ir.actions.act_window">
<field name="name">Submitted to SA Mobility</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'submitted_to_sa')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_pre_approved" model="ir.actions.act_window">
<field name="name">Pre-Approved</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'pre_approved')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_ready_delivery" model="ir.actions.act_window">
<field name="name">Ready for Delivery</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'ready_delivery')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_delivered" model="ir.actions.act_window">
<field name="name">Delivered</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'delivered')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_pod_submitted" model="ir.actions.act_window">
<field name="name">POD Submitted</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'pod_submitted')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_payment_received" model="ir.actions.act_window">
<field name="name">Payment Received</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'payment_received')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_case_closed" model="ir.actions.act_window">
<field name="name">Case Closed</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'case_closed')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_on_hold" model="ir.actions.act_window">
<field name="name">On Hold</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'on_hold')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_denied" model="ir.actions.act_window">
<field name="name">Denied</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'denied')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<record id="action_odsp_sa_cancelled" model="ir.actions.act_window">
<field name="name">Cancelled</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'sa_mobility'), ('x_fc_sa_status', '=', 'cancelled')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'sa_mobility'}</field>
</record>
<!-- ===================================================================== -->
<!-- ONTARIO WORKS: PER-STATUS ACTIONS -->
<!-- ===================================================================== -->
<record id="action_odsp_ow_quotation" model="ir.actions.act_window">
<field name="name">Quotation</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'quotation')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_documents_ready" model="ir.actions.act_window">
<field name="name">Documents Ready</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'documents_ready')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_submitted" model="ir.actions.act_window">
<field name="name">Submitted to Ontario Works</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'submitted_to_ow')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_payment_received" model="ir.actions.act_window">
<field name="name">Payment Received</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'payment_received')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_ready_delivery" model="ir.actions.act_window">
<field name="name">Ready for Delivery</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'ready_delivery')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_delivered" model="ir.actions.act_window">
<field name="name">Delivered</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'delivered')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_case_closed" model="ir.actions.act_window">
<field name="name">Case Closed</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'case_closed')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_on_hold" model="ir.actions.act_window">
<field name="name">On Hold</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'on_hold')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_denied" model="ir.actions.act_window">
<field name="name">Denied</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'denied')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<record id="action_odsp_ow_cancelled" model="ir.actions.act_window">
<field name="name">Cancelled</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form,kanban</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_odsp')})]"/>
<field name="search_view_id" ref="view_sale_order_search_odsp"/>
<field name="domain">[('x_fc_sale_type', 'in', ['odsp', 'adp_odsp']), ('x_fc_odsp_division', '=', 'ontario_works'), ('x_fc_ow_status', '=', 'cancelled')]</field>
<field name="context">{'default_x_fc_sale_type': 'odsp', 'default_x_fc_odsp_division': 'ontario_works'}</field>
</record>
<!-- ===================================================================== --> <!-- ===================================================================== -->
<!-- MARCH OF DIMES: KANBAN VIEW --> <!-- MARCH OF DIMES: KANBAN VIEW -->
<!-- ===================================================================== --> <!-- ===================================================================== -->
@@ -947,10 +1294,10 @@
</record> </record>
<!-- ===================================================================== --> <!-- ===================================================================== -->
<!-- MARCH OF DIMES: ACTION --> <!-- MARCH OF DIMES: ACTIONS -->
<!-- ===================================================================== --> <!-- ===================================================================== -->
<record id="action_fc_march_of_dimes_orders" model="ir.actions.act_window"> <record id="action_fc_march_of_dimes_orders" model="ir.actions.act_window">
<field name="name">March of Dimes Cases</field> <field name="name">All MOD Cases</field>
<field name="res_model">sale.order</field> <field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field> <field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0), <field name="view_ids" eval="[(5, 0, 0),
@@ -959,7 +1306,188 @@
<field name="search_view_id" ref="view_sale_order_search_mod"/> <field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes')]</field> <field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field> <field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
<field name="help" type="html"><p class="o_view_nocontent_smiling_face">No March of Dimes cases yet</p></field> <field name="help" type="html"><p class="o_view_nocontent_smiling_face">No MOD cases yet</p></field>
</record>
<record id="action_mod_schedule_assessment" model="ir.actions.act_window">
<field name="name">Schedule Assessment</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'need_to_schedule')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_assessment_booked" model="ir.actions.act_window">
<field name="name">Assessment Booked</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'assessment_scheduled')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_assessment_done" model="ir.actions.act_window">
<field name="name">Assessment Done</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'assessment_completed')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_processing_drawing" model="ir.actions.act_window">
<field name="name">Processing Drawing</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'processing_drawings')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_quote_sent" model="ir.actions.act_window">
<field name="name">Quote Sent</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'quote_submitted')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_awaiting_funding" model="ir.actions.act_window">
<field name="name">Awaiting Funding</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'awaiting_funding')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_approved" model="ir.actions.act_window">
<field name="name">Approved</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'funding_approved')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_pca_received" model="ir.actions.act_window">
<field name="name">PCA Received</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'contract_received')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_in_production" model="ir.actions.act_window">
<field name="name">In Production</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'in_production')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_complete" model="ir.actions.act_window">
<field name="name">Complete</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'project_complete')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_pod_sent" model="ir.actions.act_window">
<field name="name">POD Sent</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'pod_submitted')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_closed" model="ir.actions.act_window">
<field name="name">Closed</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'case_closed')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<!-- MOD Special Status Actions -->
<record id="action_mod_on_hold" model="ir.actions.act_window">
<field name="name">On Hold</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'on_hold')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_denied" model="ir.actions.act_window">
<field name="name">Denied</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'funding_denied')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record>
<record id="action_mod_cancelled" model="ir.actions.act_window">
<field name="name">Cancelled</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,kanban,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'list', 'view_id': ref('view_sale_order_list_mod')}),
(0, 0, {'view_mode': 'kanban', 'view_id': ref('view_sale_order_kanban_mod')})]"/>
<field name="search_view_id" ref="view_sale_order_search_mod"/>
<field name="domain">[('x_fc_sale_type', '=', 'march_of_dimes'), ('x_fc_mod_status', '=', 'cancelled')]</field>
<field name="context">{'default_x_fc_sale_type': 'march_of_dimes'}</field>
</record> </record>
<record id="action_fc_muscular_dystrophy_orders" model="ir.actions.act_window"> <record id="action_fc_muscular_dystrophy_orders" model="ir.actions.act_window">
@@ -1070,6 +1598,90 @@
<field name="context">{'default_move_type': 'out_invoice'}</field> <field name="context">{'default_move_type': 'out_invoice'}</field>
</record> </record>
<!-- ===================================================================== -->
<!-- INVOICE ACTIONS PER FUNDING SOURCE -->
<!-- ===================================================================== -->
<record id="action_adp_client_invoices" model="ir.actions.act_window">
<field name="name">ADP Client Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'adp_client'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_odsp_invoices" model="ir.actions.act_window">
<field name="name">ODSP Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', 'in', ['odsp', 'adp_odsp']), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_mod_invoices" model="ir.actions.act_window">
<field name="name">MOD Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'march_of_dimes'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_wsib_invoices" model="ir.actions.act_window">
<field name="name">WSIB Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'wsib'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_insurance_invoices" model="ir.actions.act_window">
<field name="name">Insurance Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'insurance'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_direct_private_invoices" model="ir.actions.act_window">
<field name="name">Direct/Private Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'direct_private'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_hardship_invoices" model="ir.actions.act_window">
<field name="name">Hardship Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'hardship'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_rental_invoices" model="ir.actions.act_window">
<field name="name">Rental Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'rental'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_muscular_dystrophy_invoices" model="ir.actions.act_window">
<field name="name">Muscular Dystrophy Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'muscular_dystrophy'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<record id="action_other_invoices" model="ir.actions.act_window">
<field name="name">Other Invoices</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('x_fc_invoice_type', '=', 'other'), ('move_type', 'in', ['out_invoice', 'out_refund'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<!-- Open AI Agent Chat --> <!-- Open AI Agent Chat -->
<record id="action_fc_open_ai_chat" model="ir.actions.server"> <record id="action_fc_open_ai_chat" model="ir.actions.server">
<field name="name">Ask Fusion Claims AI</field> <field name="name">Ask Fusion Claims AI</field>
@@ -1102,11 +1714,15 @@ else:
name="Fusion Claims" name="Fusion Claims"
web_icon="fusion_claims,static/description/icon.png" web_icon="fusion_claims,static/description/icon.png"
sequence="30" sequence="30"
groups="group_fusion_claims_user,group_field_technician"/> groups="group_fusion_claims_user,fusion_tasks.group_field_technician"/>
<!-- ===== ALL INVOICES ===== -->
<menuitem id="menu_fc_all_invoices" name="All Invoices" parent="menu_adp_claims_root"
action="action_fc_all_invoices" sequence="3"/>
<!-- ===== LTC MANAGEMENT ===== --> <!-- ===== LTC MANAGEMENT ===== -->
<menuitem id="menu_fc_ltc" <menuitem id="menu_fc_ltc"
name="LTC Management" name="LTC"
parent="menu_adp_claims_root" parent="menu_adp_claims_root"
sequence="5"/> sequence="5"/>
<menuitem id="menu_ltc_overview" <menuitem id="menu_ltc_overview"
@@ -1163,6 +1779,8 @@ else:
action="action_adp_orders_all" sequence="1"/> action="action_adp_orders_all" sequence="1"/>
<menuitem id="menu_adp_invoices" name="ADP Invoices" parent="menu_fc_adp" <menuitem id="menu_adp_invoices" name="ADP Invoices" parent="menu_fc_adp"
action="action_adp_invoices" sequence="2"/> action="action_adp_invoices" sequence="2"/>
<menuitem id="menu_adp_client_invoices" name="ADP Client Invoices" parent="menu_fc_adp"
action="action_adp_client_invoices" sequence="3"/>
<menuitem id="menu_adp_quotations" <menuitem id="menu_adp_quotations"
name="Quotation Stage" name="Quotation Stage"
@@ -1270,14 +1888,139 @@ else:
sequence="25"/> sequence="25"/>
<menuitem id="menu_fc_odsp_all" name="All ODSP Cases" parent="menu_fc_odsp" <menuitem id="menu_fc_odsp_all" name="All ODSP Cases" parent="menu_fc_odsp"
action="action_fc_odsp_orders" sequence="1"/> action="action_fc_odsp_orders" sequence="1"/>
<menuitem id="menu_odsp_invoices" name="ODSP Invoices" parent="menu_fc_odsp"
action="action_odsp_invoices" sequence="2"/>
<!-- ===== ODSP Standard ===== -->
<menuitem id="menu_fc_odsp_standard" name="ODSP Standard" parent="menu_fc_odsp" <menuitem id="menu_fc_odsp_standard" name="ODSP Standard" parent="menu_fc_odsp"
action="action_fc_odsp_standard_orders" sequence="10"/> sequence="10"/>
<menuitem id="menu_odsp_std_all" name="All Standard Cases" parent="menu_fc_odsp_standard"
action="action_fc_odsp_standard_orders" sequence="1"/>
<menuitem id="menu_odsp_std_quotation" name="Quotation" parent="menu_fc_odsp_standard"
action="action_odsp_std_quotation" sequence="10"/>
<menuitem id="menu_odsp_std_submitted" name="Submitted to ODSP" parent="menu_fc_odsp_standard"
action="action_odsp_std_submitted" sequence="12"/>
<menuitem id="menu_odsp_std_pre_approved" name="Pre-Approved" parent="menu_fc_odsp_standard"
action="action_odsp_std_pre_approved" sequence="14"/>
<menuitem id="menu_odsp_std_ready_delivery" name="Ready for Delivery" parent="menu_fc_odsp_standard"
action="action_odsp_std_ready_delivery" sequence="16"/>
<menuitem id="menu_odsp_std_delivered" name="Delivered" parent="menu_fc_odsp_standard"
action="action_odsp_std_delivered" sequence="18"/>
<menuitem id="menu_odsp_std_pod_submitted" name="POD Submitted" parent="menu_fc_odsp_standard"
action="action_odsp_std_pod_submitted" sequence="20"/>
<menuitem id="menu_odsp_std_payment_received" name="Payment Received" parent="menu_fc_odsp_standard"
action="action_odsp_std_payment_received" sequence="22"/>
<menuitem id="menu_odsp_std_case_closed" name="Case Closed" parent="menu_fc_odsp_standard"
action="action_odsp_std_case_closed" sequence="24"/>
<menuitem id="menu_odsp_std_special" name="Special Statuses" parent="menu_fc_odsp_standard"
sequence="50"/>
<menuitem id="menu_odsp_std_on_hold" name="On Hold" parent="menu_odsp_std_special"
action="action_odsp_std_on_hold" sequence="10"/>
<menuitem id="menu_odsp_std_denied" name="Denied" parent="menu_odsp_std_special"
action="action_odsp_std_denied" sequence="20"/>
<menuitem id="menu_odsp_std_cancelled" name="Cancelled" parent="menu_odsp_std_special"
action="action_odsp_std_cancelled" sequence="30"/>
<!-- ===== SA Mobility ===== -->
<menuitem id="menu_fc_odsp_sa_mobility" name="SA Mobility" parent="menu_fc_odsp" <menuitem id="menu_fc_odsp_sa_mobility" name="SA Mobility" parent="menu_fc_odsp"
action="action_fc_odsp_sa_mobility_orders" sequence="20"/> sequence="20"/>
<menuitem id="menu_odsp_sa_all" name="All SA Cases" parent="menu_fc_odsp_sa_mobility"
action="action_fc_odsp_sa_mobility_orders" sequence="1"/>
<menuitem id="menu_odsp_sa_quotation" name="Quotation" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_quotation" sequence="10"/>
<menuitem id="menu_odsp_sa_form_ready" name="SA Form Ready" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_form_ready" sequence="12"/>
<menuitem id="menu_odsp_sa_submitted" name="Submitted to SA" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_submitted" sequence="14"/>
<menuitem id="menu_odsp_sa_pre_approved" name="Pre-Approved" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_pre_approved" sequence="16"/>
<menuitem id="menu_odsp_sa_ready_delivery" name="Ready for Delivery" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_ready_delivery" sequence="18"/>
<menuitem id="menu_odsp_sa_delivered" name="Delivered" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_delivered" sequence="20"/>
<menuitem id="menu_odsp_sa_pod_submitted" name="POD Submitted" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_pod_submitted" sequence="22"/>
<menuitem id="menu_odsp_sa_payment_received" name="Payment Received" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_payment_received" sequence="24"/>
<menuitem id="menu_odsp_sa_case_closed" name="Case Closed" parent="menu_fc_odsp_sa_mobility"
action="action_odsp_sa_case_closed" sequence="26"/>
<menuitem id="menu_odsp_sa_special" name="Special Statuses" parent="menu_fc_odsp_sa_mobility"
sequence="50"/>
<menuitem id="menu_odsp_sa_on_hold" name="On Hold" parent="menu_odsp_sa_special"
action="action_odsp_sa_on_hold" sequence="10"/>
<menuitem id="menu_odsp_sa_denied" name="Denied" parent="menu_odsp_sa_special"
action="action_odsp_sa_denied" sequence="20"/>
<menuitem id="menu_odsp_sa_cancelled" name="Cancelled" parent="menu_odsp_sa_special"
action="action_odsp_sa_cancelled" sequence="30"/>
<!-- ===== Ontario Works ===== -->
<menuitem id="menu_fc_odsp_ontario_works" name="Ontario Works" parent="menu_fc_odsp" <menuitem id="menu_fc_odsp_ontario_works" name="Ontario Works" parent="menu_fc_odsp"
action="action_fc_odsp_ontario_works_orders" sequence="30"/> sequence="30"/>
<menuitem id="menu_fc_march_of_dimes" name="March of Dimes" parent="menu_adp_claims_root" <menuitem id="menu_odsp_ow_all" name="All OW Cases" parent="menu_fc_odsp_ontario_works"
action="action_fc_march_of_dimes_orders" sequence="30"/> action="action_fc_odsp_ontario_works_orders" sequence="1"/>
<menuitem id="menu_odsp_ow_quotation" name="Quotation" parent="menu_fc_odsp_ontario_works"
action="action_odsp_ow_quotation" sequence="10"/>
<menuitem id="menu_odsp_ow_documents_ready" name="Documents Ready" parent="menu_fc_odsp_ontario_works"
action="action_odsp_ow_documents_ready" sequence="12"/>
<menuitem id="menu_odsp_ow_submitted" name="Submitted to OW" parent="menu_fc_odsp_ontario_works"
action="action_odsp_ow_submitted" sequence="14"/>
<menuitem id="menu_odsp_ow_payment_received" name="Payment Received" parent="menu_fc_odsp_ontario_works"
action="action_odsp_ow_payment_received" sequence="16"/>
<menuitem id="menu_odsp_ow_ready_delivery" name="Ready for Delivery" parent="menu_fc_odsp_ontario_works"
action="action_odsp_ow_ready_delivery" sequence="18"/>
<menuitem id="menu_odsp_ow_delivered" name="Delivered" parent="menu_fc_odsp_ontario_works"
action="action_odsp_ow_delivered" sequence="20"/>
<menuitem id="menu_odsp_ow_case_closed" name="Case Closed" parent="menu_fc_odsp_ontario_works"
action="action_odsp_ow_case_closed" sequence="22"/>
<menuitem id="menu_odsp_ow_special" name="Special Statuses" parent="menu_fc_odsp_ontario_works"
sequence="50"/>
<menuitem id="menu_odsp_ow_on_hold" name="On Hold" parent="menu_odsp_ow_special"
action="action_odsp_ow_on_hold" sequence="10"/>
<menuitem id="menu_odsp_ow_denied" name="Denied" parent="menu_odsp_ow_special"
action="action_odsp_ow_denied" sequence="20"/>
<menuitem id="menu_odsp_ow_cancelled" name="Cancelled" parent="menu_odsp_ow_special"
action="action_odsp_ow_cancelled" sequence="30"/>
<menuitem id="menu_fc_march_of_dimes" name="MOD" parent="menu_adp_claims_root"
sequence="30"/>
<menuitem id="menu_mod_all_cases" name="All MOD Cases" parent="menu_fc_march_of_dimes"
action="action_fc_march_of_dimes_orders" sequence="1"/>
<menuitem id="menu_mod_invoices" name="MOD Invoices" parent="menu_fc_march_of_dimes"
action="action_mod_invoices" sequence="2"/>
<menuitem id="menu_mod_schedule_assessment" name="Schedule Assessment" parent="menu_fc_march_of_dimes"
action="action_mod_schedule_assessment" sequence="10"/>
<menuitem id="menu_mod_assessment_booked" name="Assessment Booked" parent="menu_fc_march_of_dimes"
action="action_mod_assessment_booked" sequence="12"/>
<menuitem id="menu_mod_assessment_done" name="Assessment Done" parent="menu_fc_march_of_dimes"
action="action_mod_assessment_done" sequence="14"/>
<menuitem id="menu_mod_processing_drawing" name="Processing Drawing" parent="menu_fc_march_of_dimes"
action="action_mod_processing_drawing" sequence="16"/>
<menuitem id="menu_mod_quote_sent" name="Quote Sent" parent="menu_fc_march_of_dimes"
action="action_mod_quote_sent" sequence="18"/>
<menuitem id="menu_mod_awaiting_funding" name="Awaiting Funding" parent="menu_fc_march_of_dimes"
action="action_mod_awaiting_funding" sequence="20"/>
<menuitem id="menu_mod_approved" name="Approved" parent="menu_fc_march_of_dimes"
action="action_mod_approved" sequence="22"/>
<menuitem id="menu_mod_pca_received" name="PCA Received" parent="menu_fc_march_of_dimes"
action="action_mod_pca_received" sequence="24"/>
<menuitem id="menu_mod_in_production" name="In Production" parent="menu_fc_march_of_dimes"
action="action_mod_in_production" sequence="26"/>
<menuitem id="menu_mod_complete" name="Complete" parent="menu_fc_march_of_dimes"
action="action_mod_complete" sequence="28"/>
<menuitem id="menu_mod_pod_sent" name="POD Sent" parent="menu_fc_march_of_dimes"
action="action_mod_pod_sent" sequence="30"/>
<menuitem id="menu_mod_closed" name="Closed" parent="menu_fc_march_of_dimes"
action="action_mod_closed" sequence="32"/>
<!-- MOD Special Statuses -->
<menuitem id="menu_mod_special_statuses" name="Special Statuses" parent="menu_fc_march_of_dimes"
sequence="50"/>
<menuitem id="menu_mod_on_hold" name="On Hold" parent="menu_mod_special_statuses"
action="action_mod_on_hold" sequence="10"/>
<menuitem id="menu_mod_denied" name="Denied" parent="menu_mod_special_statuses"
action="action_mod_denied" sequence="20"/>
<menuitem id="menu_mod_cancelled" name="Cancelled" parent="menu_mod_special_statuses"
action="action_mod_cancelled" sequence="30"/>
<!-- ===== OTHER FUNDINGS SUBMENU ===== --> <!-- ===== OTHER FUNDINGS SUBMENU ===== -->
<menuitem id="menu_fc_other_fundings" name="Other Fundings" parent="menu_adp_claims_root" <menuitem id="menu_fc_other_fundings" name="Other Fundings" parent="menu_adp_claims_root"
sequence="35"/> sequence="35"/>
@@ -1292,6 +2035,24 @@ else:
<menuitem id="menu_fc_wsib" name="WSIB" parent="menu_fc_other_fundings" <menuitem id="menu_fc_wsib" name="WSIB" parent="menu_fc_other_fundings"
action="action_fc_wsib_orders" sequence="50"/> action="action_fc_wsib_orders" sequence="50"/>
<!-- Invoices submenu under Other Fundings -->
<menuitem id="menu_fc_other_invoices_sep" name="Invoices" parent="menu_fc_other_fundings"
sequence="60"/>
<menuitem id="menu_wsib_invoices" name="WSIB Invoices" parent="menu_fc_other_invoices_sep"
action="action_wsib_invoices" sequence="10"/>
<menuitem id="menu_insurance_invoices" name="Insurance Invoices" parent="menu_fc_other_invoices_sep"
action="action_insurance_invoices" sequence="20"/>
<menuitem id="menu_direct_private_invoices" name="Direct/Private Invoices" parent="menu_fc_other_invoices_sep"
action="action_direct_private_invoices" sequence="30"/>
<menuitem id="menu_hardship_invoices" name="Hardship Invoices" parent="menu_fc_other_invoices_sep"
action="action_hardship_invoices" sequence="40"/>
<menuitem id="menu_rental_invoices" name="Rental Invoices" parent="menu_fc_other_invoices_sep"
action="action_rental_invoices" sequence="50"/>
<menuitem id="menu_muscular_dystrophy_invoices" name="Muscular Dystrophy Invoices" parent="menu_fc_other_invoices_sep"
action="action_muscular_dystrophy_invoices" sequence="60"/>
<menuitem id="menu_other_type_invoices" name="Other Invoices" parent="menu_fc_other_invoices_sep"
action="action_other_invoices" sequence="70"/>
<!-- ===== CLIENT INTELLIGENCE ===== --> <!-- ===== CLIENT INTELLIGENCE ===== -->
<menuitem id="menu_fc_client_intelligence" <menuitem id="menu_fc_client_intelligence"
name="Client Intelligence" name="Client Intelligence"

View File

@@ -409,7 +409,7 @@
<!-- ===================================================================== --> <!-- ===================================================================== -->
<menuitem id="menu_loaner_root" <menuitem id="menu_loaner_root"
name="Loaner Management" name="Loaners"
parent="menu_adp_claims_root" parent="menu_adp_claims_root"
sequence="58"/> sequence="58"/>

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_page11_sign_request_list" model="ir.ui.view">
<field name="name">fusion.page11.sign.request.list</field>
<field name="model">fusion.page11.sign.request</field>
<field name="arch" type="xml">
<list>
<field name="sale_order_id"/>
<field name="signer_name"/>
<field name="signer_email"/>
<field name="signer_type"/>
<field name="sent_date"/>
<field name="signed_date"/>
<field name="expiry_date"/>
<field name="state" widget="badge"
decoration-success="state == 'signed'"
decoration-info="state == 'sent'"
decoration-warning="state == 'expired'"
decoration-danger="state == 'cancelled'"/>
</list>
</field>
</record>
<record id="view_page11_sign_request_form" model="ir.ui.view">
<field name="name">fusion.page11.sign.request.form</field>
<field name="model">fusion.page11.sign.request</field>
<field name="arch" type="xml">
<form string="Page 11 Signing Request">
<header>
<button name="action_resend" type="object" string="Resend Email"
class="btn-primary" invisible="state not in ('sent', 'expired')"/>
<button name="action_request_new_signature" type="object"
string="Request New Signature"
class="btn-warning" invisible="state not in ('signed', 'cancelled')"
confirm="This will cancel the current signed version and open a new signing request. Continue?"/>
<button name="action_cancel" type="object" string="Cancel"
class="btn-secondary" invisible="state not in ('draft', 'sent')"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,sent,signed"/>
</header>
<sheet>
<group>
<group string="Request Details">
<field name="sale_order_id" readonly="1"/>
<field name="signer_name"/>
<field name="signer_email"/>
<field name="signer_type"/>
<field name="signer_relationship"
invisible="signer_type == 'client'"/>
</group>
<group string="Dates">
<field name="sent_date" readonly="1"/>
<field name="expiry_date"/>
<field name="signed_date" readonly="1"/>
</group>
</group>
<group string="Consent" invisible="state != 'signed'">
<field name="consent_signed_by"/>
<field name="consent_declaration_accepted"/>
</group>
<group string="Agent Details"
invisible="consent_signed_by != 'agent' or state != 'signed'">
<group>
<field name="agent_first_name"/>
<field name="agent_last_name"/>
<field name="agent_phone"/>
</group>
<group>
<field name="agent_street"/>
<field name="agent_city"/>
<field name="agent_province"/>
<field name="agent_postal_code"/>
</group>
</group>
<group string="Signature" invisible="state != 'signed'">
<field name="signature_data" widget="image" readonly="1"/>
</group>
<group string="Signed PDF" invisible="state != 'signed' or not signed_pdf">
<field name="signed_pdf" filename="signed_pdf_filename"/>
<field name="signed_pdf_filename" invisible="1"/>
</group>
<group string="Custom Message" invisible="not custom_message">
<field name="custom_message" readonly="1" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@@ -194,26 +194,6 @@
</div> </div>
</div> </div>
<h2>External APIs</h2>
<div class="row mt-4 o_settings_container">
<!-- Google Maps API Key -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Google Maps API</span>
<div class="text-muted">
API key for Google Maps Places autocomplete in address fields (accessibility assessments, etc.)
</div>
<div class="mt-2">
<field name="fc_google_maps_api_key" placeholder="Enter your Google Maps API Key" password="True"/>
</div>
<div class="alert alert-info mt-2" role="alert">
<i class="fa fa-info-circle"/> Enable the "Places API" in your Google Cloud Console for address autocomplete.
</div>
</div>
</div>
</div>
<h2>AI Client Intelligence</h2> <h2>AI Client Intelligence</h2>
<div class="row mt-4 o_settings_container"> <div class="row mt-4 o_settings_container">
@@ -256,117 +236,6 @@
</div> </div>
</div> </div>
<h2>Technician Management</h2>
<div class="row mt-4 o_settings_container">
<!-- Store Hours -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Store / Scheduling Hours</span>
<div class="text-muted">
Operating hours for technician task scheduling. Tasks can only be booked
within these hours. Calendar view is also restricted to this range.
</div>
<div class="mt-2 d-flex align-items-center gap-2">
<field name="fc_store_open_hour" widget="float_time" style="max-width: 100px;"/>
<span>to</span>
<field name="fc_store_close_hour" widget="float_time" style="max-width: 100px;"/>
</div>
</div>
</div>
<!-- Distance Matrix Toggle -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="fc_google_distance_matrix_enabled"/>
</div>
<div class="o_setting_right_pane">
<label for="fc_google_distance_matrix_enabled"/>
<div class="text-muted">
Calculate travel time between technician tasks using Google Distance Matrix API.
Requires Google Maps API key above with Distance Matrix API enabled.
</div>
</div>
</div>
<!-- Start Address (Company Default / Fallback) -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Default HQ / Fallback Address</span>
<div class="text-muted">
Company default start location used when a technician has no personal
start address set. Each technician can set their own start location
in their user profile or from the portal.
</div>
<div class="mt-2">
<field name="fc_technician_start_address" placeholder="e.g. 123 Main St, Brampton, ON"/>
</div>
</div>
</div>
<!-- Location History Retention -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Location History Retention</span>
<div class="text-muted">
How many days to keep technician GPS location history before automatic cleanup.
</div>
<div class="mt-2 d-flex align-items-center gap-2">
<field name="fc_location_retention_days" placeholder="30" style="max-width: 80px;"/>
<span class="text-muted">days</span>
</div>
<div class="text-muted small mt-1">
Leave empty = 30 days. Enter 0 = delete at end of each day. 1+ = keep that many days.
</div>
</div>
</div>
</div>
<h2>Push Notifications</h2>
<div class="row mt-4 o_settings_container">
<!-- Push Enable -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="fc_push_enabled"/>
</div>
<div class="o_setting_right_pane">
<label for="fc_push_enabled"/>
<div class="text-muted">
Send web push notifications to technicians about upcoming tasks.
Requires VAPID keys (auto-generated on first save if empty).
</div>
</div>
</div>
<!-- Advance Minutes -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">Notification Advance Time</span>
<div class="text-muted">
Send push notification this many minutes before a scheduled task.
</div>
<div class="mt-2">
<field name="fc_push_advance_minutes"/> minutes
</div>
</div>
</div>
<!-- VAPID Public Key -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">VAPID Public Key</span>
<div class="mt-2">
<field name="fc_vapid_public_key" placeholder="Auto-generated"/>
</div>
</div>
</div>
<!-- VAPID Private Key -->
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<span class="o_form_label">VAPID Private Key</span>
<div class="mt-2">
<field name="fc_vapid_private_key" password="True" placeholder="Auto-generated"/>
</div>
</div>
</div>
</div>
<h2>March of Dimes</h2> <h2>March of Dimes</h2>
<div class="row mt-4 o_settings_container"> <div class="row mt-4 o_settings_container">

View File

@@ -1088,6 +1088,13 @@
invisible="x_fc_technician_task_count == 0"> invisible="x_fc_technician_task_count == 0">
<field name="x_fc_technician_task_count" widget="statinfo" string="Tasks"/> <field name="x_fc_technician_task_count" widget="statinfo" string="Tasks"/>
</button> </button>
<!-- Page 11 Signing Requests -->
<button name="action_view_page11_requests" type="object"
class="oe_stat_button" icon="fa-pencil-square-o"
invisible="page11_sign_request_count == 0">
<field name="page11_sign_request_count" widget="statinfo" string="Page 11 Requests"/>
</button>
</xpath> </xpath>
</field> </field>
</record> </record>
@@ -1201,6 +1208,18 @@
invisible="not x_fc_is_adp_sale or x_fc_adp_application_status not in ('quotation', 'assessment_scheduled')" invisible="not x_fc_is_adp_sale or x_fc_adp_application_status not in ('quotation', 'assessment_scheduled')"
help="Mark assessment as completed (override available from Quotation stage)"/> help="Mark assessment as completed (override available from Quotation stage)"/>
<!-- Request Page 11 Remote Signature (before Application Received) -->
<button name="action_request_page11_signature" type="object"
string="Request Page 11 Signature" class="btn-warning"
icon="fa-pencil-square-o"
invisible="not x_fc_is_adp_sale or x_fc_adp_application_status not in ('assessment_completed', 'waiting_for_application', 'application_received') or x_fc_signed_pages_11_12"
help="Send Page 11 to a family member or agent for remote digital signing"/>
<button name="action_request_page11_signature" type="object"
string="Re-sign Page 11" class="btn-secondary"
icon="fa-repeat"
invisible="not x_fc_is_adp_sale or x_fc_adp_application_status not in ('assessment_completed', 'waiting_for_application', 'application_received') or not x_fc_signed_pages_11_12"
help="Page 11 already signed. Click to request a new signature."/>
<!-- Waiting for Application -> Application Received --> <!-- Waiting for Application -> Application Received -->
<button name="action_application_received" type="object" <button name="action_application_received" type="object"
string="Application Received" class="btn-info" string="Application Received" class="btn-info"
@@ -1274,12 +1293,19 @@
invisible="not x_fc_is_adp_sale or x_fc_adp_application_status not in ('submitted', 'resubmitted', 'needs_correction', 'accepted', 'approved', 'approved_deduction', 'ready_delivery', 'ready_bill')" invisible="not x_fc_is_adp_sale or x_fc_adp_application_status not in ('submitted', 'resubmitted', 'needs_correction', 'accepted', 'approved', 'approved_deduction', 'ready_delivery', 'ready_bill')"
help="Put this application on hold"/> help="Put this application on hold"/>
<button name="%(fusion_claims.action_set_status_withdrawn)d" <button name="%(fusion_claims.action_set_status_withdrawn)d"
type="action" string="Withdraw" class="btn-secondary" type="action" string="Withdraw" class="btn-secondary"
icon="fa-undo" icon="fa-undo"
invisible="not x_fc_is_adp_sale or x_fc_adp_application_status not in ('submitted', 'resubmitted', 'needs_correction', 'accepted', 'approved', 'approved_deduction', 'ready_bill')" invisible="not x_fc_is_adp_sale or x_fc_adp_application_status not in ('submitted', 'resubmitted', 'needs_correction', 'accepted', 'approved', 'approved_deduction', 'ready_bill')"
help="Withdraw this application"/> help="Withdraw this application"/>
<button name="action_resubmit_from_withdrawn" type="object"
string="Resubmit Application" class="btn-primary"
icon="fa-repeat"
invisible="not x_fc_is_adp_sale or x_fc_adp_application_status != 'withdrawn'"
confirm="This will return the application to Ready for Submission status. Continue?"
help="Return this withdrawn application to Ready for Submission"/>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- REVIEW BUTTONS (color changes based on verified/approved) --> <!-- REVIEW BUTTONS (color changes based on verified/approved) -->
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -1493,10 +1519,16 @@
icon="fa-pause" icon="fa-pause"
invisible="x_fc_adp_application_status in ('on_hold', 'denied', 'withdrawn', 'cancelled', 'case_closed')"/> invisible="x_fc_adp_application_status in ('on_hold', 'denied', 'withdrawn', 'cancelled', 'case_closed')"/>
<button name="action_resume_from_hold" type="object" <button name="action_resume_from_hold" type="object"
string="Resume" string="Resume"
class="btn-success btn-sm me-1" class="btn-success btn-sm me-1"
icon="fa-play" icon="fa-play"
invisible="x_fc_adp_application_status != 'on_hold'"/> invisible="x_fc_adp_application_status != 'on_hold'"/>
<button name="action_resubmit_from_withdrawn" type="object"
string="Resubmit Application"
class="btn-primary btn-sm me-1"
icon="fa-repeat"
invisible="x_fc_adp_application_status != 'withdrawn'"
confirm="This will return the application to Ready for Submission status. Continue?"/>
<button name="%(fusion_claims.action_set_status_withdrawn)d" <button name="%(fusion_claims.action_set_status_withdrawn)d"
type="action" string="Withdraw" type="action" string="Withdraw"
class="btn-secondary btn-sm me-1" class="btn-secondary btn-sm me-1"
@@ -1815,8 +1847,14 @@
widget="binary" nolabel="1" class="fc-tile-upload-field" widget="binary" nolabel="1" class="fc-tile-upload-field"
required="x_fc_is_adp_sale and x_fc_adp_application_status not in ('quotation', 'assessment_scheduled', 'assessment_completed', 'waiting_for_application', 'application_received')" required="x_fc_is_adp_sale and x_fc_adp_application_status not in ('quotation', 'assessment_scheduled', 'assessment_completed', 'waiting_for_application', 'application_received')"
readonly="x_fc_case_locked"/> readonly="x_fc_case_locked"/>
<button name="action_request_page11_signature" type="object"
class="btn btn-sm btn-outline-primary mt-1"
string="Request Signature"
title="Send Page 11 to a family member or agent for remote signing"
invisible="x_fc_signed_pages_11_12 or not x_fc_is_adp_sale"/>
</div> </div>
</div> </div>
<field name="page11_sign_status" invisible="1"/>
</div> </div>
</div> </div>
</div> </div>
@@ -2105,22 +2143,6 @@
</div> </div>
</div> </div>
<!-- SYNC TO INVOICES - Shows when there are invoices to sync -->
<div class="alert alert-secondary mb-3" role="alert"
invisible="not x_fc_has_adp_invoice and not x_fc_has_client_invoice">
<div class="d-flex align-items-center justify-content-between">
<div>
<strong><i class="fa fa-refresh"/> Sync ADP Fields</strong>
<p class="mb-0 small text-muted">
Push claim number, client references, dates, and serial numbers from this order to linked invoices.
</p>
</div>
<button name="action_sync_adp_fields" type="object"
string="Sync to Invoices" class="btn btn-secondary btn-sm ms-3"
icon="fa-refresh"/>
</div>
</div>
<!-- DEDUCTION ALERT - Only show when there are deductions --> <!-- DEDUCTION ALERT - Only show when there are deductions -->
<field name="x_fc_has_deductions" invisible="1"/> <field name="x_fc_has_deductions" invisible="1"/>
<field name="x_fc_total_deduction_amount" invisible="1"/> <field name="x_fc_total_deduction_amount" invisible="1"/>

View File

@@ -1,541 +1,156 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024-2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Claims-specific extensions to the base technician task views
defined in fusion_tasks. Adds SO/PO/facility/rental fields.
-->
<odoo> <odoo>
<!-- ================================================================== --> <!-- ================================================================== -->
<!-- SEQUENCE --> <!-- SEARCH VIEW EXTENSION -->
<!-- ================================================================== --> <!-- ================================================================== -->
<record id="seq_technician_task" model="ir.sequence"> <record id="view_technician_task_search_claims" model="ir.ui.view">
<field name="name">Technician Task</field> <field name="name">fusion.technician.task.search.claims</field>
<field name="code">fusion.technician.task</field> <field name="model">fusion.technician.task</field>
<field name="prefix">TASK-</field> <field name="inherit_id" ref="fusion_tasks.view_technician_task_search"/>
<field name="padding">5</field>
<field name="number_increment">1</field>
</record>
<!-- ================================================================== -->
<!-- RES.USERS FORM EXTENSION - Field Staff toggle -->
<!-- ================================================================== -->
<record id="view_users_form_field_staff" model="ir.ui.view">
<field name="name">res.users.form.field.staff</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='share']" position="after"> <xpath expr="//filter[@name='filter_pod']" position="after">
<field name="x_fc_is_field_staff"/> <filter string="Has Purchase Order" name="has_po"
<field name="x_fc_start_address" domain="[('purchase_order_id', '!=', False)]"/>
invisible="not x_fc_is_field_staff"
placeholder="e.g. 123 Main St, Brampton, ON"/>
<field name="x_fc_tech_sync_id"
invisible="not x_fc_is_field_staff"
placeholder="e.g. gordy, manpreet"/>
</xpath> </xpath>
</field> </field>
</record> </record>
<!-- ================================================================== --> <!-- ================================================================== -->
<!-- SEARCH VIEW --> <!-- FORM VIEW EXTENSION -->
<!-- ================================================================== --> <!-- ================================================================== -->
<record id="view_technician_task_search" model="ir.ui.view"> <record id="view_technician_task_form_claims" model="ir.ui.view">
<field name="name">fusion.technician.task.search</field> <field name="name">fusion.technician.task.form.claims</field>
<field name="model">fusion.technician.task</field> <field name="model">fusion.technician.task</field>
<field name="inherit_id" ref="fusion_tasks.view_technician_task_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Search Tasks"> <!-- Stat buttons: View Case + Purchase Order -->
<!-- Quick Filters --> <xpath expr="//div[@name='button_box']" position="inside">
<filter string="Today" name="filter_today" <button name="action_view_sale_order" type="object"
domain="[('scheduled_date', '=', context_today().strftime('%Y-%m-%d'))]"/> class="oe_stat_button" icon="fa-file-text-o"
<filter string="Tomorrow" name="filter_tomorrow" invisible="not sale_order_id">
domain="[('scheduled_date', '=', (context_today() + datetime.timedelta(days=1)).strftime('%Y-%m-%d'))]"/> <div class="o_field_widget o_stat_info">
<filter string="This Week" name="filter_this_week" <span class="o_stat_text">View Case</span>
domain="[('scheduled_date', '>=', (context_today() - datetime.timedelta(days=context_today().weekday())).strftime('%Y-%m-%d')), </div>
('scheduled_date', '&lt;=', (context_today() + datetime.timedelta(days=6-context_today().weekday())).strftime('%Y-%m-%d'))]"/> </button>
<separator/> <button name="action_view_purchase_order" type="object"
<filter string="Pending" name="filter_pending" domain="[('status', '=', 'pending')]"/> class="oe_stat_button" icon="fa-shopping-cart"
<filter string="Scheduled" name="filter_scheduled" domain="[('status', '=', 'scheduled')]"/> invisible="not purchase_order_id">
<filter string="En Route" name="filter_en_route" domain="[('status', '=', 'en_route')]"/> <div class="o_field_widget o_stat_info">
<filter string="In Progress" name="filter_in_progress" domain="[('status', '=', 'in_progress')]"/> <span class="o_stat_text">Purchase Order</span>
<filter string="Completed" name="filter_completed" domain="[('status', '=', 'completed')]"/> </div>
<filter string="Active" name="filter_active" domain="[('status', 'not in', ['cancelled', 'completed'])]"/> </button>
<separator/> </xpath>
<filter string="My Tasks" name="filter_my_tasks"
domain="['|', ('technician_id', '=', uid), ('additional_technician_ids', 'in', [uid])]"/> <!-- Add facility_id, sale_order_id, purchase_order_id after priority -->
<filter string="Deliveries" name="filter_deliveries" domain="[('task_type', '=', 'delivery')]"/> <xpath expr="//field[@name='priority']" position="after">
<filter string="Repairs" name="filter_repairs" domain="[('task_type', '=', 'repair')]"/> <field name="facility_id"
<filter string="POD Required" name="filter_pod" domain="[('pod_required', '=', True)]"/> invisible="task_type != 'ltc_visit'"/>
<filter string="Has Purchase Order" name="has_po" <field name="sale_order_id"
domain="[('purchase_order_id', '!=', False)]"/> invisible="task_type == 'ltc_visit'"/>
<separator/> <field name="purchase_order_id"
<filter string="Local Tasks" name="filter_local" invisible="task_type == 'ltc_visit'"/>
domain="[('x_fc_sync_source', '=', False)]"/> </xpath>
<filter string="Synced Tasks" name="filter_synced"
domain="[('x_fc_sync_source', '!=', False)]"/> <!-- Add Rental Inspection tab after Completion tab -->
<separator/> <xpath expr="//page[@name='completion']" position="after">
<!-- Group By --> <page string="Rental Inspection" name="rental_inspection"
<filter string="Technician" name="group_technician" context="{'group_by': 'technician_id'}"/> invisible="task_type != 'pickup'">
<filter string="Date" name="group_date" context="{'group_by': 'scheduled_date'}"/> <group>
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/> <group string="Condition">
<filter string="Task Type" name="group_type" context="{'group_by': 'task_type'}"/> <field name="rental_inspection_condition"/>
<filter string="Client" name="group_client" context="{'group_by': 'partner_id'}"/> <field name="rental_inspection_completed"/>
</search> </group>
</group>
<group string="Inspection Notes">
<field name="rental_inspection_notes" nolabel="1"/>
</group>
<group string="Inspection Photos">
<field name="rental_inspection_photo_ids"
widget="many2many_binary" nolabel="1"/>
</group>
</page>
</xpath>
</field> </field>
</record> </record>
<!-- ================================================================== --> <!-- ================================================================== -->
<!-- FORM VIEW --> <!-- LIST VIEW EXTENSION -->
<!-- ================================================================== --> <!-- ================================================================== -->
<record id="view_technician_task_form" model="ir.ui.view"> <record id="view_technician_task_list_claims" model="ir.ui.view">
<field name="name">fusion.technician.task.form</field> <field name="name">fusion.technician.task.list.claims</field>
<field name="model">fusion.technician.task</field> <field name="model">fusion.technician.task</field>
<field name="inherit_id" ref="fusion_tasks.view_technician_task_list"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Technician Task"> <xpath expr="//field[@name='pod_required']" position="after">
<field name="x_fc_is_shadow" invisible="1"/>
<field name="x_fc_sync_source" invisible="1"/>
<header>
<button name="action_start_en_route" type="object" string="En Route"
class="btn-primary" invisible="status != 'scheduled' or x_fc_is_shadow"/>
<button name="action_start_task" type="object" string="Start Task"
class="btn-primary" invisible="status not in ('scheduled', 'en_route') or x_fc_is_shadow"/>
<button name="action_complete_task" type="object" string="Complete"
class="btn-success" invisible="status not in ('in_progress', 'en_route') or x_fc_is_shadow"/>
<button name="action_reschedule" type="object" string="Reschedule"
class="btn-warning" invisible="status not in ('scheduled', 'en_route') or x_fc_is_shadow"/>
<button name="action_cancel_task" type="object" string="Cancel"
class="btn-danger" invisible="status in ('completed', 'cancelled') or x_fc_is_shadow"
confirm="Are you sure you want to cancel this task?"/>
<button name="action_reset_to_scheduled" type="object" string="Reset to Scheduled"
invisible="status not in ('cancelled', 'rescheduled') or x_fc_is_shadow"/>
<button string="Calculate Travel"
class="btn-secondary o_fc_calculate_travel" icon="fa-car"
invisible="x_fc_is_shadow"/>
<field name="status" widget="statusbar"
statusbar_visible="pending,scheduled,en_route,in_progress,completed"/>
</header>
<sheet>
<!-- Shadow task banner -->
<div class="alert alert-info text-center" role="alert"
invisible="not x_fc_is_shadow">
<strong><i class="fa fa-link"/> This task is synced from
<field name="x_fc_sync_source" readonly="1" nolabel="1" class="d-inline"/>
— view only.</strong>
</div>
<div class="oe_button_box" name="button_box">
<button name="action_view_sale_order" type="object"
class="oe_stat_button" icon="fa-file-text-o"
invisible="not sale_order_id">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">View Case</span>
</div>
</button>
<button name="action_view_purchase_order" type="object"
class="oe_stat_button" icon="fa-shopping-cart"
invisible="not purchase_order_id">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Purchase Order</span>
</div>
</button>
</div>
<widget name="web_ribbon" title="Completed" bg_color="text-bg-success"
invisible="status != 'completed'"/>
<widget name="web_ribbon" title="Cancelled" bg_color="text-bg-danger"
invisible="status != 'cancelled'"/>
<widget name="web_ribbon" title="Synced" bg_color="text-bg-info"
invisible="not x_fc_is_shadow or status in ('completed', 'cancelled')"/>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<!-- Schedule Info Banner -->
<field name="schedule_info_html" nolabel="1" colspan="2"
invisible="not technician_id or not scheduled_date"/>
<!-- Previous Task / Travel Warning Banner -->
<field name="prev_task_summary_html" nolabel="1" colspan="2"
invisible="not technician_id or not scheduled_date"/>
<!-- Hidden fields for calendar sync and legacy -->
<field name="datetime_start" invisible="1"/>
<field name="datetime_end" invisible="1"/>
<field name="time_start_12h" invisible="1"/>
<field name="time_end_12h" invisible="1"/>
<group>
<group string="Assignment">
<field name="technician_id"
domain="[('x_fc_is_field_staff', '=', True)]"/>
<field name="additional_technician_ids"
widget="many2many_tags_avatar"
domain="[('x_fc_is_field_staff', '=', True), ('id', '!=', technician_id)]"
options="{'color_field': 'color'}"/>
<field name="task_type"/>
<field name="priority" widget="priority"/>
<field name="facility_id"
invisible="task_type != 'ltc_visit'"/>
<field name="sale_order_id"
invisible="task_type == 'ltc_visit'"/>
<field name="purchase_order_id"
invisible="task_type == 'ltc_visit'"/>
</group>
<group string="Schedule">
<field name="scheduled_date"/>
<field name="time_start" widget="float_time"
string="Start Time"/>
<field name="duration_hours" widget="float_time"
string="Duration"/>
<field name="time_end" widget="float_time"
string="End Time" readonly="1"
force_save="1"/>
</group>
</group>
<group>
<group string="Client">
<field name="partner_id"/>
<field name="partner_phone" widget="phone"/>
</group>
<group string="Location">
<field name="is_in_store"/>
<field name="address_partner_id" invisible="is_in_store"/>
<field name="address_street" readonly="is_in_store"/>
<field name="address_street2" string="Unit/Suite #" invisible="is_in_store"/>
<field name="address_buzz_code" invisible="is_in_store"/>
<field name="address_city" invisible="1"/>
<field name="address_state_id" invisible="1"/>
<field name="address_zip" invisible="1"/>
<field name="address_lat" invisible="1"/>
<field name="address_lng" invisible="1"/>
</group>
</group>
<group>
<group string="Travel (Auto-Calculated)">
<field name="travel_time_minutes" readonly="1"/>
<field name="travel_distance_km" readonly="1"/>
<field name="travel_origin" readonly="1"/>
<field name="previous_task_id" readonly="1"/>
</group>
<group string="Options">
<field name="pod_required"/>
<field name="active" invisible="1"/>
</group>
</group>
<notebook>
<page string="Description" name="description">
<group>
<field name="description" placeholder="What needs to be done..."/>
</group>
<group>
<field name="equipment_needed" placeholder="Tools, parts, materials..."/>
</group>
</page>
<page string="Completion" name="completion">
<group>
<field name="completion_datetime"/>
<field name="completion_notes"/>
</group>
<group>
<field name="voice_note_transcription"/>
</group>
</page>
<page string="Rental Inspection" name="rental_inspection"
invisible="task_type != 'pickup'">
<group>
<group string="Condition">
<field name="rental_inspection_condition"/>
<field name="rental_inspection_completed"/>
</group>
</group>
<group string="Inspection Notes">
<field name="rental_inspection_notes" nolabel="1"/>
</group>
<group string="Inspection Photos">
<field name="rental_inspection_photo_ids"
widget="many2many_binary" nolabel="1"/>
</group>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- ================================================================== -->
<!-- LIST VIEW -->
<!-- ================================================================== -->
<record id="view_technician_task_list" model="ir.ui.view">
<field name="name">fusion.technician.task.list</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<list string="Technician Tasks" decoration-success="status == 'completed'"
decoration-warning="status == 'in_progress'"
decoration-info="status == 'en_route'"
decoration-danger="status == 'cancelled'"
decoration-muted="status == 'rescheduled'"
default_order="scheduled_date, sequence, time_start">
<field name="name"/>
<field name="technician_id" widget="many2one_avatar_user"/>
<field name="additional_technician_ids" widget="many2many_tags_avatar"
optional="show" string="+ Techs"/>
<field name="task_type" decoration-bf="1"/>
<field name="scheduled_date"/>
<field name="time_start_display" string="Start"/>
<field name="time_end_display" string="End"/>
<field name="partner_id"/>
<field name="address_city"/>
<field name="travel_time_minutes" string="Travel (min)" optional="show"/>
<field name="status" widget="badge"
decoration-success="status == 'completed'"
decoration-warning="status == 'in_progress'"
decoration-info="status in ('scheduled', 'en_route')"
decoration-danger="status == 'cancelled'"/>
<field name="priority" widget="priority" optional="hide"/>
<field name="pod_required" optional="hide"/>
<field name="sale_order_id" optional="hide"/> <field name="sale_order_id" optional="hide"/>
<field name="purchase_order_id" optional="hide"/> <field name="purchase_order_id" optional="hide"/>
<field name="x_fc_source_label" string="Source" optional="show" </xpath>
widget="badge" decoration-info="x_fc_is_shadow"
decoration-success="not x_fc_is_shadow"/>
</list>
</field> </field>
</record> </record>
<!-- ================================================================== --> <!-- ================================================================== -->
<!-- KANBAN VIEW --> <!-- MENU ITEMS - Field Service under Claims app -->
<!-- ================================================================== -->
<record id="view_technician_task_kanban" model="ir.ui.view">
<field name="name">fusion.technician.task.kanban</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<kanban default_group_by="status" class="o_kanban_small_column"
records_draggable="1" group_create="0">
<field name="color"/>
<field name="priority"/>
<field name="technician_id"/>
<field name="additional_technician_ids"/>
<field name="additional_tech_count"/>
<field name="partner_id"/>
<field name="task_type"/>
<field name="scheduled_date"/>
<field name="time_start_display"/>
<field name="address_city"/>
<field name="travel_time_minutes"/>
<field name="status"/>
<field name="x_fc_is_shadow"/>
<field name="x_fc_sync_client_name"/>
<templates>
<t t-name="card">
<div t-attf-class="oe_kanban_color_#{record.color.raw_value} oe_kanban_card oe_kanban_global_click">
<div class="oe_kanban_content">
<div class="o_kanban_record_top mb-1">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<field name="name"/>
</strong>
</div>
<field name="priority" widget="priority"/>
</div>
<div class="mb-1">
<span class="badge bg-primary me-1"><field name="task_type"/></span>
<span class="text-muted"><field name="scheduled_date"/> - <field name="time_start_display"/></span>
</div>
<div class="mb-1">
<i class="fa fa-user me-1"/>
<t t-if="record.x_fc_is_shadow.raw_value">
<span t-out="record.x_fc_sync_client_name.value"/>
</t>
<t t-else="">
<field name="partner_id"/>
</t>
</div>
<div class="text-muted small" t-if="record.address_city.raw_value">
<i class="fa fa-map-marker me-1"/><field name="address_city"/>
<t t-if="record.travel_time_minutes.raw_value">
<span class="ms-2"><i class="fa fa-car me-1"/><field name="travel_time_minutes"/> min</span>
</t>
</div>
<div t-if="record.additional_tech_count.raw_value > 0" class="text-muted small mb-1">
<i class="fa fa-users me-1"/>
<span>+<field name="additional_tech_count"/> technician(s)</span>
</div>
<div class="o_kanban_record_bottom mt-2">
<div class="oe_kanban_bottom_left">
<field name="activity_ids" widget="kanban_activity"/>
</div>
<div class="oe_kanban_bottom_right">
<field name="technician_id" widget="many2one_avatar_user"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- ================================================================== -->
<!-- CALENDAR VIEW -->
<!-- ================================================================== -->
<record id="view_technician_task_calendar" model="ir.ui.view">
<field name="name">fusion.technician.task.calendar</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<calendar string="Technician Schedule"
date_start="datetime_start" date_stop="datetime_end"
color="technician_id" mode="week" event_open_popup="1"
quick_create="0">
<!-- Displayed on the calendar card -->
<field name="partner_id"/>
<field name="x_fc_sync_client_name"/>
<field name="task_type"/>
<field name="time_start_display" string="Start"/>
<field name="time_end_display" string="End"/>
<!-- Popover (hover/click) details -->
<field name="name"/>
<field name="technician_id" avatar_field="image_128"/>
<field name="address_display" string="Address"/>
<field name="travel_time_minutes" string="Travel (min)"/>
<field name="status"/>
<field name="duration_hours" widget="float_time" string="Duration"/>
</calendar>
</field>
</record>
<!-- ================================================================== -->
<!-- MAP VIEW (Enterprise web_map) -->
<!-- ================================================================== -->
<record id="view_technician_task_map" model="ir.ui.view">
<field name="name">fusion.technician.task.map</field>
<field name="model">fusion.technician.task</field>
<field name="arch" type="xml">
<map res_partner="address_partner_id" default_order="time_start"
routing="1" js_class="fusion_task_map">
<field name="partner_id" string="Client"/>
<field name="task_type" string="Type"/>
<field name="technician_id" string="Technician"/>
<field name="time_start_display" string="Start"/>
<field name="time_end_display" string="End"/>
<field name="status" string="Status"/>
<field name="travel_time_minutes" string="Travel (min)"/>
</map>
</field>
</record>
<!-- ================================================================== -->
<!-- ACTIONS -->
<!-- ================================================================== --> <!-- ================================================================== -->
<!-- Main Tasks Action (List/Kanban) --> <!-- Field Service parent menu under Claims -->
<record id="action_technician_tasks" model="ir.actions.act_window">
<field name="name">Technician Tasks</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">list,kanban,form,calendar,map</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_active': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first technician task
</p>
<p>Schedule deliveries, repairs, and other field tasks for your technicians.</p>
</field>
</record>
<!-- Schedule Action (Map default) -->
<record id="action_technician_schedule" model="ir.actions.act_window">
<field name="name">Schedule</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">map,calendar,list,kanban,form</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_active': 1}</field>
</record>
<!-- Map View Action (for app landing page) -->
<record id="action_technician_map_view" model="ir.actions.act_window">
<field name="name">Delivery Map</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">map,list,kanban,form,calendar</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_active': 1}</field>
</record>
<!-- Today's Tasks Action -->
<record id="action_technician_tasks_today" model="ir.actions.act_window">
<field name="name">Today's Tasks</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">kanban,list,form,map</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_today': 1, 'search_default_filter_active': 1}</field>
</record>
<!-- My Tasks Action -->
<record id="action_technician_my_tasks" model="ir.actions.act_window">
<field name="name">My Tasks</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">list,kanban,form,calendar,map</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_my_tasks': 1, 'search_default_filter_active': 1}</field>
</record>
<!-- Pending Tasks Action -->
<record id="action_technician_tasks_pending" model="ir.actions.act_window">
<field name="name">Pending Tasks</field>
<field name="res_model">fusion.technician.task</field>
<field name="view_mode">list,kanban,form</field>
<field name="search_view_id" ref="view_technician_task_search"/>
<field name="context">{'search_default_filter_pending': 1}</field>
</record>
<!-- ================================================================== -->
<!-- MENU ITEMS -->
<!-- ================================================================== -->
<!-- Field Service - top-level menu (sequence 3 = first child = app default) -->
<menuitem id="menu_technician_management" <menuitem id="menu_technician_management"
name="Field Service" name="Field Service"
parent="fusion_claims.menu_adp_claims_root" parent="fusion_claims.menu_adp_claims_root"
sequence="3" sequence="3"
groups="fusion_claims.group_fusion_claims_user,fusion_claims.group_field_technician"/> groups="fusion_claims.group_fusion_claims_user,fusion_tasks.group_field_technician"/>
<!-- Delivery Map - first item under Field Service = default landing view --> <!-- Delivery Map - first item = default landing view -->
<menuitem id="menu_fc_delivery_map" <menuitem id="menu_fc_delivery_map"
name="Delivery Map" name="Delivery Map"
parent="menu_technician_management" parent="menu_technician_management"
action="action_technician_map_view" action="fusion_tasks.action_technician_map_view"
sequence="5" sequence="5"
groups="fusion_claims.group_fusion_claims_user,fusion_claims.group_field_technician"/> groups="fusion_claims.group_fusion_claims_user,fusion_tasks.group_field_technician"/>
<menuitem id="menu_technician_tasks_today" <menuitem id="menu_technician_tasks_today"
name="Today's Tasks" name="Today's Tasks"
parent="menu_technician_management" parent="menu_technician_management"
action="action_technician_tasks_today" action="fusion_tasks.action_technician_tasks_today"
sequence="10"/> sequence="10"/>
<menuitem id="menu_technician_schedule" <menuitem id="menu_technician_schedule"
name="Schedule" name="Schedule"
parent="menu_technician_management" parent="menu_technician_management"
action="action_technician_schedule" action="fusion_tasks.action_technician_schedule"
sequence="15"/> sequence="15"/>
<menuitem id="menu_technician_tasks_pending" <menuitem id="menu_technician_tasks_pending"
name="Pending Tasks" name="Pending Tasks"
parent="menu_technician_management" parent="menu_technician_management"
action="action_technician_tasks_pending" action="fusion_tasks.action_technician_tasks_pending"
sequence="20"/> sequence="20"/>
<menuitem id="menu_technician_tasks" <menuitem id="menu_technician_tasks"
name="All Tasks" name="All Tasks"
parent="menu_technician_management" parent="menu_technician_management"
action="action_technician_tasks" action="fusion_tasks.action_technician_tasks"
sequence="30"/> sequence="30"/>
<menuitem id="menu_technician_my_tasks" <menuitem id="menu_technician_my_tasks"
name="My Tasks" name="My Tasks"
parent="menu_technician_management" parent="menu_technician_management"
action="action_technician_my_tasks" action="fusion_tasks.action_technician_my_tasks"
sequence="35" sequence="35"
groups="fusion_claims.group_field_technician"/> groups="fusion_tasks.group_field_technician"/>
<!-- Task Sync under Field Service in Claims -->
<menuitem id="menu_task_sync_claims"
name="Task Sync"
parent="menu_technician_management"
action="fusion_tasks.action_task_sync_config"
sequence="99"/>
</odoo> </odoo>

View File

@@ -30,4 +30,5 @@ from . import odsp_discretionary_wizard
from . import odsp_pre_approved_wizard from . import odsp_pre_approved_wizard
from . import odsp_ready_delivery_wizard from . import odsp_ready_delivery_wizard
from . import odsp_submit_to_odsp_wizard from . import odsp_submit_to_odsp_wizard
from . import ltc_repair_create_so_wizard from . import ltc_repair_create_so_wizard
from . import send_page11_wizard

Some files were not shown because too many files have changed in this diff Show More